Files
AnthoLume/frontend/src/auth/AuthContext.tsx
2026-03-22 17:21:33 -04:00

118 lines
3.6 KiB
TypeScript

import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLogin, useLogout, useGetMe } from '../generated/anthoLumeAPIV1';
interface AuthState {
isAuthenticated: boolean;
user: { username: string; is_admin: boolean } | null;
isCheckingAuth: boolean;
}
interface AuthContextType extends AuthState {
login: (_username: string, _password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
user: null,
isCheckingAuth: true, // Start with checking state to prevent redirects during initial load
});
const loginMutation = useLogin();
const logoutMutation = useLogout();
// Always call /me to check authentication status
const { data: meData, error: meError, isLoading: meLoading } = useGetMe();
const navigate = useNavigate();
// Update auth state based on /me endpoint response
useEffect(() => {
setAuthState(prev => {
if (meLoading) {
// Still checking authentication
console.log('[AuthContext] Checking authentication status...');
return { ...prev, isCheckingAuth: true };
} else if (meData?.data && meData.status === 200) {
// User is authenticated - check that response has valid data
console.log('[AuthContext] User authenticated:', meData.data);
return {
isAuthenticated: true,
user: meData.data,
isCheckingAuth: false,
};
} else if (
meError ||
(meData && meData.status === 401) ||
(meData && meData.status === 403)
) {
// User is not authenticated or error occurred
console.log('[AuthContext] User not authenticated:', meError?.message || String(meError));
return {
isAuthenticated: false,
user: null,
isCheckingAuth: false,
};
}
console.log('[AuthContext] Unexpected state - checking...');
return { ...prev, isCheckingAuth: false }; // Assume not authenticated if we can't determine
});
}, [meData, meError, meLoading]);
const login = useCallback(
async (username: string, password: string) => {
try {
const response = await loginMutation.mutateAsync({
data: {
username,
password,
},
});
// The backend uses session-based authentication, so no token to store
// The session cookie is automatically set by the browser
setAuthState({
isAuthenticated: true,
user: response.data,
isCheckingAuth: false,
});
navigate('/');
} catch (_error) {
console.error('[AuthContext] Login failed:', _error);
throw new Error('Login failed');
}
},
[loginMutation, navigate]
);
const logout = useCallback(() => {
logoutMutation.mutate(undefined, {
onSuccess: () => {
setAuthState({
isAuthenticated: false,
user: null,
isCheckingAuth: false,
});
navigate('/login');
},
});
}, [logoutMutation, navigate]);
return (
<AuthContext.Provider value={{ ...authState, login, logout }}>{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;
}