The login and registration forms are the entry point for every authenticated user. Getting them right matters: clear error messages (wrong password vs account not found), no user enumeration (do not tell attackers which emails are registered), token storage after success, and smooth redirection to the intended destination. These forms connect directly to the FastAPI auth endpoints built in Chapter 29.
Login Form
// src/pages/LoginPage.jsx
import { useState } from "react";
import { useNavigate, useLocation, Link } from "react-router-dom";
import { authApi } from "@/services/auth";
import { useAuthStore } from "@/stores/authStore";
export default function LoginPage() {
const [form, setForm] = useState({ email: "", password: "" });
const [error, setError] = useState(null);
const [isLoading, setLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const { setTokens } = useAuthStore();
const from = location.state?.from?.pathname ?? "/dashboard";
function handleChange(e) {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
setError(null);
}
async function handleSubmit(e) {
e.preventDefault();
if (!form.email || !form.password) {
setError("Please enter your email and password");
return;
}
setLoading(true);
try {
const { access_token, refresh_token } = await authApi.login(
form.email.trim().toLowerCase(),
form.password,
);
// Store tokens — authStore handles persistence
setTokens(access_token, refresh_token);
navigate(from, { replace: true });
} catch (err) {
// Keep error message generic — prevent user enumeration
if (err.response?.status === 401) {
setError("Invalid email or password");
} else {
setError("Login failed. Please try again.");
}
} finally {
setLoading(false);
}
}
return (
<div className="max-w-sm mx-auto mt-16">
<h1 className="text-2xl font-bold mb-6">Sign In</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} noValidate className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">Email</label>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={handleChange}
autoComplete="email"
className="w-full border rounded-lg px-3 py-2"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium mb-1">Password</label>
<input
id="password"
name="password"
type="password"
value={form.password}
onChange={handleChange}
autoComplete="current-password"
className="w-full border rounded-lg px-3 py-2"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white py-2 rounded-lg disabled:opacity-50"
>
{isLoading ? "Signing in..." : "Sign In"}
</button>
</form>
<p className="text-center text-sm text-gray-500 mt-4">
No account?{" "}
<Link to="/register" className="text-blue-600 hover:underline">Sign up</Link>
</p>
</div>
);
}
Note: Always add
autoComplete attributes to auth form inputs — autoComplete="email" and autoComplete="current-password" on login; autoComplete="new-password" on registration. Password managers use these attributes to offer to save and autofill credentials. Without them, some password managers will not recognise the form fields and users must type their passwords manually every time. This is a significant usability improvement for essentially zero effort.Tip: After successful login, clear the password from state immediately to prevent it lingering in memory:
setForm(prev => ({ ...prev, password: "" })). While React state is in JavaScript memory (not persisted), it is good practice to not hold sensitive data longer than necessary. More importantly, if the component stays mounted (the form does not navigate away for some reason), the password is not visible in React DevTools.Warning: Do not show specific error messages that reveal whether an account exists — “Wrong password” vs “Account not found” allows attackers to enumerate registered email addresses. Always use a generic message: “Invalid email or password.” Only show specific messages for validation errors that cannot reveal account existence, such as “Email format is invalid” for a malformed email address.
Registration Form
// src/pages/RegisterPage.jsx
export default function RegisterPage() {
const [form, setForm] = useState({ name: "", email: "", password: "", confirm: "" });
const [errors, setErrors] = useState({});
const [isLoading, setLoading] = useState(false);
const navigate = useNavigate();
const { setTokens } = useAuthStore();
function validate() {
const errs = {};
if (form.name.trim().length < 2) errs.name = "Name must be at least 2 characters";
if (!form.email.includes("@")) errs.email = "Enter a valid email address";
if (form.password.length < 8) errs.password = "Password must be at least 8 characters";
if (!/[A-Z]/.test(form.password)) errs.password = "Password must contain an uppercase letter";
if (!/\d/.test(form.password)) errs.password = "Password must contain a number";
if (form.password !== form.confirm) errs.confirm = "Passwords do not match";
return errs;
}
async function handleSubmit(e) {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length) { setErrors(errs); return; }
setLoading(true);
try {
await authApi.register({ name: form.name, email: form.email, password: form.password });
// Auto-login after registration
const { access_token, refresh_token } = await authApi.login(form.email, form.password);
setTokens(access_token, refresh_token);
navigate("/dashboard", { replace: true });
} catch (err) {
if (err.response?.status === 409) {
setErrors({ email: "An account with this email already exists" });
} else {
setErrors({ _general: "Registration failed. Please try again." });
}
} finally {
setLoading(false);
}
}
// ... render similar to LoginPage with additional fields
}
Common Mistakes
Mistake 1 — Specific “wrong password” vs “user not found” messages
❌ Wrong — reveals which emails are registered:
if (err.response?.status === 401) {
const msg = err.response.data.detail;
setError(msg); // "Invalid email or password" sometimes, "User not found" other times!
✅ Correct — always generic:
if (err.response?.status === 401) setError("Invalid email or password"); // ✓
Mistake 2 — Missing autoComplete attributes
❌ Wrong — password manager cannot autofill:
<input type="password" value={form.password} /> // no autoComplete!
✅ Correct:
<input type="password" autoComplete="current-password" value={form.password} /> // ✓
Quick Reference
| Task | Code |
|---|---|
| Store tokens | authStore.setTokens(access_token, refresh_token) |
| Redirect after login | navigate(from, { replace: true }) |
| Handle 401 | Generic “Invalid email or password” message |
| Handle 409 (register) | setErrors({ email: "Email already taken" }) |
| AutoComplete login | autoComplete="email" and autoComplete="current-password" |
| AutoComplete register | autoComplete="new-password" on password field |