import { getCurrentLocale, translateInstant } from "../i18n"; import { authSessionSchema, currentUserProfileSchema, type AuthSession, type CurrentUserProfile, type UpdateCurrentUserProfile, } from "@penclipai/shared"; type AuthErrorBody = | { code?: string; message?: string; error?: string | { code?: string; message?: string }; } | null; export class AuthApiError extends Error { status: number; code: string | null; body: unknown; constructor(message: string, status: number, body: unknown, code: string | null = null) { super(message); this.name = "AuthApiError"; this.status = status; this.code = code; this.body = body; } } function toSession(value: unknown): AuthSession | null { const direct = authSessionSchema.safeParse(value); if (direct.success) return direct.data; if (!value || typeof value !== "object") return null; const nested = authSessionSchema.safeParse((value as Record).data); return nested.success ? nested.data : null; } function extractAuthError(payload: AuthErrorBody, status: number) { const nested = payload?.error && typeof payload.error === "object" ? payload.error : null; const code = typeof nested?.code === "string" ? nested.code : typeof payload?.code === "string" ? payload.code : null; const message = typeof nested?.message === "string" && nested.message.trim().length > 0 ? nested.message : typeof payload?.message === "string" && payload.message.trim().length > 0 ? payload.message : typeof payload?.error === "string" && payload.error.trim().length > 0 ? payload.error : `Request failed: ${status}`; return new AuthApiError(message, status, payload, code); } async function authPost(path: string, body: Record) { const res = await fetch(`/api/auth${path}`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "Accept-Language": getCurrentLocale(), }, body: JSON.stringify(body), }); const payload = await res.json().catch(() => null); if (!res.ok) { throw extractAuthError(payload as AuthErrorBody, res.status); } return payload; } async function authPatch(path: string, body: Record, parse: (value: unknown) => T): Promise { const res = await fetch(`/api/auth${path}`, { method: "PATCH", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body), }); const payload = await res.json().catch(() => null); if (!res.ok) { throw extractAuthError(payload as AuthErrorBody, res.status); } return parse(payload); } export const authApi = { getSession: async (): Promise => { const res = await fetch("/api/auth/get-session", { credentials: "include", headers: { Accept: "application/json", "Accept-Language": getCurrentLocale(), }, }); if (res.status === 401) return null; const payload = await res.json().catch(() => null); if (!res.ok) { throw new Error(translateInstant("auth.failedToLoadSession", { status: res.status })); } const direct = toSession(payload); if (direct) return direct; const nested = payload && typeof payload === "object" ? toSession((payload as Record).data) : null; return nested; }, signInEmail: async (input: { email: string; password: string }) => { await authPost("/sign-in/email", input); }, signUpEmail: async (input: { name: string; email: string; password: string }) => { await authPost("/sign-up/email", input); }, getProfile: async (): Promise => { const res = await fetch("/api/auth/profile", { credentials: "include", headers: { Accept: "application/json" }, }); const payload = await res.json().catch(() => null); if (!res.ok) { throw new Error((payload as { error?: string } | null)?.error ?? `Failed to load profile (${res.status})`); } return currentUserProfileSchema.parse(payload); }, updateProfile: async (input: UpdateCurrentUserProfile): Promise => authPatch("/profile", input, (payload) => currentUserProfileSchema.parse(payload)), signOut: async () => { await authPost("/sign-out", {}); }, };