mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
merge: reconcile paid hosting with latest main
This commit is contained in:
commit
ded6f905fd
180 files changed
+21709
-3614
No files matched your search
+302
-42
@@ -2,6 +2,87 @@ import { setupBilling, readBilling } from "./billing.js";
|
||||
import { reactive, nextTick } from "vue";
|
||||
import { createTimers, createListeners } from "./state.js";
|
||||
import { formDraft } from "./drafts.js";
|
||||
import translations from "../i18n/locale-en.json";
|
||||
|
||||
function viewerErrorCode(error) {
|
||||
let data = error?.data;
|
||||
if (typeof data === "string") {
|
||||
try { data = JSON.parse(data); } catch (_) { data = null; }
|
||||
}
|
||||
if (typeof data?.error === "string" && Object.prototype.hasOwnProperty.call(translations.ERRORS, data.error)) return data.error;
|
||||
if (error?.status === -1 || error?.status === 0) return "request_error";
|
||||
if ([502, 503, 504].includes(error?.status)) return "unreachable";
|
||||
return "unknown_error";
|
||||
}
|
||||
|
||||
// The mask comes from /api/options; mainController sets it once loaded.
|
||||
let redactionMask = "XXXX";
|
||||
export const setRedactionMask = (mask) => { redactionMask = mask || "XXXX"; };
|
||||
|
||||
// Matches the replacements the anonymizer writes: MASK and MASK-<n>.
|
||||
export const redactionPattern = (mask = redactionMask) => {
|
||||
if (!mask) return null;
|
||||
const escaped = mask.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(escaped + "(?:-\\d+)?", "g");
|
||||
};
|
||||
|
||||
// Wraps redacted terms under root in <mark class="redaction">, touching text
|
||||
// nodes only so attributes and URLs stay intact. Mermaid sources are left
|
||||
// alone (Mermaid parses their text), and so are code blocks Prism will
|
||||
// rewrite unless root is that highlighted block itself.
|
||||
export const markRedactionsIn = (root, mask = redactionMask) => {
|
||||
const pattern = redactionPattern(mask);
|
||||
if (!pattern || !root || !root.textContent.includes(mask)) return;
|
||||
const doc = root.ownerDocument;
|
||||
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
for (const node of nodes) {
|
||||
const text = node.nodeValue;
|
||||
const parent = node.parentElement;
|
||||
if (!text.includes(mask) || !parent || parent.closest("script, style, mark.redaction, .mermaid")) continue;
|
||||
const highlighted = parent.closest('code[class*="language-"]');
|
||||
if (highlighted && !highlighted.contains(root)) continue;
|
||||
const fragment = doc.createDocumentFragment();
|
||||
let last = 0;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
fragment.append(text.slice(last, match.index));
|
||||
const mark = doc.createElement("mark");
|
||||
mark.className = "redaction";
|
||||
mark.title = "Redacted by Anonymous GitHub";
|
||||
mark.textContent = match[0];
|
||||
fragment.append(mark);
|
||||
last = match.index + match[0].length;
|
||||
}
|
||||
fragment.append(text.slice(last));
|
||||
node.replaceWith(fragment);
|
||||
}
|
||||
};
|
||||
|
||||
export const highlightRedactions = (html, mask = redactionMask) => {
|
||||
if (!html || !mask || !html.includes(mask)) return html;
|
||||
const doc = new DOMParser().parseFromString("<body>" + html + "</body>", "text/html");
|
||||
markRedactionsIn(doc.body, mask);
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
async function copyText(text) {
|
||||
if (window.navigator.clipboard?.writeText) {
|
||||
await window.navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
const field = document.createElement("textarea");
|
||||
field.value = text;
|
||||
field.setAttribute("readonly", "");
|
||||
field.style.position = "fixed";
|
||||
field.style.opacity = "0";
|
||||
const focused = document.activeElement;
|
||||
document.body.appendChild(field);
|
||||
try {
|
||||
field.select();
|
||||
if (!document.execCommand?.("copy")) throw Error("Copy unavailable");
|
||||
} finally { field.remove(); focused?.focus(); }
|
||||
}
|
||||
|
||||
function previewTabKeydown(event) {
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
||||
@@ -55,8 +136,11 @@ export const mainController = function (state, http, location, timeout) {
|
||||
return toast;
|
||||
};
|
||||
|
||||
// Anonymized content that signed-out reviewers browse.
|
||||
const reviewerRoutes = /^\/(r|repository|pr|gist)\//;
|
||||
state.path = location.url();
|
||||
state.paths = location.path().substring(1).split("/");
|
||||
state.isReviewerRoute = reviewerRoutes.test(location.path());
|
||||
|
||||
state.darkMode = function (on) {
|
||||
localStorage.setItem("darkMode", on);
|
||||
@@ -80,19 +164,49 @@ export const mainController = function (state, http, location, timeout) {
|
||||
}
|
||||
const kofiBtn = document.querySelector("[class*='floatingchat-container-wrap'] [class*='floating-chat-kofi-text-container-wrap']");
|
||||
if (kofiBtn) {
|
||||
kofiBtn.style.backgroundColor = on ? "#FAF9F6" : "#1A1815";
|
||||
kofiBtn.style.color = on ? "#1A1815" : "#FAF9F6";
|
||||
kofiBtn.style.backgroundColor = on ? "#34312C" : "#1A1815";
|
||||
kofiBtn.style.color = "#FAF9F6";
|
||||
}
|
||||
state.emit("dark-mode", on);
|
||||
};
|
||||
|
||||
state.darkMode(localStorage.getItem("darkMode") == "true");
|
||||
|
||||
state.emailPromptOpen = false;
|
||||
state.emailPromptAddress = "";
|
||||
state.emailPromptBusy = false;
|
||||
state.emailPromptError = "";
|
||||
let emailPromptFocus;
|
||||
state.respondToEmailPrompt = async (action) => {
|
||||
if (state.emailPromptBusy) return;
|
||||
state.emailPromptBusy = true;
|
||||
state.emailPromptError = "";
|
||||
try {
|
||||
const res = await http.post("/github/connections/email", { action, email: state.emailPromptAddress },
|
||||
{ headers: { "X-CSRF-Token": state.user.emailPromptCSRF } });
|
||||
if (action === "save") state.user.notificationEmail = res.data.notificationEmail;
|
||||
state.emailPromptOpen = false;
|
||||
state.user.showEmailPrompt = false;
|
||||
emailPromptFocus?.focus();
|
||||
} catch (error) { state.emailPromptError = error.data?.error || "Unable to save your preference. Please try again."; }
|
||||
finally { state.emailPromptBusy = false; }
|
||||
};
|
||||
|
||||
function getUser() {
|
||||
http.get("/api/user").then(
|
||||
(res) => {
|
||||
if (res) state.user = res.data;
|
||||
if (state.user?.username) readBilling(http).then(res => updateHeaderBilling(res.data)).catch(() => {});
|
||||
if (state.user?.emailNotificationsEnabled && state.user.showEmailPrompt && !state.user.notificationEmail) {
|
||||
emailPromptFocus = document.activeElement;
|
||||
state.emailPromptOpen = true;
|
||||
timeout(() => {
|
||||
const dialog = document.getElementById("email-prompt");
|
||||
if (dialog?.showModal) dialog.showModal();
|
||||
else dialog?.setAttribute("open", "");
|
||||
document.getElementById("email-prompt-address")?.focus();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
state.user = null;
|
||||
@@ -105,6 +219,8 @@ export const mainController = function (state, http, location, timeout) {
|
||||
http.get("/api/options").then(
|
||||
(res) => {
|
||||
if (res) state.site_options = res.data;
|
||||
setRedactionMask(res?.data?.ANONYMIZATION_MASK);
|
||||
state.emit("site-options");
|
||||
},
|
||||
() => {
|
||||
state.site_options = null;
|
||||
@@ -131,6 +247,7 @@ export const mainController = function (state, http, location, timeout) {
|
||||
}
|
||||
state.path = location.url();
|
||||
state.paths = location.path().substring(1).split("/");
|
||||
state.isReviewerRoute = reviewerRoutes.test(location.path());
|
||||
}
|
||||
|
||||
state.on("routeChange", changedUrl);
|
||||
@@ -191,6 +308,42 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
state.message = null;
|
||||
state.error = null;
|
||||
|
||||
state.accountEmail = "";
|
||||
state.emailLoading = true;
|
||||
state.emailCSRFReady = false;
|
||||
state.emailSaving = false;
|
||||
state.emailSaved = false;
|
||||
state.emailError = "";
|
||||
let emailCSRF = "";
|
||||
state.loadAccountEmail = async () => {
|
||||
state.emailLoading = true;
|
||||
state.emailError = "";
|
||||
try {
|
||||
const res = await http.get("/github/connections/email");
|
||||
state.accountEmail = res.data.notificationEmail || "";
|
||||
emailCSRF = res.data.csrf;
|
||||
state.emailCSRFReady = !!emailCSRF;
|
||||
} catch {
|
||||
state.emailError = "Unable to load your email address. Please try again.";
|
||||
} finally { state.emailLoading = false; }
|
||||
};
|
||||
state.saveAccountEmail = async () => {
|
||||
if (state.emailSaving || state.emailLoading || !emailCSRF) return;
|
||||
state.emailSaving = true;
|
||||
state.emailSaved = false;
|
||||
state.emailError = "";
|
||||
try {
|
||||
const res = await http.post("/github/connections/email", { email: state.accountEmail },
|
||||
{ headers: { "X-CSRF-Token": emailCSRF } });
|
||||
state.accountEmail = res.data.notificationEmail;
|
||||
if (state.user) state.user.notificationEmail = res.data.notificationEmail;
|
||||
state.emailSaved = true;
|
||||
} catch {
|
||||
state.emailError = "Unable to save your email address. Check the address and try again.";
|
||||
} finally { state.emailSaving = false; }
|
||||
};
|
||||
state.loadAccountEmail();
|
||||
|
||||
quotaService.load().then((quota) => {
|
||||
state.quota = quota;
|
||||
}, console.error);
|
||||
@@ -236,11 +389,13 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
(error) => {
|
||||
state.saving = false;
|
||||
const code = error && error.data && error.data.error;
|
||||
const key = "ERRORS." + code;
|
||||
translate(key).then((translation) => {
|
||||
state.error = translation && translation !== key ? translation : "Unable to save your defaults. Please try again.";
|
||||
const fallback = "Unable to save your defaults. Please try again.";
|
||||
// Unknown codes translate to the generic unknown_error message;
|
||||
// this page has a more specific one.
|
||||
Promise.all([translate("ERRORS." + code), translate("ERRORS.unknown_error")]).then(([translation, unknown]) => {
|
||||
state.error = code && translation !== unknown ? translation : fallback;
|
||||
}, () => {
|
||||
state.error = "Unable to save your defaults. Please try again.";
|
||||
state.error = fallback;
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -544,21 +699,7 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
const link = new URL(item._viewUrl, window.location.origin).href;
|
||||
state.copyingId = item._type + ":" + item._id;
|
||||
try {
|
||||
if (window.navigator.clipboard?.writeText) {
|
||||
await window.navigator.clipboard.writeText(link);
|
||||
} else {
|
||||
const field = document.createElement("textarea");
|
||||
field.value = link;
|
||||
field.setAttribute("readonly", "");
|
||||
field.style.position = "fixed";
|
||||
field.style.opacity = "0";
|
||||
const focused = document.activeElement;
|
||||
document.body.appendChild(field);
|
||||
try {
|
||||
field.select();
|
||||
if (!document.execCommand?.("copy")) throw Error("Copy unavailable");
|
||||
} finally { field.remove(); focused?.focus(); }
|
||||
}
|
||||
await copyText(link);
|
||||
state.addToast({ title: "Link copied", body: "The anonymous link is ready to share." });
|
||||
} catch {
|
||||
state.addToast({ title: "Copy this anonymous link", body: link });
|
||||
@@ -1034,6 +1175,19 @@ export const statusController = function (state, http, params) {
|
||||
state.progress = 0;
|
||||
state.rateLimitResetAt = 0;
|
||||
state.rateLimitCountdown = "";
|
||||
state.shareUrl = (window.location?.origin || "") + "/r/" + encodeURIComponent(state.repoId) + "/";
|
||||
state.copied = false;
|
||||
let copiedTimer = null;
|
||||
state.copyShareUrl = async () => {
|
||||
try {
|
||||
await copyText(state.shareUrl);
|
||||
state.copied = true;
|
||||
if (copiedTimer) timers.timeout.cancel(copiedTimer);
|
||||
copiedTimer = timers.timeout(() => { state.copied = false; }, 2000);
|
||||
} catch {
|
||||
state.addToast?.({ title: "Copy this anonymous link", body: state.shareUrl });
|
||||
}
|
||||
};
|
||||
|
||||
var countdownTimer = null;
|
||||
let pollTimer = null;
|
||||
@@ -1324,6 +1478,33 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
state.html_readme = "";
|
||||
state.isUpdate = false;
|
||||
|
||||
state.previousConfigurations = [];
|
||||
state.previousConfigurationId = "";
|
||||
let previousRepositories;
|
||||
const normalizedRepositoryName = name => String(name || "").replace(/\/$/, "").replace(/\.git$/i, "").toLowerCase();
|
||||
async function findPreviousConfigurations() {
|
||||
if (state.isUpdate || params.repoId || params.pullRequestId || params.gistId) return;
|
||||
const request = { generation: sourceGeneration, url: state.sourceUrl.trim(), connection: state.githubConnection };
|
||||
try {
|
||||
// This endpoint only returns repositories the signed-in user can manage.
|
||||
previousRepositories ||= http.get("/api/user/anonymized_repositories").then(res => res.data);
|
||||
const repositories = await previousRepositories;
|
||||
if (!isCurrentSource(request)) return;
|
||||
const name = normalizedRepositoryName(parseRepoFullName(request.url));
|
||||
state.previousConfigurations = repositories.filter(repo => repo.repoId && repo.options
|
||||
&& normalizedRepositoryName(repo.source?.fullName) === name)
|
||||
.sort((a, b) => (Date.parse(b.anonymizeDate) || 0) - (Date.parse(a.anonymizeDate) || 0));
|
||||
state.previousConfigurationId = state.previousConfigurations[0]?.repoId || "";
|
||||
} catch (_) {
|
||||
// A suggestion failure must not prevent creating a repository. Retry on the next URL selection.
|
||||
previousRepositories = undefined;
|
||||
}
|
||||
}
|
||||
state.existingConfigurationUrl = () => {
|
||||
const previous = state.previousConfigurations.find(repo => repo.repoId === state.previousConfigurationId);
|
||||
return previous ? "/anonymize/" + encodeURIComponent(previous.repoId) : null;
|
||||
};
|
||||
|
||||
function getDefault(cb) {
|
||||
http.get("/api/user/default").then((res) => {
|
||||
const data = res.data;
|
||||
@@ -1360,6 +1541,38 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
return null;
|
||||
}
|
||||
|
||||
state.accessRepairAllowed = false;
|
||||
state.repairingGitHubAccess = false;
|
||||
state.accessRepairMessage = "";
|
||||
state.repairGitHubAccess = async () => {
|
||||
if (!state.accessRepairAllowed || state.repairingGitHubAccess || !state.githubConnections?.appEnabled || !params.repoId || state.role !== "owner" ||
|
||||
state.githubConnection !== "github-app" || parseRepoFullName(state.sourceUrl) !== state._originalFullName) return;
|
||||
state.repairingGitHubAccess = true;
|
||||
state.accessRepairMessage = "";
|
||||
const request = beginSourceRequest();
|
||||
try {
|
||||
const connections = await http.get("/github/connections");
|
||||
state.githubConnections = connections.data;
|
||||
if (!connections.data.appEnabled) return;
|
||||
const repaired = await http.post("/github/connections/migrate", { type: "repository", id: params.repoId, connection: "github-app" },
|
||||
{ headers: { "X-CSRF-Token": connections.data.csrf } });
|
||||
if (!isCurrentSource(request)) return;
|
||||
state.error = null;
|
||||
if (repaired.data.fullName) {
|
||||
state._originalFullName = repaired.data.fullName;
|
||||
state.source.fullName = repaired.data.fullName;
|
||||
state.sourceUrl = "https://github.com/" + repaired.data.fullName;
|
||||
}
|
||||
state.accessRepairMessage = "GitHub access repaired. Your anonymous URL and published commit are unchanged.";
|
||||
await state.urlSelected(true);
|
||||
} catch (_) {
|
||||
if (isCurrentSource(request)) state.accessRepairMessage = "Unable to repair GitHub access. Check the App's repository permissions in Manage access, then retry. Your published snapshot has not been replaced.";
|
||||
} finally {
|
||||
state.repairingGitHubAccess = false;
|
||||
finishSourceRequest(request);
|
||||
}
|
||||
};
|
||||
|
||||
state.reconnectingSource = false;
|
||||
state.reconnectRepositoryId = undefined;
|
||||
state.reconnectSource = async () => {
|
||||
@@ -1438,6 +1651,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
state.repoId = params.repoId;
|
||||
http.get("/api/repo/" + state.repoId).then(
|
||||
async (res) => {
|
||||
state.accessRepairAllowed = !["removed", "archived", "preparing", "queue", "download", "removing", "expiring"].includes(res.data.status);
|
||||
state.githubConnection = res.data.connection || "oauth";
|
||||
state.sourceUrl = "https://github.com/" + res.data.source.fullName;
|
||||
state._originalFullName = res.data.source.fullName;
|
||||
@@ -1534,6 +1748,8 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
// Keep the same input focused until a complete GitHub URL can be loaded.
|
||||
state.urlSelected = async (preserveDraft = false) => {
|
||||
const generation = ++sourceGeneration;
|
||||
state.previousConfigurations = [];
|
||||
state.previousConfigurationId = "";
|
||||
let o;
|
||||
try { o = parseGithubUrl(state.sourceUrl.trim()); }
|
||||
catch (_) {
|
||||
@@ -1589,6 +1805,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
await getPrDetails();
|
||||
} else {
|
||||
state.detectedType = "repo";
|
||||
void findPreviousConfigurations();
|
||||
await Promise.all([getRepoDetails(), getReadme()]);
|
||||
anonymizeReadme();
|
||||
}
|
||||
@@ -2279,6 +2496,11 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
|
||||
export const exploreController = function (state, http, location, params, html, promises) {
|
||||
const timers = createTimers();
|
||||
// Prism rewrites fenced code blocks, so mark redactions once it is done.
|
||||
if (window.Prism?.hooks && !window.Prism.redactionHook) {
|
||||
window.Prism.hooks.add("complete", (env) => markRedactionsIn(env.element));
|
||||
window.Prism.redactionHook = true;
|
||||
}
|
||||
const listen = createListeners();
|
||||
state.on("dark-mode", (event, on) => {
|
||||
if (!state.aceOption) return;
|
||||
@@ -2510,7 +2732,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
}, function (err) {
|
||||
if (destroyed || repoId !== state.repoId) return [];
|
||||
state.type = "error";
|
||||
state.content = (err && err.data && err.data.error) || "unknown_error";
|
||||
state.content = viewerErrorCode(err);
|
||||
state.files = [];
|
||||
});
|
||||
};
|
||||
@@ -2580,7 +2802,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
timers.timeout(function () { getOptions(callback); }, 3000);
|
||||
} else {
|
||||
state.type = "error";
|
||||
state.content = data.error;
|
||||
state.content = viewerErrorCode(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -2705,23 +2927,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
(err) => {
|
||||
if (destroyed || generation !== contentGeneration) return;
|
||||
state.type = "error";
|
||||
state.content = "unknown_error";
|
||||
try {
|
||||
err.data = JSON.parse(err.data);
|
||||
if (err.data.error) {
|
||||
state.content = err.data.error;
|
||||
} else {
|
||||
state.content = err.data;
|
||||
}
|
||||
} catch (ignore) {
|
||||
console.log(err);
|
||||
if (err.status == -1) {
|
||||
state.content = "request_error";
|
||||
} else if (err.status == 502) {
|
||||
// cloudflare error
|
||||
state.content = "unreachable";
|
||||
}
|
||||
}
|
||||
state.content = viewerErrorCode(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2765,7 +2971,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
theme: "chrome",
|
||||
useSoftTab: true,
|
||||
tabSize: 2,
|
||||
fontSize: 15,
|
||||
fontSize: 13,
|
||||
keyBinding: "vscode",
|
||||
fullLineSelection: true,
|
||||
highlightActiveLine: false,
|
||||
@@ -2870,9 +3076,43 @@ export const exploreController = function (state, http, location, params, html,
|
||||
);
|
||||
_editor.session.setUseSoftTabs(state.aceOption.useSoftTab);
|
||||
_editor.session.setTabSize(state.aceOption.tabSize);
|
||||
// Phones can't scroll long lines comfortably, so wrap them there,
|
||||
// following the viewport when a tablet rotates or a window resizes.
|
||||
const narrow = window.matchMedia("(max-width: 991px)");
|
||||
const applyWrap = () => _editor.session.setUseWrapMode(state.aceOption.useWrapMode && narrow.matches);
|
||||
applyWrap();
|
||||
narrow.addEventListener?.("change", applyWrap);
|
||||
_editor.setBehavioursEnabled(state.aceOption.enableBehaviours);
|
||||
_editor.setFadeFoldWidgets(state.aceOption.fadeFoldWidgets);
|
||||
return removeHashListener;
|
||||
|
||||
// Outline redacted terms (XXXX-1, ...) so reviewers can tell a
|
||||
// replacement from the author's own text.
|
||||
let redactionMarkers = [];
|
||||
const markRedactions = () => {
|
||||
redactionMarkers.forEach((id) => _editor.session.removeMarker(id));
|
||||
redactionMarkers = [];
|
||||
const pattern = redactionPattern();
|
||||
if (!pattern) return;
|
||||
const lines = _editor.session.getDocument().getAllLines();
|
||||
lines.forEach((line, row) => {
|
||||
for (const match of line.matchAll(pattern)) {
|
||||
redactionMarkers.push(_editor.session.addMarker(
|
||||
new Range(row, match.index, row, match.index + match[0].length),
|
||||
"ace_redaction",
|
||||
"text"
|
||||
));
|
||||
}
|
||||
});
|
||||
};
|
||||
markRedactions();
|
||||
_editor.session.on("change", () => timers.timeout(markRedactions, 0));
|
||||
// The mask may arrive after the editor mounts.
|
||||
const offOptions = state.on("site-options", markRedactions);
|
||||
return () => {
|
||||
removeHashListener();
|
||||
offOptions();
|
||||
narrow.removeEventListener?.("change", applyWrap);
|
||||
};
|
||||
},
|
||||
};
|
||||
if (state.isDarkMode) {
|
||||
@@ -2892,6 +3132,10 @@ export const exploreController = function (state, http, location, params, html,
|
||||
getContent(state.filePath, state.file);
|
||||
}
|
||||
|
||||
state.isSourceAccessError = () => ["repo_not_found", "token_expired", "github_app_access_required",
|
||||
"github_app_reconnect_required", "repo_saml_enforcement", "repo_access_limited"].includes(state.content);
|
||||
state.retryContent = () => init();
|
||||
|
||||
function init() {
|
||||
contentGeneration++;
|
||||
state.files = [];
|
||||
@@ -3319,11 +3563,27 @@ export const conferenceController = function (state, http, location, params) {
|
||||
|
||||
|
||||
export const connectionsController = function (state, http) {
|
||||
state.notificationEmail = "";
|
||||
state.emailSaved = false;
|
||||
state.saveNotificationEmail = async () => {
|
||||
state.busy = true;
|
||||
state.emailSaved = false;
|
||||
state.connectionError = "";
|
||||
try {
|
||||
const res = await http.post("/github/connections/email", { email: state.notificationEmail },
|
||||
{ headers: { "X-CSRF-Token": state.connections.csrf } });
|
||||
state.notificationEmail = res.data.notificationEmail;
|
||||
if (state.user) state.user.notificationEmail = res.data.notificationEmail;
|
||||
state.emailSaved = true;
|
||||
} catch (error) { state.connectionError = error.data?.error || "Unable to save email address."; }
|
||||
finally { state.busy = false; }
|
||||
};
|
||||
state.connections = null;
|
||||
state.connectionError = "";
|
||||
state.busy = false;
|
||||
state.loadConnections = () => http.get("/github/connections").then(res => {
|
||||
state.connections = res.data;
|
||||
state.notificationEmail = res.data.notificationEmail || "";
|
||||
}).catch(error => { state.connectionError = error.data?.error || "Unable to load connections."; });
|
||||
state.changeConnection = async (resource, connection, preview) => {
|
||||
state.busy = true;
|
||||
|
||||
Reference in new issue
Block a user