import { request } from './client'; // --- wire types ------------------------------------------------------------- // The auth surface (`/api/v1/auth/*`) is deliberately excluded from the OpenAPI document (server-side // [IgnoreApi]), so — unlike every other domain module (spa-conventions §4) — these DTOs are NOT in the // generated types and are hand-written here. Keep the field names camelCase to match the server records // exactly (the JSON the auth endpoints emit). This deviation is intentional; do not try to source these // from `./generated/v1`. /** `GET /api/v1/auth/config` — PUBLIC; drives the boot gate. */ export interface AuthConfig { oidcEnabled: boolean; localLoginEnabled: boolean; setupRequired: boolean; } /** `GET /api/v1/auth/session` — anonymous callers get 200 with `authenticated: false` (never 401). */ export interface AuthSession { authenticated: boolean; // Omitted (undefined) for anonymous callers — the server serializes `{ "authenticated": false }` and // Newtonsoft's global NullValueHandling.Ignore drops these when null, so they are optional on the wire. username?: string | null; method?: string | null; } /** * `GET /api/v1/auth/machine-key` — the server-generated machine API key. * The wire field is `apiKey` (server record `MachineKeyResponse(string ApiKey)`), not `key`. */ export interface MachineKey { apiKey: string; } const legacyApiKeyStorageKey = 'ctv-api-key'; function getStorage(): Storage | undefined { if (typeof window === 'undefined') { return undefined; } try { return window.localStorage; } catch { return undefined; } } // The SPA no longer stores or sends an API key — it authenticates with the session cookie (#295). This // one-shot cleanup removes any legacy `ctv-api-key` left over from the pre-session model; AuthGate calls // it once on reaching `ready`. (The former `get/set/clearStoredApiKey` shims were removed once the // ApiKeyScreen stopped consuming them — this is the sole remaining reader of the legacy key.) export function clearLegacyStoredApiKey(): void { getStorage()?.removeItem(legacyApiKeyStorageKey); } // --- 401 signal ------------------------------------------------------------- // The whole `/api` surface is gated behind an authenticated session cookie (#295). Rather than teach // every screen's error path to special-case 401, the request client emits a single app-wide signal when // any request comes back Unauthorized; a shell-level banner subscribes and prompts the user to sign in. // Kept here (in the auth domain module) so it never depends on a successful `/api` call. type UnauthorizedListener = () => void; const unauthorizedListeners = new Set(); export function subscribeUnauthorized(listener: UnauthorizedListener): () => void { unauthorizedListeners.add(listener); return () => { unauthorizedListeners.delete(listener); }; } export function notifyUnauthorized(): void { for (const listener of unauthorizedListeners) { listener(); } } // --- auth endpoints --------------------------------------------------------- export function getAuthConfig(): Promise { return request('/api/v1/auth/config'); } export function getAuthSession(): Promise { return request('/api/v1/auth/session'); } export function login(username: string, password: string): Promise { // A wrong-password 401 is an expected inline answer here — it must NOT trip the global 401 banner. return request('/api/v1/auth/login', { body: { username, password }, method: 'POST', suppressUnauthorizedSignal: true }); } export function setup(username: string, password: string): Promise { return request('/api/v1/auth/setup', { body: { username, password }, method: 'POST' }); } export function logout(): Promise { return request('/api/v1/auth/logout', { method: 'POST' }); } export function changePassword(currentPassword: string, newPassword: string): Promise { // A wrong current-password 401 is shown inline, not via the global banner. return request('/api/v1/auth/password', { body: { currentPassword, newPassword }, method: 'POST', suppressUnauthorizedSignal: true }); } export function getMachineKey(): Promise { return request('/api/v1/auth/machine-key'); }