"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { clearSharedAuthCookies, hydrateLocalTokensFromSharedCookies, SHARED_ACCESS_TOKEN_KEY, SHARED_REFRESH_TOKEN_KEY, writeSharedAuthCookies, } from "./shared-auth-cookie"; export type AuthBusinessMembership = { id: string; name: string; slug: string; isOwner: boolean; teamRole: string | null; }; export type AuthUser = { id: string; firstName?: string; lastName?: string; cellNumber?: string; email?: string; roles?: string[]; isSuperAdmin?: boolean; businesses?: AuthBusinessMembership[]; }; type SessionContextValue = { user: AuthUser | null; isAuthenticated: boolean; ready: boolean; businessId: string; isWebsiteAdmin: boolean; logout: () => void; getAccessToken: () => string | null; }; function normalizeUser(raw: Partial & { id?: string | number }): AuthUser | null { if (raw.id == null) return null; return { id: String(raw.id), firstName: raw.firstName ?? undefined, lastName: raw.lastName ?? undefined, cellNumber: raw.cellNumber ?? undefined, email: raw.email ?? undefined, roles: Array.isArray(raw.roles) ? raw.roles : undefined, isSuperAdmin: Boolean(raw.isSuperAdmin), businesses: Array.isArray(raw.businesses) ? raw.businesses.map((business) => ({ id: String(business.id), name: business.name, slug: business.slug, isOwner: Boolean(business.isOwner), teamRole: business.teamRole ?? null, })) : undefined, }; } export function isWebsiteAdminForBusiness(user: AuthUser | null | undefined, businessId: string): boolean { if (!user || !businessId) return false; if (user.isSuperAdmin || user.roles?.includes("super_admin")) return true; return Boolean( user.businesses?.some( (business) => String(business.id) === String(businessId) && (business.isOwner || Boolean(business.teamRole)), ), ); } const SessionContext = createContext(null); function readToken(key: string): string | null { try { return localStorage.getItem(key); } catch { return null; } } function writeToken(key: string, value: string | null) { try { if (value) localStorage.setItem(key, value); else localStorage.removeItem(key); } catch { // ignore } } export function SessionProvider({ children, businessId, }: { children: ReactNode; businessId: string; }) { const [user, setUser] = useState(null); const [ready, setReady] = useState(false); const getAccessToken = useCallback(() => { hydrateLocalTokensFromSharedCookies(SHARED_ACCESS_TOKEN_KEY, SHARED_REFRESH_TOKEN_KEY); return readToken(SHARED_ACCESS_TOKEN_KEY); }, []); const clearTokens = useCallback(() => { writeToken(SHARED_ACCESS_TOKEN_KEY, null); writeToken(SHARED_REFRESH_TOKEN_KEY, null); writeToken("accessToken", null); writeToken("meshkee_access_token", null); clearSharedAuthCookies(); setUser(null); }, []); useEffect(() => { const token = getAccessToken(); if (!token) { setReady(true); return; } const refresh = readToken(SHARED_REFRESH_TOKEN_KEY); writeSharedAuthCookies(token, refresh ?? undefined); fetch("/api/auth/me", { headers: { Authorization: `Bearer ${token}` } }) .then(async (res) => { if (res.status === 401) { clearTokens(); return; } if (!res.ok) return; const data = (await res.json()) as { user?: AuthUser } & AuthUser; const next = normalizeUser(data.user ?? data); if (next) setUser(next); }) .catch(() => undefined) .finally(() => setReady(true)); }, [clearTokens, getAccessToken]); const logout = useCallback(() => { clearTokens(); }, [clearTokens]); const isWebsiteAdmin = useMemo(() => isWebsiteAdminForBusiness(user, businessId), [user, businessId]); const value = useMemo( () => ({ user, isAuthenticated: Boolean(user), ready, businessId, isWebsiteAdmin, logout, getAccessToken, }), [user, ready, businessId, isWebsiteAdmin, logout, getAccessToken], ); return {children}; } export function useSession() { const ctx = useContext(SessionContext); if (!ctx) throw new Error("useSession must be used within SessionProvider"); return ctx; }