import { createContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { useIdentify } from "@/lib/posthog"; import { getMeRequest, loginRequest, verifyMfaRequest } from "./api"; import { AUTH_TOKEN_COOKIE, AUTH_USER_COOKIE, clearSessionCookies, getCookie, setCookie, } from "./cookies"; import { applyTokens } from "./http"; import { startTokenRefreshScheduler, stopTokenRefreshScheduler, } from "./refreshScheduler"; import type { AuthTokens, AuthUser } from "./types"; interface LoginPayload { email: string; password: string; } interface VerifyMfaPayload { email: string; otp: string; } export interface AuthContextValue { user: AuthUser | null; loading: boolean; login: (payload: LoginPayload) => Promise<{ mfaRequired: boolean }>; verifyMfa: (payload: VerifyMfaPayload) => Promise; logout: () => void; } export const AuthContext = createContext(null); const persistUser = (user: AuthUser | null) => { if (user) { setCookie(AUTH_USER_COOKIE, JSON.stringify(user)); return; } clearSessionCookies(); }; const bootstrapCachedUser = (): AuthUser | null => { const serialized = getCookie(AUTH_USER_COOKIE); if (!serialized) { return null; } try { return JSON.parse(serialized) as AuthUser; } catch { return null; } }; export const AuthProvider = ({ children }: { children: ReactNode }) => { const [user, setUser] = useState(() => bootstrapCachedUser()); const [loading, setLoading] = useState(true); const mfaEmailRef = useRef(null); // Attribute replays and exceptions to the signed-in user (id/org only). useIdentify(user); const loadCurrentUser = async () => { const currentUser = await getMeRequest(); setUser(currentUser); persistUser(currentUser); }; useEffect(() => { ["auth-token", "refresh-token"].forEach((name) => { const value = getCookie(name); if (value === "undefined" || value === "null" || value === "") { clearSessionCookies(); } }); const bootstrap = async () => { const token = getCookie(AUTH_TOKEN_COOKIE); if (!token) { setLoading(false); return; } try { await loadCurrentUser(); } catch { clearSessionCookies(); setUser(null); } finally { setLoading(false); } }; void bootstrap(); }, []); // Keep the server session alive while a user is logged in. Runs after // login, MFA verification, and page-reload bootstrap alike. useEffect(() => { if (!user) { stopTokenRefreshScheduler(); return; } startTokenRefreshScheduler(); return stopTokenRefreshScheduler; }, [user]); const value = useMemo( () => ({ user, loading, login: async ({ email, password }) => { const result = await loginRequest({ email, password }); if (result.mfaRequired) { mfaEmailRef.current = email; return { mfaRequired: true }; } const tokens = result as AuthTokens; applyTokens(tokens); await loadCurrentUser(); mfaEmailRef.current = null; return { mfaRequired: false }; }, verifyMfa: async ({ email, otp }) => { const identifier = mfaEmailRef.current ?? email; const tokens = await verifyMfaRequest({ email: identifier, otp }); applyTokens(tokens); await loadCurrentUser(); mfaEmailRef.current = null; }, logout: () => { const preservedTheme = window.localStorage.getItem("edr-theme"); clearSessionCookies(); window.localStorage.clear(); if (preservedTheme === "dark" || preservedTheme === "light") { window.localStorage.setItem("edr-theme", preservedTheme); } setUser(null); window.location.replace("/auth"); }, }), [loading, user], ); return {children}; };