import { getApiUrl } from "../env"; export interface ProblemDetails { type: string; title: string; status: number; detail?: string; instance?: string; errors?: { field?: string; message: string }[]; } export class ApiError extends Error { readonly status: number; readonly title: string; readonly detail?: string; readonly errors?: { field?: string; message: string }[]; constructor(problem: ProblemDetails) { super(problem.detail ?? problem.title); this.name = "ApiError"; this.status = problem.status; this.title = problem.title; this.detail = problem.detail; this.errors = problem.errors; } } export interface AuthCredentials { email: string; password: string; } export interface RegisterPayload { email: string; password: string; username?: string; displayName?: string; } export interface LoginResponse { twoFactorRequired?: boolean; challengeId?: string; } export interface CurrentUser { id: string; email: string; username: string; displayName?: string | null; emailVerifiedAt?: string | null; emailVerified?: boolean; roles?: string[]; twoFactorEnabled?: boolean; impersonation?: { isImpersonation: boolean; label?: string; impersonatorLabel?: string; }; } export interface MeResponse { user: CurrentUser; impersonation?: { isImpersonation: boolean; impersonatorLabel?: string; label?: string; }; } export interface TwoFactorSetupResponse { secret: string; qrCodeUrl: string; recoveryCodes?: string[]; } export interface UserSession { id: string; ipAddress?: string | null; userAgent?: string | null; createdAt: string; expiresAt: string; current?: boolean; } async function parseProblemResponse(response: Response): Promise { const contentType = response.headers.get("content-type") ?? ""; if (contentType.includes("application/problem+json")) { const problem = (await response.json()) as ProblemDetails; return new ApiError({ ...problem, status: problem.status ?? response.status, title: problem.title ?? "Request failed", }); } return new ApiError({ type: "about:blank", title: "Request failed", status: response.status, detail: response.statusText || undefined, }); } export async function apiFetch( path: string, options: RequestInit = {}, ): Promise { const endpoint = `${getApiUrl()}${path}`; if (!endpoint.startsWith("http")) { throw new Error("Invalid API URL configuration"); } const headers = new Headers(options.headers); if (options.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(endpoint, { ...options, credentials: "include", headers, }); if (!response.ok) { throw await parseProblemResponse(response); } if (response.status === 204) { return undefined as T; } const text = await response.text(); if (!text) { return undefined as T; } return JSON.parse(text) as T; } export async function submitLogin( credentials: AuthCredentials, ): Promise { return apiFetch("/auth/login", { method: "POST", body: JSON.stringify(credentials), }); } export async function submitRegister( payload: RegisterPayload, ): Promise { await apiFetch("/auth/register", { method: "POST", body: JSON.stringify(payload), }); } export async function submitLogout(): Promise { await apiFetch("/auth/logout", { method: "POST", }); } export async function getCurrentUser(): Promise { try { const response = await apiFetch("/me"); return { ...response.user, impersonation: response.impersonation ? { isImpersonation: response.impersonation.isImpersonation, label: response.impersonation.label ?? response.impersonation.impersonatorLabel, } : undefined, }; } catch { return null; } } export async function consumeSsoTicket(ticket: string): Promise<{ redirectPath: string }> { return apiFetch<{ redirectPath: string }>("/auth/sso/consume", { method: "POST", body: JSON.stringify({ ticket }), }); } export async function verifyEmail(token: string): Promise { await apiFetch("/auth/verify-email", { method: "POST", body: JSON.stringify({ token }), }); } export async function forgotPassword(email: string): Promise { await apiFetch("/auth/forgot-password", { method: "POST", body: JSON.stringify({ email }), }); } export async function resetPassword( token: string, newPassword: string, ): Promise { await apiFetch("/auth/reset-password", { method: "POST", body: JSON.stringify({ token, newPassword }), }); } export async function setupTwoFactor(): Promise { return apiFetch("/auth/2fa/setup", { method: "POST", }); } export async function confirmTwoFactor(code: string): Promise { await apiFetch("/auth/2fa/confirm", { method: "POST", body: JSON.stringify({ code }), }); } export async function verifyTwoFactorLogin( challengeId: string, code: string, ): Promise { await apiFetch("/auth/2fa/verify", { method: "POST", body: JSON.stringify({ challengeId, code }), }); } export async function getSessions(): Promise { return apiFetch("/me/sessions"); } export async function revokeSession(sessionId: string): Promise { await apiFetch(`/me/sessions/${sessionId}`, { method: "DELETE", }); }