118 lines
3.6 KiB
TypeScript
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;
|
|
}
|