SPB Git forge

spb/llm-api

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
3.2 KB · 74 lines tsx
Raw Blame History
1"use client";23import { useRouter, useSearchParams } from "next/navigation";4import { Suspense, useEffect, useState } from "react";5import { api, ApiError } from "@/lib/api";67function LoginForm() {8  const router = useRouter();9  const params = useSearchParams();10  const [needsSetup, setNeedsSetup] = useState(false);11  const [email, setEmail] = useState("");12  const [password, setPassword] = useState("");13  const [confirm, setConfirm] = useState("");14  const [error, setError] = useState<string | null>(null);15  const [busy, setBusy] = useState(false);1617  useEffect(() => {18    api.get<{ needs_setup: boolean; authenticated: boolean }>("/api/auth/status").then((s) => {19      setNeedsSetup(s.needs_setup);20      if (s.authenticated) router.replace(params.get("next") || "/");21    }).catch(() => {});22  }, [router, params]);2324  const submit = async (e: React.FormEvent) => {25    e.preventDefault();26    setError(null);27    if (needsSetup && password !== confirm) return setError("Passwords do not match.");28    setBusy(true);29    try {30      await api.post(needsSetup ? "/api/auth/setup" : "/api/auth/login", { email, password });31      router.replace(params.get("next") || "/");32    } catch (err) {33      setError(err instanceof ApiError ? err.message : "Login failed");34    } finally {35      setBusy(false);36    }37  };3839  return (40    <div className="min-h-screen grid place-items-center p-6">41      <form onSubmit={submit} className="card w-full max-w-sm p-7 flex flex-col gap-4">42        <div className="flex items-center gap-2.5 mb-1">43          <div className="h-8 w-8 rounded-lg bg-accent grid place-items-center text-white font-bold">λ</div>44          <div>45            <div className="font-semibold tracking-tight">LLM API</div>46            <div className="text-xs text-ink-3">{needsSetup ? "Create the administrator account" : "Sign in to the console"}</div>47          </div>48        </div>49        <label className="flex flex-col gap-1 text-sm">50          <span className="label">Email</span>51          <input className="input" type="email" autoComplete="username" value={email} onChange={(e) => setEmail(e.target.value)} required />52        </label>53        <label className="flex flex-col gap-1 text-sm">54          <span className="label">Password</span>55          <input className="input" type="password" autoComplete={needsSetup ? "new-password" : "current-password"} value={password} onChange={(e) => setPassword(e.target.value)} required minLength={needsSetup ? 10 : 1} />56        </label>57        {needsSetup && (58          <label className="flex flex-col gap-1 text-sm">59            <span className="label">Confirm password</span>60            <input className="input" type="password" autoComplete="new-password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required />61          </label>62        )}63        {error && <div className="text-sm text-bad">{error}</div>}64        <button className="btn btn-primary h-9" disabled={busy}>{busy ? "…" : needsSetup ? "Create account" : "Sign in"}</button>65        <div className="text-[11px] text-ink-3 text-center">Private system · all access is logged</div>66      </form>67    </div>68  );69}7071export default function LoginPage() {72  return <Suspense><LoginForm /></Suspense>;73}74