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/src | |
| parent | 0c9ea445a277c23aeb7156578c623ac502591499 (diff) | |
| download | website-3e726a87c9ae60aea3403e12c8a0aab3870942df.tar.gz | |
require verification after Google auth and add account deletion
Diffstat (limited to 'frontend/src')
| -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/scripts/dashboard.ts | 127 | ||||
| -rw-r--r-- | frontend/src/styles/global.css | 21 |
4 files changed, 55 insertions, 131 deletions
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/scripts/dashboard.ts b/frontend/src/scripts/dashboard.ts deleted file mode 100644 index 84c700a..0000000 --- a/frontend/src/scripts/dashboard.ts +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Leitwolf <xs-lang.chess031@slmails.com> -// SPDX-License-Identifier: AGPL-3.0-or-later - -export {}; - -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 { - if (!alert) return; - alert.textContent = message; - alert.dataset.kind = error ? "error" : "info"; - alert.hidden = false; -} - -function renderTokens(tokens: RegistryToken[]): void { - if (!tokenList) return; - if (tokenCount) tokenCount.textContent = String(tokens.length); - if (tokens.length === 0) { - tokenList.innerHTML = '<p class="token-list-empty">No API tokens have been created.</p>'; - return; - } - tokenList.replaceChildren( - ...tokens.map((token) => { - const row = document.createElement("article"); - row.className = "token-row"; - const details = document.createElement("div"); - const name = document.createElement("strong"); - name.textContent = token.name; - const metadata = document.createElement("span"); - metadata.textContent = `${token.prefix}… · ${token.scopes.join(", ")} · created ${new Date(token.createdAt).toLocaleDateString()}`; - details.append(name, metadata); - const revoke = document.createElement("button"); - revoke.type = "button"; - revoke.textContent = "Revoke"; - revoke.addEventListener("click", () => revokeToken(token.id)); - row.append(details, revoke); - return row; - }), - ); -} - -async function loadTokens(): Promise<void> { - 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[]); -} - -async function revokeToken(id: string): Promise<void> { - 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> { - 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 email = account.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]"); - 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"; - await loadTokens(); -} - -root?.querySelector("[data-signout]")?.addEventListener("click", async () => { - await fetch("/api/v1/auth/logout", { method: "POST", credentials: "include" }); - window.location.assign("https://repo.xsharp-lang.xyz/login/"); -}); -root?.querySelector("[data-token-open]")?.addEventListener("click", () => dialog?.showModal()); -dialog?.querySelector("[data-token-close]")?.addEventListener("click", () => dialog?.close()); -dialog?.querySelector("[data-token-copy]")?.addEventListener("click", async () => { - if (tokenValue?.textContent) await navigator.clipboard.writeText(tokenValue.textContent); -}); - -tokenForm?.addEventListener("submit", async (event) => { - event.preventDefault(); - const formData = new FormData(tokenForm); - const name = String(formData.get("name") ?? "").trim(); - const scopes = formData.getAll("scope").map(String); - const response = await fetch("/api/v1/tokens/", { - method: "POST", - credentials: "include", - headers: { "Content-Type": "application/json" }, - 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 }; - if (tokenValue) tokenValue.textContent = created.token; - if (tokenSecret) tokenSecret.hidden = false; - await loadTokens(); -}); - -void initialize(); 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; |