diff options
| author | AlfaPC11 | 2026-07-28 21:38:04 +0300 |
|---|---|---|
| committer | AlfaPC11 | 2026-07-28 21:38:04 +0300 |
| commit | 3e726a87c9ae60aea3403e12c8a0aab3870942df (patch) | |
| tree | 09a55fb7328e8ca7eb29f43a5e930de557104c13 /frontend | |
| parent | 0c9ea445a277c23aeb7156578c623ac502591499 (diff) | |
| download | website-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.astro | 2 | ||||
| -rw-r--r-- | frontend/src/pages/repo/dashboard/index.astro | 36 | ||||
| -rw-r--r-- | frontend/src/styles/global.css | 21 |
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; |