peerman 0.2.0

DN42 peer manager with WireGuard, BIRD, and cluster support
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}</>;
}