Implement authentication system with access control UI
This commit is contained in:
166
frontend/src/contexts/AuthContext.tsx
Normal file
166
frontend/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user