// The signed-in account & session: the bootstrap `me` probe, multi-account switch/list, sign-out, // app version, and preference save. Only touches core's req. import { req } from "./core"; import type { DebugSession, DiagnosticsState } from "./diagnostics"; export interface Me { id: number; email: string; display_name: string | null; avatar_url: string | null; role: string; /** The latest sign-in on any device (ISO), or null when none is recorded. */ last_login_at: string | null; has_google: boolean; /** The address Google answers with, ONLY when it differs from `email` — the backend does that * comparison, so a surface can render this whenever it is present. Someone who registered as * `me+siftlode@gmail.com` keeps that address; this is the other one that also signs them in. */ google_email: string | null; has_password: boolean; google_enabled: boolean; plex_enabled: boolean; search_source: "scrape" | "api"; /** Whether live YouTube search / related videos are on: an admin turns either off with a daily * limit of 0, and what would only be refused is then left out. */ search_enabled: boolean; related_enabled: boolean; can_read: boolean; can_write: boolean; pending_invites: number; /** Admin-only: sign-ups an admin still has to let in — approved BEFORE they registered, so nothing * ever reached the Access-requests queue and its badge read zero (roadmap UX-25). Counted apart * from pending_invites because they are different facts, and widening one number silently would * change what an existing field means. */ pending_activations: number; /** Admin-only: false when SMTP is incomplete, so every outbound message is silently skipped. */ email_configured: boolean; preferences: Record; /** The consented diagnostics level in force and when it lapses. */ diagnostics: DiagnosticsState; /** The debug session recording right now, if any — a reload resumes it. */ debug_session: DebugSession | null; /** Where a bug report goes — the tracker's repository — or null: download only. */ issues: { repo: string } | null; } /** The `nav.role.*` key an account is shown under. */ export function roleKey(a: Pick): "admin" | "user" { return a.role === "admin" ? "admin" : "user"; } /** What an account is called on screen: its display name, else the local part of its email. */ export function displayName(a: { display_name: string | null; email: string }): string { return a.display_name ?? a.email.split("@")[0]!; } export interface VersionInfo { app_version: string; git_sha: string; build_date: string | null; db_revision: string | null; } export const accountApi = { // Bootstrap probe: 200 always. Returns the user when signed in, or null when logged out // (the endpoint answers `{authenticated:false}` rather than 401, so the public landing never // logs a failed request to the console). React Query treats the null as data, not an error. me: async (): Promise => { // Always 200: `{ authenticated: false }` when logged out, else the full Me + `authenticated`. const r = await req<{ authenticated: boolean } & Partial>("/api/me"); return r.authenticated ? (r as Me) : null; }, deleteAccount: (): Promise<{ deleted: boolean }> => req("/api/me/account", { method: "DELETE" }), // Ends this browser's session. logout: (): Promise<{ ok: boolean }> => req("/auth/logout", { method: "POST" }), version: (): Promise => req("/api/version"), // Overwriting preferences is idempotent, so let it retry on a transient gateway blip. savePrefs: (p: Record) => req("/api/me/preferences", { method: "PUT", body: JSON.stringify(p) }, { idempotent: true }), };