import { createContext, useEffect, useState, useCallback, ReactNode, useRef } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { SSOClient } from '@lilith/sso-client'; import { authEvents } from './auth-events'; import type { AuthContextValue, AuthState, LoginCredentials, RegisterData, DirectRegisterData, User, DevAuthOverride, } from './types'; interface AuthProviderProps { children: ReactNode; /** * SSO service URL (required) */ ssoUrl: string; /** * Session check interval in ms (default: 300000 = 5 minutes) */ checkInterval?: number; /** * Popup width for login/register (default: 500) */ popupWidth?: number; /** * Popup height for login/register (default: 600) */ popupHeight?: number; /** * Dev mode override - when provided, bypasses real SSO auth. * Only functional when import.meta.env.DEV is true. */ devOverride?: DevAuthOverride; } export const AuthContext = createContext(undefined); export function AuthProvider({ children, ssoUrl, checkInterval = 300000, popupWidth = 500, popupHeight = 600, devOverride, }: AuthProviderProps) { const queryClient = useQueryClient(); const ssoClientRef = useRef(null); // Check if dev override is active (only in dev mode) const isDevOverrideActive = import.meta.env.DEV && devOverride !== undefined; const [authState, setAuthState] = useState({ user: null, isLoading: !isDevOverrideActive, // No loading when dev override isAuthenticated: false, error: null, isDevMode: false, }); // Sync dev override to auth state useEffect(() => { if (isDevOverrideActive && devOverride) { setAuthState({ user: devOverride.user, isLoading: false, isAuthenticated: devOverride.isAuthenticated, error: null, isDevMode: true, }); // Also update query cache queryClient.setQueryData(['auth', 'me'], devOverride.user); } }, [isDevOverrideActive, devOverride, queryClient]); // Skip SSO client setup when dev override is active useEffect(() => { if (isDevOverrideActive) { return; // Don't initialize SSO client in dev override mode } const ssoClient = new SSOClient({ ssoUrl, checkInterval, popupWidth, popupHeight, onAuthChange: (authenticated, user) => { setAuthState({ user: user as User | null, isLoading: false, isAuthenticated: authenticated, error: null, }); if (authenticated && user) { authEvents.broadcast({ type: 'login', timestamp: Date.now(), }); } else { authEvents.broadcast({ type: 'logout', timestamp: Date.now(), }); } queryClient.setQueryData(['auth', 'me'], user); }, onError: (error) => { setAuthState((prev) => ({ ...prev, error, isLoading: false, })); }, }); ssoClientRef.current = ssoClient; ssoClient.checkSession().then((response) => { setAuthState({ user: response.user as User | null, isLoading: false, isAuthenticated: response.authenticated, error: null, }); if (response.user) { queryClient.setQueryData(['auth', 'me'], response.user); } }); ssoClient.startAutoCheck(); authEvents.initialize(); const unsubscribe = authEvents.subscribe((event) => { if (event.type === 'login' || event.type === 'token_refresh') { ssoClient.checkSession(); } else if (event.type === 'logout') { setAuthState({ user: null, isLoading: false, isAuthenticated: false, error: null, }); queryClient.setQueryData(['auth', 'me'], null); queryClient.clear(); } }); return () => { ssoClient.destroy(); unsubscribe(); authEvents.destroy(); }; }, [ssoUrl, checkInterval, popupWidth, popupHeight, queryClient, isDevOverrideActive]); const login = useCallback( async (_credentials: LoginCredentials) => { if (!ssoClientRef.current) { throw new Error('SSO client not initialized'); } setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); try { const user = await ssoClientRef.current.login(); setAuthState({ user: user as User, isLoading: false, isAuthenticated: true, error: null, }); queryClient.setQueryData(['auth', 'me'], user); authEvents.broadcast({ type: 'login', timestamp: Date.now(), }); } catch (error) { setAuthState((prev) => ({ ...prev, isLoading: false, error: error as Error, })); throw error; } }, [queryClient] ); const register = useCallback( async (data: RegisterData) => { if (!ssoClientRef.current) { throw new Error('SSO client not initialized'); } setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); try { const user = await ssoClientRef.current.register({ role: data.role }); setAuthState({ user: user as User, isLoading: false, isAuthenticated: true, error: null, }); queryClient.setQueryData(['auth', 'me'], user); authEvents.broadcast({ type: 'login', timestamp: Date.now(), }); } catch (error) { setAuthState((prev) => ({ ...prev, isLoading: false, error: error as Error, })); throw error; } }, [queryClient] ); const loginWithCredentials = useCallback( async (email: string, password: string) => { if (!ssoClientRef.current) { throw new Error('SSO client not initialized'); } setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); try { const result = await ssoClientRef.current.loginWithCredentials(email, password); if (result.mfaRequired) { // MFA will be handled by the SSOClient's onMfaRequired callback setAuthState((prev) => ({ ...prev, isLoading: false })); return; } setAuthState({ user: result.user as User, isLoading: false, isAuthenticated: true, error: null, }); queryClient.setQueryData(['auth', 'me'], result.user); authEvents.broadcast({ type: 'login', timestamp: Date.now(), }); } catch (error) { setAuthState((prev) => ({ ...prev, isLoading: false, error: error as Error, })); throw error; } }, [queryClient] ); const registerWithCredentials = useCallback( async (data: DirectRegisterData) => { if (!ssoClientRef.current) { throw new Error('SSO client not initialized'); } setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); try { const result = await ssoClientRef.current.registerWithCredentials( data.email, data.username, data.password, data.role ); setAuthState({ user: result.user as User, isLoading: false, isAuthenticated: true, error: null, }); queryClient.setQueryData(['auth', 'me'], result.user); authEvents.broadcast({ type: 'login', timestamp: Date.now(), }); } catch (error) { setAuthState((prev) => ({ ...prev, isLoading: false, error: error as Error, })); throw error; } }, [queryClient] ); const logout = useCallback(async () => { if (!ssoClientRef.current) { throw new Error('SSO client not initialized'); } setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); try { await ssoClientRef.current.logout(); setAuthState({ user: null, isLoading: false, isAuthenticated: false, error: null, }); queryClient.setQueryData(['auth', 'me'], null); queryClient.clear(); authEvents.broadcast({ type: 'logout', timestamp: Date.now(), }); } catch (error) { setAuthState((prev) => ({ ...prev, isLoading: false, error: error as Error, })); throw error; } }, [queryClient]); const refreshAuth = useCallback(async () => { if (!ssoClientRef.current) { throw new Error('SSO client not initialized'); } const response = await ssoClientRef.current.checkSession(); setAuthState({ user: response.user as User | null, isLoading: false, isAuthenticated: response.authenticated, error: null, }); if (response.user) { queryClient.setQueryData(['auth', 'me'], response.user); } }, [queryClient]); const contextValue: AuthContextValue = { ...authState, login, register, loginWithCredentials, registerWithCredentials, logout, refreshAuth, }; return {children}; }