import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import type { ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { fetchWithAuth } from './http';
interface AuthState {
isAuthenticated: boolean;
username: string | null;
loading: boolean;
}
interface AuthContextValue extends AuthState {
login(username: string, password: string): Promise<boolean>;
logout(): Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
username: null,
loading: true,
});
useEffect(() => {
let cancelled = false;
fetchWithAuth('/api/auth/me')
.then((r) => r.json())
.then((data: { authenticated: boolean; username?: string }) => {
if (cancelled) return;
setState({
isAuthenticated: data.authenticated,
username: data.username ?? null,
loading: false,
});
})
.catch(() => {
if (cancelled) return;
setState({ isAuthenticated: false, username: null, loading: false });
});
return () => {
cancelled = true;
};
}, []);
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (res.ok && data.success) {
setState({
isAuthenticated: true,
username: data.user?.username ?? null,
loading: false,
});
return true;
}
return false;
} catch {
return false;
}
}, []);
const logout = useCallback(async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setState({ isAuthenticated: false, username: null, loading: false });
}, []);
const value = useMemo(
() => ({ ...state, login, logout }),
[state, login, logout],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}
export function ProtectedRoute({ children }: { children: ReactNode }) {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();
if (loading) return null;
if (!isAuthenticated) {
return (
<Navigate
to={`/login?redirect=${encodeURIComponent(location.pathname)}`}
replace
/>
);
}
return <>{children}</>;
}