Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
authorAlfaPC112026-07-28 21:38:04 +0300
committerAlfaPC112026-07-28 21:38:04 +0300
commit3e726a87c9ae60aea3403e12c8a0aab3870942df (patch)
tree09a55fb7328e8ca7eb29f43a5e930de557104c13 /frontend
parent0c9ea445a277c23aeb7156578c623ac502591499 (diff)
downloadwebsite-3e726a87c9ae60aea3403e12c8a0aab3870942df.tar.gz
require verification after Google auth and add account deletion
Diffstat (limited to 'frontend')
-rw-r--r--frontend/public/scripts/auth-flow.v2.js (renamed from frontend/public/scripts/auth-flow.v1.js)15
-rw-r--r--frontend/public/scripts/dashboard.v1.js (renamed from frontend/src/scripts/dashboard.ts)89
-rw-r--r--frontend/src/components/AuthPanel.astro2
-rw-r--r--frontend/src/pages/repo/dashboard/index.astro36
-rw-r--r--frontend/src/styles/global.css21
5 files changed, 124 insertions, 39 deletions
diff --git a/frontend/public/scripts/auth-flow.v1.js b/frontend/public/scripts/auth-flow.v2.js
index 695bcbd..4bab52f 100644
--- a/frontend/public/scripts/auth-flow.v1.js
+++ b/frontend/public/scripts/auth-flow.v2.js
@@ -77,6 +77,20 @@ async function initializeProviders() {
}
}
+function initializeGoogleVerification() {
+ const parameters = new URLSearchParams(window.location.search);
+ const requestedEmail = parameters.get("email")?.trim().toLowerCase() ?? "";
+ if (mode !== "register" || parameters.get("verify") !== "google" || !requestedEmail || !emailInput) {
+ return;
+ }
+ emailInput.value = requestedEmail;
+ if (!emailInput.checkValidity()) return;
+ email = requestedEmail;
+ showStep("code");
+ showAlert("Google sign-in succeeded. Check your email for the X# verification code.");
+ window.history.replaceState({}, "", window.location.pathname);
+}
+
form?.addEventListener("submit", async (event) => {
event.preventDefault();
if (!mode) return;
@@ -144,4 +158,5 @@ form?.addEventListener("submit", async (event) => {
window.location.assign("https://repo.xsharp-lang.xyz/dashboard/");
});
+initializeGoogleVerification();
void initializeProviders();
diff --git a/frontend/src/scripts/dashboard.ts b/frontend/public/scripts/dashboard.v1.js
index 84c700a..82286d5 100644
--- a/frontend/src/scripts/dashboard.ts
+++ b/frontend/public/scripts/dashboard.v1.js
@@ -1,34 +1,27 @@
// SPDX-FileCopyrightText: 2026 Leitwolf <xs-lang.chess031@slmails.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
-export {};
+const root = document.querySelector("[data-dashboard]");
+const alert = root?.querySelector("[data-dashboard-alert]");
+const tokenList = root?.querySelector("[data-token-list]");
+const tokenCount = root?.querySelector("[data-token-count]");
+const dialog = document.querySelector("[data-token-dialog]");
+const tokenForm = dialog?.querySelector("[data-token-form]");
+const tokenSecret = dialog?.querySelector("[data-token-secret]");
+const tokenValue = dialog?.querySelector("[data-token-value]");
+const deleteDialog = document.querySelector("[data-delete-dialog]");
+const deleteForm = deleteDialog?.querySelector("[data-delete-form]");
+const deleteConfirmation = deleteDialog?.querySelector("[data-delete-confirmation]");
+let accountEmail = "";
-interface RegistryToken {
- id: string;
- name: string;
- prefix: string;
- scopes: string[];
- createdAt: string;
- lastUsedAt: string | null;
-}
-
-const root = document.querySelector<HTMLElement>("[data-dashboard]");
-const alert = root?.querySelector<HTMLElement>("[data-dashboard-alert]");
-const tokenList = root?.querySelector<HTMLElement>("[data-token-list]");
-const tokenCount = root?.querySelector<HTMLElement>("[data-token-count]");
-const dialog = document.querySelector<HTMLDialogElement>("[data-token-dialog]");
-const tokenForm = dialog?.querySelector<HTMLFormElement>("[data-token-form]");
-const tokenSecret = dialog?.querySelector<HTMLElement>("[data-token-secret]");
-const tokenValue = dialog?.querySelector<HTMLElement>("[data-token-value]");
-
-function showAlert(message: string, error = false): void {
+function showAlert(message, error = false) {
if (!alert) return;
alert.textContent = message;
alert.dataset.kind = error ? "error" : "info";
alert.hidden = false;
}
-function renderTokens(tokens: RegistryToken[]): void {
+function renderTokens(tokens) {
if (!tokenList) return;
if (tokenCount) tokenCount.textContent = String(tokens.length);
if (tokens.length === 0) {
@@ -55,44 +48,43 @@ function renderTokens(tokens: RegistryToken[]): void {
);
}
-async function loadTokens(): Promise<void> {
+async function loadTokens() {
const response = await fetch("/api/v1/tokens/", { credentials: "include" });
if (!response.ok) {
showAlert("API tokens could not be loaded.", true);
return;
}
- renderTokens((await response.json()) as RegistryToken[]);
+ renderTokens(await response.json());
}
-async function revokeToken(id: string): Promise<void> {
+async function revokeToken(id) {
if (!window.confirm("Revoke this token? Existing CLI sessions using it will stop working.")) return;
const response = await fetch(`/api/v1/tokens/${id}`, { method: "DELETE", credentials: "include" });
if (!response.ok) return showAlert("The token could not be revoked.", true);
await loadTokens();
}
-async function initialize(): Promise<void> {
+async function initialize() {
const response = await fetch("/api/v1/auth/me", { credentials: "include" });
if (response.status === 401) {
window.location.replace("https://repo.xsharp-lang.xyz/login/");
return;
}
if (!response.ok) return showAlert("Account information could not be loaded.", true);
- const account = (await response.json()) as {
- email: string;
- displayName: string;
- emailVerified: boolean;
- };
+ const account = await response.json();
const email = account.email ?? "";
+ accountEmail = email;
const displayName = account.displayName || email.split("@")[0] || "Account";
- const avatar = root?.querySelector<HTMLElement>("[data-account-avatar]");
- const name = root?.querySelector<HTMLElement>("[data-account-name]");
- const emailElement = root?.querySelector<HTMLElement>("[data-account-email]");
- const status = root?.querySelector<HTMLElement>("[data-email-status]");
+ const avatar = root?.querySelector("[data-account-avatar]");
+ const name = root?.querySelector("[data-account-name]");
+ const emailElement = root?.querySelector("[data-account-email]");
+ const status = root?.querySelector("[data-email-status]");
+ const deleteEmail = deleteDialog?.querySelector("[data-delete-email]");
if (avatar) avatar.textContent = displayName.slice(0, 1).toUpperCase();
if (name) name.textContent = displayName;
if (emailElement) emailElement.textContent = email;
if (status) status.textContent = account.emailVerified ? "Verified" : "Pending";
+ if (deleteEmail) deleteEmail.textContent = email;
await loadTokens();
}
@@ -106,6 +98,33 @@ dialog?.querySelector("[data-token-copy]")?.addEventListener("click", async () =
if (tokenValue?.textContent) await navigator.clipboard.writeText(tokenValue.textContent);
});
+root?.querySelector("[data-delete-open]")?.addEventListener("click", () => {
+ if (deleteConfirmation) deleteConfirmation.value = "";
+ deleteDialog?.showModal();
+ deleteConfirmation?.focus();
+});
+deleteDialog?.querySelector("[data-delete-close]")?.addEventListener("click", () => deleteDialog?.close());
+deleteForm?.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const confirmation = deleteConfirmation?.value.trim() ?? "";
+ if (!confirmation || confirmation.toLowerCase() !== accountEmail.toLowerCase()) {
+ showAlert("Enter the account email address exactly to confirm deletion.", true);
+ return;
+ }
+ if (!window.confirm("Permanently delete this registry account? This cannot be undone.")) return;
+ const response = await fetch("/api/v1/auth/account", {
+ method: "DELETE",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ confirmation }),
+ });
+ if (!response.ok) {
+ showAlert("The account could not be deleted.", true);
+ return;
+ }
+ window.location.replace("https://repo.xsharp-lang.xyz/?account=deleted");
+});
+
tokenForm?.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(tokenForm);
@@ -118,7 +137,7 @@ tokenForm?.addEventListener("submit", async (event) => {
body: JSON.stringify({ name, scopes }),
});
if (!response.ok) return showAlert("The token could not be created.", true);
- const created = (await response.json()) as { token: string };
+ const created = await response.json();
if (tokenValue) tokenValue.textContent = created.token;
if (tokenSecret) tokenSecret.hidden = false;
await loadTokens();
diff --git a/frontend/src/components/AuthPanel.astro b/frontend/src/components/AuthPanel.astro
index c902e46..29ac1f8 100644
--- a/frontend/src/components/AuthPanel.astro
+++ b/frontend/src/components/AuthPanel.astro
@@ -146,4 +146,4 @@ const content = copy[mode];
</section>
</main>
-<script is:inline type="module" src="/scripts/auth-flow.v1.js"></script>
+<script is:inline type="module" src="/scripts/auth-flow.v2.js"></script>
diff --git a/frontend/src/pages/repo/dashboard/index.astro b/frontend/src/pages/repo/dashboard/index.astro
index aa88ced..f19fedd 100644
--- a/frontend/src/pages/repo/dashboard/index.astro
+++ b/frontend/src/pages/repo/dashboard/index.astro
@@ -94,6 +94,13 @@ import SiteLayout from "../../../layouts/SiteLayout.astro";
<h2>Security</h2>
</div>
</div>
+ <div class="security-row security-row-danger glass-panel">
+ <div>
+ <strong>Delete account</strong>
+ <span>Permanently delete this account, its API tokens, and its registry credentials.</span>
+ </div>
+ <button class="button danger" type="button" data-delete-open>Delete account</button>
+ </div>
<div class="security-row glass-panel">
<div>
<strong>Password</strong>
@@ -133,7 +140,30 @@ import SiteLayout from "../../../layouts/SiteLayout.astro";
</form>
</dialog>
- <script>
- import "../../../scripts/dashboard";
- </script>
+ <dialog class="token-dialog glass-panel" data-delete-dialog>
+ <form method="dialog" data-delete-form>
+ <div class="dialog-heading">
+ <div>
+ <p class="eyebrow">Permanent action</p>
+ <h2>Delete account</h2>
+ </div>
+ <button type="button" class="dialog-close" data-delete-close aria-label="Close">×</button>
+ </div>
+ <p class="dashboard-copy">
+ This cannot be undone. Enter <strong data-delete-email></strong> to confirm.
+ </p>
+ <label for="delete-confirmation">Account email</label>
+ <input
+ id="delete-confirmation"
+ name="confirmation"
+ type="email"
+ autocomplete="off"
+ data-delete-confirmation
+ required
+ />
+ <button class="auth-submit danger" type="submit">Permanently delete account</button>
+ </form>
+ </dialog>
+
+ <script is:inline type="module" src="/scripts/dashboard.v1.js"></script>
</SiteLayout>
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 0b69278..9ff1c0c 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -2051,6 +2051,27 @@ footer a {
display: block;
}
+.security-row + .security-row {
+ margin-top: 0.75rem;
+}
+
+.security-row-danger {
+ border-color: rgb(255 89 137 / 28%);
+}
+
+.button.danger,
+.auth-submit.danger {
+ border-color: rgb(255 89 137 / 38%);
+ color: #ffd6e4;
+ background: linear-gradient(135deg, rgb(180 28 80 / 72%), rgb(111 18 55 / 80%));
+}
+
+.button.danger:hover,
+.auth-submit.danger:hover {
+ border-color: rgb(255 128 169 / 65%);
+ box-shadow: 0 0.75rem 2rem rgb(105 13 52 / 28%);
+}
+
.token-dialog {
width: min(30rem, calc(100% - 2rem));
padding: 1.5rem;