← back to Hub

components/AuthProvider.tsx

150 lines

'use client';

import {
  createContext,
  useContext,
  useEffect,
  useState,
  useCallback,
  ReactNode,
} from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Loader2 } from 'lucide-react';

/* ---- Types --------------------------------------------------------------- */
export type Role = 'admin' | 'member' | 'viewer' | null;

interface AuthContextValue {
  user: string | null;
  role: Role;
  isAdmin: boolean;
  canWrite: boolean;
  canUseAI: boolean;
  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 [role, setRole]       = useState<Role>(null);
  const [isAdmin, setIsAdmin] = useState(false);
  const [canWrite, setCanWrite] = useState(false);
  const [canUseAI, setCanUseAI] = useState(false);
  const [loading, setLoading] = useState(true);

  /* Fetch session identity (user + capabilities) */
  const checkSession = useCallback(async (): Promise<boolean> => {
    try {
      const res = await fetch('/api/auth/session', { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        if (data.authenticated) {
          setUser(data.user);
          setRole(data.role ?? null);
          setIsAdmin(Boolean(data.isAdmin));
          setCanWrite(Boolean(data.canWrite));
          setCanUseAI(Boolean(data.canUseAI));
          return true;
        }
      }
      setUser(null); setRole(null); setIsAdmin(false); setCanWrite(false); setCanUseAI(false);
      return false;
    } catch {
      setUser(null); setRole(null); setIsAdmin(false); setCanWrite(false); setCanUseAI(false);
      return false;
    }
  }, []);

  /* Check session on mount */
  useEffect(() => {
    let cancelled = false;
    (async () => {
      await checkSession();
      if (!cancelled) setLoading(false);
    })();
    return () => { cancelled = true; };
  }, [checkSession]);

  /* 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) {
      await checkSession();
      router.push('/');
      router.refresh();
      return {};
    }

    return { error: data.error ?? 'Login failed' };
  }, [router, checkSession]);

  /* Logout */
  const logout = useCallback(async () => {
    await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
    setUser(null); setRole(null); setIsAdmin(false); setCanWrite(false); setCanUseAI(false);
    router.replace('/login');
  }, [router]);

  /* Loading screen */
  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, role, isAdmin, canWrite, canUseAI, 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;
}