import { useState, type FormEvent } from "react"; import { useNavigate, useSearchParams, Navigate } from "react-router-dom"; import { useAuth } from "@/auth/useAuth"; /** * Sign-in screen. Mounted by the `/login` route in `App.tsx`, which * sits outside `` so unauthenticated operators can reach * it. The page is also reachable via the auto-redirect on a 401 from * `authedFetch` — that path appends `?next=` so the operator * lands back on the page they were trying to view. * * Error mapping is driven by the backend's `error` code on the * `ApiError` thrown from `authApi.login`. The codes are defined in * `backend/src/cyclone/auth/routes.py::login`. */ export function Login() { const { user, login } = useAuth(); const [params] = useSearchParams(); const next = params.get("next") ?? "/"; const navigate = useNavigate(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); // Already authenticated — bounce to the original destination (or // dashboard) so we don't show a sign-in form to a logged-in user. if (user) return ; async function onSubmit(e: FormEvent) { e.preventDefault(); setError(null); setSubmitting(true); try { await login(username, password); navigate(next, { replace: true }); } catch (err) { const code = (err as { code?: string })?.code ?? "error"; if (code === "invalid_credentials") { setError("Username or password is incorrect."); } else if (code === "account_disabled") { setError("Account is disabled. Contact your administrator."); } else if (code === "rate_limited") { setError("Too many attempts. Try again shortly."); } else { setError("Sign in failed. Try again."); } } finally { setSubmitting(false); } } return (

Cyclone

Sign in to continue.

{error ? (
{error}
) : null}
); }