167 lines
4.6 KiB
TypeScript
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;
|
|
}
|