import { useEffect, useState } from "react"; import { ArrowRight, KeyRound, MessageSquareText, ShieldCheck, User, UserPlus, Zap } from "lucide-react"; import { api, setToken } from "../lib/api.ts"; import type { User as UserType } from "../lib/types.ts"; import { Button, FlowerMark, Input, Spinner } from "./ui.tsx"; import { RecoverDialog, RegisterDialog } from "./dialogs.tsx"; import { AccessRequestModal } from "./ReviewPage.tsx"; const PERKS = [ { icon: ShieldCheck, text: "Private by design — no cookies, no tracking" }, { icon: Zap, text: "Your own models, streamed in real time" }, ]; export function Login({ onLogin }: { onLogin: (user: UserType) => void }) { const [username, setUsername] = useState("admin"); const [key, setKey] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [recoverOpen, setRecoverOpen] = useState(false); const [recoveryOn, setRecoveryOn] = useState(false); const [recoveryFrom, setRecoveryFrom] = useState(undefined); const [registerOpen, setRegisterOpen] = useState(false); const [registrationOn, setRegistrationOn] = useState(false); const [requestOpen, setRequestOpen] = useState(false); const [requestOn, setRequestOn] = useState(false); // TOTP second step (set when the server answers totpRequired). const [totpToken, setTotpToken] = useState(null); const [totpUsername, setTotpUsername] = useState(""); const [code, setCode] = useState(""); useEffect(() => { api.methods().then((m) => { setRecoveryOn(m.recovery); setRecoveryFrom(m.from); setRegistrationOn(m.registration); setRequestOn(m.accessRequest); }).catch(() => {}); }, []); async function submit(e: React.FormEvent) { e.preventDefault(); if (busy) return; setError(""); setBusy(true); try { const res = await api.login(username.trim().toLowerCase(), key); if ("totpRequired" in res) { setTotpToken(res.totpToken); setTotpUsername(res.username); return; } setToken(res.token); onLogin(res.user); } catch (err) { setError(err instanceof Error ? err.message : "Login failed"); } finally { setBusy(false); } } async function submitTotp(e: React.FormEvent) { e.preventDefault(); if (busy || !totpToken) return; setError(""); setBusy(true); try { const res = await api.totpLogin(totpToken, code); setToken(res.token); onLogin(res.user); } catch (err) { setError(err instanceof Error ? err.message : "Invalid code"); } finally { setBusy(false); } } return (

Welcome back

Sign in to KisAssistant to continue

{totpToken ? (

Two-factor for @{totpUsername} — enter the 6-digit code from your authenticator app.

setCode(e.target.value.replace(/\D/g, "").slice(0, 6))} autoComplete="one-time-code" placeholder="000000" aria-label="Authenticator code" inputMode="numeric" required />
{error && (

{error}

)}
) : (
setUsername(e.target.value)} autoComplete="username" placeholder="Username" aria-label="Username" required />
setKey(e.target.value)} autoComplete="current-password" placeholder="Access key" aria-label="Access key" required />
{error && (

{error}

)}
)}
New here?
setRegisterOpen(false)} onDone={(u, k) => { setUsername(u); setKey(k); }} /> {/* Wishlist path: only when open registration is off AND the admin enabled access requests. They are mutually exclusive server-side. */} {!registrationOn && requestOn && ( )} setRequestOpen(false)} /> {recoveryOn && ( )} setRecoverOpen(false)} from={recoveryFrom} />
    {PERKS.map((p) => (
  • {p.text}
  • ))}
); }