Files
orchard/frontend/src/contexts/AuthContext.tsx

167 lines
4.6 KiB
TypeScript

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<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
clearError: () => void;
getProjectPermission: (projectName: string) => Promise<AccessLevel | null>;
invalidatePermissionCache: (projectName?: string) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(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<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const permissionCacheRef = useRef<Map<string, PermissionCacheEntry>>(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<AccessLevel | null> => {
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 (
<AuthContext.Provider value={{
user,
loading,
error,
login,
logout,
refreshUser,
clearError,
getProjectPermission,
invalidatePermissionCache,
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}