// Client-side session storage for Meshkee customer auth. Tokens live in // localStorage (per-browser, never sent anywhere but api.meshkee.com). const ACCESS_KEY = "sanihome_access_token"; const REFRESH_KEY = "sanihome_refresh_token"; export type AuthUser = { id: string; firstName?: string; lastName?: string; cellNumber?: string; email?: string; }; export function getStoredTokens(): { accessToken: string; refreshToken: string | null } | null { if (typeof window === "undefined") return null; try { const accessToken = window.localStorage.getItem(ACCESS_KEY); if (!accessToken) return null; return { accessToken, refreshToken: window.localStorage.getItem(REFRESH_KEY) }; } catch { return null; } } export function storeTokens(accessToken: string, refreshToken?: string | null): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(ACCESS_KEY, accessToken); if (refreshToken) window.localStorage.setItem(REFRESH_KEY, refreshToken); } catch { // Private browsing / storage disabled — session just won't persist. } } // Access tokens are short-lived (~15 min). Call this whenever a request // comes back 401 so the shopper doesn't get silently logged out mid-session. // Returns the fresh access token, or null if the refresh token is also gone/invalid // (caller should then clear the session and prompt login again). // // AuthContext and CartContext each independently notice an expired token and // call this — without coordination they'd fire two concurrent /auth/refresh // requests. If the backend rotates the refresh token on use, the second // request then fails (observed as a 500/400). This in-flight guard makes // every concurrent caller share the same single request instead of racing. let inFlightRefresh: Promise | null = null; export async function refreshAccessToken(): Promise { if (inFlightRefresh) return inFlightRefresh; inFlightRefresh = (async () => { const stored = getStoredTokens(); if (!stored?.refreshToken) return null; try { const res = await fetch(`${AUTH_API_BASE}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: stored.refreshToken }), }); if (!res.ok) { clearTokens(); return null; } const data = (await res.json()) as RawAuthResponse; const accessToken = data.accessToken ?? data.token ?? data.access_token; if (!accessToken) return null; storeTokens(accessToken, data.refreshToken ?? data.refresh_token ?? stored.refreshToken); return accessToken; } catch { return null; } })(); try { return await inFlightRefresh; } finally { inFlightRefresh = null; } } export function clearTokens(): void { if (typeof window === "undefined") return; try { window.localStorage.removeItem(ACCESS_KEY); window.localStorage.removeItem(REFRESH_KEY); } catch { // ignore } } const AUTH_API_BASE = "https://api.meshkee.com/api/v1"; export const WEBSITE_DOMAIN = "sanihome.ir"; async function authFetch( path: string, options: { method?: string; body?: unknown; token?: string } = {}, ): Promise<{ ok: true; data: T } | { ok: false; status: number; message: string }> { try { const res = await fetch(`${AUTH_API_BASE}${path}`, { method: options.method ?? "GET", headers: { "Content-Type": "application/json", ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}), }, body: options.body != null ? JSON.stringify(options.body) : undefined, }); const data = await res.json().catch(() => ({})); if (!res.ok) { return { ok: false, status: res.status, message: (data as { message?: string })?.message ?? "خطایی رخ داد. دوباره تلاش کنید.", }; } return { ok: true, data: data as T }; } catch { return { ok: false, status: 0, message: "ارتباط با سرور برقرار نشد." }; } } // The API doesn't document the exact success-response field names for // login/register/login-otp — accessToken/refreshToken/user is the standard // shape (confirmed accessToken+refreshToken naming via /auth/refresh's own // request body, and a `user` wrapper via /auth/me's documented description). // Read defensively in case a field is nested or named slightly differently. type RawAuthResponse = { accessToken?: string; token?: string; access_token?: string; refreshToken?: string; refresh_token?: string; user?: Record; customer?: Record; }; function extractSession(raw: RawAuthResponse): { accessToken: string; refreshToken: string | null; user: AuthUser } | null { const accessToken = raw.accessToken ?? raw.token ?? raw.access_token; if (!accessToken) return null; const refreshToken = raw.refreshToken ?? raw.refresh_token ?? null; const rawUser = raw.user ?? raw.customer ?? {}; return { accessToken, refreshToken, user: { id: String(rawUser.id ?? ""), firstName: rawUser.firstName as string | undefined, lastName: rawUser.lastName as string | undefined, cellNumber: rawUser.cellNumber as string | undefined, email: rawUser.email as string | undefined, }, }; } export async function loginWithPassword(cellNumber: string, password: string) { const res = await authFetch("/auth/login", { method: "POST", body: { cellNumber, password }, }); if (!res.ok) return res; const session = extractSession(res.data); if (!session) return { ok: false as const, status: 200, message: "پاسخ سرور نامعتبر بود." }; return { ok: true as const, data: session }; } export async function loginWithOtp(cellNumber: string, code: string) { const res = await authFetch("/auth/login-otp", { method: "POST", body: { cellNumber, code }, }); if (!res.ok) return res; const session = extractSession(res.data); if (!session) return { ok: false as const, status: 200, message: "پاسخ سرور نامعتبر بود." }; return { ok: true as const, data: session }; } export async function sendOtp(cellNumber: string) { return authFetch<{ message?: string }>("/auth/send-otp", { method: "POST", body: { cellNumber, domain: WEBSITE_DOMAIN }, }); } export async function register(input: { cellNumber: string; password: string; firstName: string; lastName: string; email?: string; }) { const res = await authFetch("/auth/register", { method: "POST", body: { ...input, domain: WEBSITE_DOMAIN }, }); if (!res.ok) return res; const session = extractSession(res.data); if (!session) return { ok: false as const, status: 200, message: "پاسخ سرور نامعتبر بود." }; return { ok: true as const, data: session }; } export async function fetchMe(accessToken: string) { const res = await authFetch<{ user?: Record }>("/auth/me", { token: accessToken }); if (!res.ok) return res; const rawUser = res.data.user ?? {}; const user: AuthUser = { id: String(rawUser.id ?? ""), firstName: rawUser.firstName as string | undefined, lastName: rawUser.lastName as string | undefined, cellNumber: rawUser.cellNumber as string | undefined, email: rawUser.email as string | undefined, }; return { ok: true as const, data: user }; }