9de0132676
Package web/ : React 19 + Vite 8 + Tailwind 4 + react-router 7 + PWA. Tokens dark HUD Jarvis-red, client API, contexte auth JWT, shell + garde de route, écran Login (mot de passe + TOTP). Chat/Review en stubs. Build OK, 0 vuln. docs/ui-design.md. Palier de risque : reversible (front statique, aucun accès infra direct). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
import { useState, type FormEvent } from "react";
|
|
import { useAuth } from "../auth";
|
|
import { ApiError } from "../api";
|
|
|
|
export function Login() {
|
|
const { login } = useAuth();
|
|
const [user, setUser] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [totp, setTotp] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const submit = async (e: FormEvent): Promise<void> => {
|
|
e.preventDefault();
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await login(user, password, totp);
|
|
} catch (err) {
|
|
setError(err instanceof ApiError && err.status === 429 ? "Trop de tentatives, patiente." : "Identifiants invalides.");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const field = "w-full rounded-md bg-surface-2 border border-border px-3 py-2 text-fg placeholder:text-muted ring-accent";
|
|
|
|
return (
|
|
<div className="min-h-dvh grid place-items-center px-4">
|
|
<form onSubmit={submit} className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-surface p-6">
|
|
<h1 className="text-2xl font-bold tracking-wide glow text-accent">CHLOVA</h1>
|
|
<p className="text-sm text-muted">Accès propriétaire — authentification forte.</p>
|
|
|
|
<input className={field} placeholder="Utilisateur" autoComplete="username" value={user} onChange={(e) => setUser(e.target.value)} />
|
|
<input className={field} type="password" placeholder="Mot de passe" autoComplete="current-password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
<input className={field} inputMode="numeric" placeholder="Code 2FA (TOTP)" autoComplete="one-time-code" value={totp} onChange={(e) => setTotp(e.target.value)} />
|
|
|
|
{error && <p role="alert" className="text-sm text-danger">{error}</p>}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={busy}
|
|
className="w-full rounded-md bg-accent px-3 py-2 font-medium text-bg disabled:opacity-50 ring-accent cursor-pointer"
|
|
>
|
|
{busy ? "Connexion…" : "Se connecter"}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|