This commit is contained in:
Anaïs Saraiva
2026-03-29 18:45:25 +02:00
parent 0f7d692d12
commit afc839d43d
42 changed files with 3809 additions and 58 deletions
+75
View File
@@ -0,0 +1,75 @@
import { useState } from "react";
import type { FormEvent } from "react";
type LoginPageProps = {
busy: boolean;
error: string | null;
onLogin: (email: string, password: string, totpCode?: string) => Promise<void>;
};
export const LoginPage = ({ busy, error, onLogin }: LoginPageProps) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [totpCode, setTotpCode] = useState("");
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
await onLogin(email, password, totpCode || undefined);
};
return (
<main className="auth-shell">
<section className="auth-card">
<div className="auth-card__eyebrow">Public internet hardened</div>
<h1>Secure Webmail</h1>
<p>
Sessions stay on the server, cookies are <code>httpOnly</code>, and every state-changing request carries an in-memory CSRF token.
</p>
<form className="auth-form" onSubmit={handleSubmit}>
<label>
Email
<input
autoComplete="username"
inputMode="email"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
</label>
<label>
Password
<input
autoComplete="current-password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
required
/>
</label>
<label>
Two-factor code
<input
autoComplete="one-time-code"
inputMode="numeric"
pattern="\d{6}"
type="text"
value={totpCode}
onChange={(event) => setTotpCode(event.target.value)}
placeholder="123456"
/>
</label>
{error ? <div className="form-error">{error}</div> : null}
<button type="submit" className="primary-button" disabled={busy}>
{busy ? "Signing in..." : "Sign in"}
</button>
</form>
</section>
</main>
);
};