← back to PoppyPetitions
components/AuthProvider.tsx
133 lines
'use client';
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
ReactNode,
} from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Loader2 } from 'lucide-react';
/* ─── Types ──────────────────────────────────────────────────────────────── */
interface AuthContextValue {
user: string | null;
loading: boolean;
login: (username: string, password: string) => Promise<{ error?: string }>;
logout: () => Promise<void>;
}
/* ─── Context ────────────────────────────────────────────────────────────── */
const AuthContext = createContext<AuthContextValue | null>(null);
/* ─── Provider ───────────────────────────────────────────────────────────── */
export function AuthProvider({ children }: { children: ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [user, setUser] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
/* Check session on mount */
useEffect(() => {
let cancelled = false;
async function checkSession() {
try {
const res = await fetch('/api/auth/session', { credentials: 'include' });
if (!cancelled) {
if (res.ok) {
const data = await res.json();
setUser(data.authenticated ? data.user : null);
} else {
setUser(null);
}
}
} catch {
if (!cancelled) setUser(null);
} finally {
if (!cancelled) setLoading(false);
}
}
checkSession();
return () => { cancelled = true; };
}, []);
/* Redirect unauthenticated users away from protected routes */
useEffect(() => {
if (loading) return;
if (!user && pathname !== '/login') {
router.replace('/login');
}
}, [user, loading, pathname, router]);
/* Login */
const login = useCallback(async (username: string, password: string) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (res.ok && data.success) {
setUser(username);
router.push('/');
router.refresh();
return {};
}
return { error: data.error ?? 'Login failed' };
}, [router]);
/* Logout */
const logout = useCallback(async () => {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
setUser(null);
router.replace('/login');
}, [router]);
/* Loading screen — shown on every route while session is being verified */
if (loading) {
return (
<div
className="min-h-screen flex flex-col items-center justify-center gap-3"
style={{ backgroundColor: 'var(--color-bg)' }}
>
<Loader2
size={28}
className="animate-spin"
style={{ color: 'var(--color-primary)' }}
aria-hidden="true"
/>
<span
className="text-sm"
style={{ color: 'var(--color-text-secondary)' }}
>
Loading...
</span>
</div>
);
}
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
/* ─── Hook ───────────────────────────────────────────────────────────────── */
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used inside <AuthProvider>');
}
return ctx;
}