From a79efafda0773e989fc3a6b826e56cdc25d78671 Mon Sep 17 00:00:00 2001 From: bigben-7 Date: Sat, 17 Oct 2026 11:02:23 +0100 Subject: [PATCH] monique-7arch: security settings API + safe upload progress (#1508-#1511) --- frontend/src/lib/security-api.ts | 65 +++++++++++++++++++++++++++++ frontend/src/lib/upload-progress.ts | 46 ++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 frontend/src/lib/security-api.ts create mode 100644 frontend/src/lib/upload-progress.ts diff --git a/frontend/src/lib/security-api.ts b/frontend/src/lib/security-api.ts new file mode 100644 index 0000000..7f07459 --- /dev/null +++ b/frontend/src/lib/security-api.ts @@ -0,0 +1,65 @@ +/** + * Security settings API (#1508, #1509, #1510). + * + * The security settings page used hard-coded mock sessions, a local-only + * "Revoke", a 2FA toggle that only flipped local state, and "Delete account" + * that signed the user out without deleting anything. These calls back each + * action with the real API. + */ +import { apiUrl } from "./api-version"; + +export interface SessionInfo { + id: string; + device?: string; + lastActive?: string; +} + +async function expectOk( + path: string, + init: RequestInit, + fetchImpl: typeof fetch, +): Promise { + const response = await fetchImpl(apiUrl(path), { + credentials: "include", + ...init, + }); + if (!response.ok) { + throw new Error(`${path} failed (${response.status})`); + } + return response; +} + +export async function listSessions( + fetchImpl: typeof fetch = fetch, +): Promise { + const response = await expectOk("/auth/sessions", {}, fetchImpl); + return (await response.json()) as SessionInfo[]; +} + +export async function revokeSession( + id: string, + fetchImpl: typeof fetch = fetch, +): Promise { + await expectOk(`/auth/sessions/${id}`, { method: "DELETE" }, fetchImpl); +} + +export async function setTwoFactor( + enabled: boolean, + fetchImpl: typeof fetch = fetch, +): Promise { + await expectOk( + "/auth/2fa", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled }), + }, + fetchImpl, + ); +} + +export async function deleteAccount( + fetchImpl: typeof fetch = fetch, +): Promise { + await expectOk("/auth/account", { method: "DELETE" }, fetchImpl); +} diff --git a/frontend/src/lib/upload-progress.ts b/frontend/src/lib/upload-progress.ts new file mode 100644 index 0000000..96d16f5 --- /dev/null +++ b/frontend/src/lib/upload-progress.ts @@ -0,0 +1,46 @@ +/** + * Upload progress tracking that cleans up after itself (#1511). + * + * The upload page faked progress and leaked its `setInterval` on errors. This + * tracker owns its timer and guarantees it is cleared on completion or failure. + */ +export interface ProgressTracker { + set(value: number): void; + done(): void; + fail(): void; +} + +export function createProgressTracker( + onProgress: (value: number) => void, + intervalMs = 250, + schedule: (fn: () => void, ms: number) => ReturnType = + setInterval, + clear: (handle: ReturnType) => void = clearInterval, +): ProgressTracker { + let value = 0; + let handle: ReturnType | null = schedule(() => { + value = Math.min(value + 5, 95); + onProgress(value); + }, intervalMs); + + const stop = (): void => { + if (handle !== null) { + clear(handle); + handle = null; + } + }; + + return { + set(next: number): void { + value = Math.min(Math.max(next, 0), 100); + onProgress(value); + }, + done(): void { + stop(); + onProgress(100); + }, + fail(): void { + stop(); + }, + }; +}