import { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from 'react'; import { User, AccessLevel } from '../types'; import { getCurrentUser, login as apiLogin, logout as apiLogout, getMyProjectAccess } from '../api'; interface PermissionCacheEntry { accessLevel: AccessLevel | null; timestamp: number; } interface AuthContextType { user: User | null; loading: boolean; error: string | null; login: (username: string, password: string) => Promise; logout: () => Promise; refreshUser: () => Promise; clearError: () => void; getProjectPermission: (projectName: string) => Promise; invalidatePermissionCache: (projectName?: string) => void; } const AuthContext = createContext(undefined); interface AuthProviderProps { children: ReactNode; } // Cache TTL in milliseconds (5 minutes) const PERMISSION_CACHE_TTL = 5 * 60 * 1000; export function AuthProvider({ children }: AuthProviderProps) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const permissionCacheRef = useRef>(new Map()); // Clear permission cache const clearPermissionCache = useCallback(() => { permissionCacheRef.current.clear(); }, []); // Check session on initial load useEffect(() => { async function checkAuth() { try { const currentUser = await getCurrentUser(); setUser(currentUser); } catch { setUser(null); } finally { setLoading(false); } } checkAuth(); }, []); const login = useCallback(async (username: string, password: string) => { setLoading(true); setError(null); try { const loggedInUser = await apiLogin({ username, password }); setUser(loggedInUser); // Clear permission cache on login - permissions may have changed clearPermissionCache(); } catch (err) { const message = err instanceof Error ? err.message : 'Login failed'; setError(message); throw err; } finally { setLoading(false); } }, [clearPermissionCache]); const logout = useCallback(async () => { setLoading(true); setError(null); try { await apiLogout(); setUser(null); // Clear permission cache on logout clearPermissionCache(); } catch (err) { const message = err instanceof Error ? err.message : 'Logout failed'; setError(message); throw err; } finally { setLoading(false); } }, [clearPermissionCache]); const clearError = useCallback(() => { setError(null); }, []); const refreshUser = useCallback(async () => { try { const currentUser = await getCurrentUser(); setUser(currentUser); } catch { setUser(null); } }, []); // Get project permission with caching const getProjectPermission = useCallback(async (projectName: string): Promise => { const cached = permissionCacheRef.current.get(projectName); const now = Date.now(); // Return cached value if still valid if (cached && (now - cached.timestamp) < PERMISSION_CACHE_TTL) { return cached.accessLevel; } // Fetch fresh permission try { const result = await getMyProjectAccess(projectName); const entry: PermissionCacheEntry = { accessLevel: result.access_level, timestamp: now, }; permissionCacheRef.current.set(projectName, entry); return result.access_level; } catch { // On error, cache null to avoid repeated failed requests const entry: PermissionCacheEntry = { accessLevel: null, timestamp: now, }; permissionCacheRef.current.set(projectName, entry); return null; } }, []); // Invalidate permission cache for a specific project or all projects const invalidatePermissionCache = useCallback((projectName?: string) => { if (projectName) { permissionCacheRef.current.delete(projectName); } else { clearPermissionCache(); } }, [clearPermissionCache]); return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }