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

summaryrefslogtreecommitdiffhomepage
path: root/frontend/src
diff options
context:
space:
mode:
authorAlfaPC112026-07-28 21:38:04 +0300
committerAlfaPC112026-07-28 21:38:04 +0300
commit3e726a87c9ae60aea3403e12c8a0aab3870942df (patch)
tree09a55fb7328e8ca7eb29f43a5e930de557104c13 /frontend/src
parent0c9ea445a277c23aeb7156578c623ac502591499 (diff)
downloadwebsite-3e726a87c9ae60aea3403e12c8a0aab3870942df.tar.gz
require verification after Google auth and add account deletion
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/AuthPanel.astro2
-rw-r--r--frontend/src/pages/repo/dashboard/index.astro36
-rw-r--r--frontend/src/scripts/dashboard.ts127
-rw-r--r--frontend/src/styles/global.css21
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;