/* Auth gate: email + password sign-in, shown until a session exists. On a fresh * deployment (no accounts yet) it becomes the first-run setup form — the account * created there is the admin. A collapsible fallback still accepts a raw API token * for headless/legacy setups. Holds no data; the session token lives in localStorage. */ "use client"; import { useEffect, useState } from "react"; import { authApi, type ApiError, type AuthStatus, type SessionResponse } from "@/lib/api"; type Mode = "loading" | "setup" | "login" | "forgot" | "token"; export function AuthGate({ error, onSession, onToken, }: { error?: string; /* A fresh session from login/setup: token + user. */ onSession: (s: SessionResponse) => void; /* Raw API-token fallback (legacy/scripts). */ onToken: (token: string) => void; }) { const [mode, setMode] = useState("loading"); const [status, setStatus] = useState(null); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); const [rawToken, setRawToken] = useState(""); const [message, setMessage] = useState(""); const [formError, setFormError] = useState(error ?? ""); const [busy, setBusy] = useState(false); useEffect(() => { authApi("/auth/status") .then((s) => { setStatus(s); setMode(s.initialized ? "login" : "setup"); }) .catch(() => { // API unreachable or very old server — fall back to the raw token prompt. setMode("token"); }); }, []); const submit = async (e: React.FormEvent) => { e.preventDefault(); setFormError(""); setMessage(""); if (mode === "token") { if (rawToken.trim()) onToken(rawToken.trim()); return; } setBusy(true); try { if (mode === "setup") { if (password !== confirm) { setFormError("Passwords don't match."); return; } onSession(await authApi("/auth/setup", { email, password })); } else if (mode === "login") { onSession(await authApi("/auth/login", { email, password })); } else if (mode === "forgot") { const r = await authApi<{ ok: boolean; emailed: boolean }>("/auth/forgot", { email }); setMessage( r.emailed ? "If that address has an account, a reset link is on its way." : "Email isn't configured on this deployment — ask an admin to generate a reset link for you.", ); } } catch (err) { setFormError((err as ApiError).message || "Something went wrong."); } finally { setBusy(false); } }; if (mode === "loading") { return (
Claude Monitor

Loading…

); } const heading = mode === "setup" ? "Welcome — create the first account. It becomes the admin; everyone else is invited by you." : mode === "forgot" ? "Enter your account email and we'll send a password reset link." : mode === "token" ? "Paste a raw API token (legacy / script access)." : "Sign in with your email and password."; return (
Claude Monitor

{heading}

{mode !== "token" && ( setEmail(e.target.value)} autoFocus required /> )} {(mode === "login" || mode === "setup") && ( setPassword(e.target.value)} minLength={8} required /> )} {mode === "setup" && ( setConfirm(e.target.value)} minLength={8} required /> )} {mode === "token" && ( setRawToken(e.target.value)} autoFocus /> )} {formError && (

{formError}

)} {message && (

{message}

)}
{mode === "login" && ( )} {(mode === "forgot" || mode === "token") && status?.initialized !== false && ( )} {mode !== "token" && ( )} {mode === "token" && status?.initialized === false && ( )}
); }