import type { AccountDevice, AuthCodeResponse, AuthOptions, AuthSession, AuthUser, StoredAuthSession } from "@/domain/auth"; import { createApiFailure, createNetworkFailure } from "@/infrastructure/userMessages"; const sessionStorageKey = "img-infinite-canvas.auth.session"; const accessTokenCookie = "usertoken"; const refreshTokenCookie = "refreshToken"; async function request(path: string, body: unknown): Promise { let response: Response; try { response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); } catch (err) { throw createNetworkFailure("auth", err); } if (!response.ok) { const payload = await response.json().catch(() => ({ error: response.statusText })); throw createApiFailure("auth", response, payload); } return response.json() as Promise; } async function get(path: string): Promise { let response: Response; try { response = await fetch(path, { cache: "no-store" }); } catch (err) { throw createNetworkFailure("auth", err); } if (!response.ok) { const payload = await response.json().catch(() => ({ error: response.statusText })); throw createApiFailure("auth", response, payload); } return response.json() as Promise; } async function apiRequest(path: string, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); if (!(init.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } Object.entries(authHeaders()).forEach(([key, value]) => headers.set(key, value)); let response: Response; try { response = await fetch(path, { ...init, headers, cache: "no-store" }); } catch (err) { throw createNetworkFailure("auth", err); } if (!response.ok) { const payload = await response.json().catch(() => ({ error: response.statusText })); throw createApiFailure("auth", response, payload); } return response.json() as Promise; } export const authGateway = { options() { return get("/api/auth/options"); }, requestGlobalEmailCode(email: string, turnstileToken: string) { return request("/api/auth/global/email-code", { email, turnstileToken }); }, loginGlobalEmailCode(email: string, code: string) { return request("/api/auth/global/email-login", { email, code }); }, loginGlobalGoogle(idToken: string) { return request("/api/auth/global/google-login", { idToken }); }, profile() { return apiRequest<{ user: AuthUser }>("/api/account/profile"); }, updateProfile(userPatch: Partial>) { return apiRequest<{ user: AuthUser }>("/api/account/profile", { method: "PATCH", body: JSON.stringify(userPatch) }); }, devices() { return apiRequest<{ devices: AccountDevice[] }>("/api/account/devices"); }, removeDevices() { return apiRequest<{ removed: number }>("/api/account/devices", { method: "DELETE" }); }, logoutAccount() { return apiRequest<{ removed: number }>("/api/account/logout", { method: "POST", body: JSON.stringify({}) }); }, async uploadAvatar(file: File) { const upload = await apiRequest<{ uploadUrl: string; publicUrl: string; headers: Record; }>("/api/assets/upload-url", { method: "POST", body: JSON.stringify({ fileName: file.name, contentType: file.type || "application/octet-stream", size: file.size }) }); if (upload.uploadUrl && !upload.uploadUrl.includes("/memory-assets/")) { await fetch(upload.uploadUrl, { method: "PUT", headers: upload.headers, body: file }); } return upload.publicUrl; } }; export function readStoredAuthSession(): StoredAuthSession | null { if (typeof window === "undefined") return null; const raw = window.localStorage.getItem(sessionStorageKey); if (!raw) return null; try { const session = JSON.parse(raw) as StoredAuthSession; if (!session.accessToken || isSessionExpired(session)) { clearStoredAuthSession(); return null; } return session; } catch { clearStoredAuthSession(); return null; } } export function storeAuthSession(session: AuthSession): StoredAuthSession { const stored = { ...session, savedAt: Date.now() }; if (typeof window !== "undefined") { window.localStorage.setItem(sessionStorageKey, JSON.stringify(stored)); setCookie(accessTokenCookie, session.accessToken, session.expiresIn); if (session.refreshToken && session.refreshExpiresIn) { setCookie(refreshTokenCookie, session.refreshToken, session.refreshExpiresIn); } } return stored; } export function updateStoredAuthUser(userPatch: Partial): StoredAuthSession | null { const current = readStoredAuthSession(); if (!current) return null; const stored = { ...current, user: { ...current.user, ...userPatch } }; if (typeof window !== "undefined") { window.localStorage.setItem(sessionStorageKey, JSON.stringify(stored)); } return stored; } export function clearStoredAuthSession() { if (typeof window === "undefined") return; window.localStorage.removeItem(sessionStorageKey); expireCookie(accessTokenCookie); expireCookie(refreshTokenCookie); } export function getAuthAccessToken() { return readStoredAuthSession()?.accessToken ?? ""; } export function authHeaders() { const token = getAuthAccessToken(); if (!token) return {}; return { Authorization: `Bearer ${token}`, token } satisfies Record; } function isSessionExpired(session: StoredAuthSession) { const expiresIn = Number(session.expiresIn || 0); return expiresIn > 0 && session.savedAt + expiresIn * 1000 <= Date.now() + 30_000; } function setCookie(name: string, value: string, maxAge: number) { document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${Math.max(0, Math.floor(maxAge))}; SameSite=Lax`; } function expireCookie(name: string) { document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`; }