Wire up email verification flow for new signups

requireEmailVerification was enabled but no sendVerificationEmail was
configured — new users saw "check your email", never received anything,
and couldn't log in (403 shown as "wrong email or password").

- email.ts: add verification email template; welcome email no longer
  claims the account is "confirmed"
- auth.ts: configure emailVerification (send on signup, resend on
  unverified login attempt, auto sign-in after verification, 24h TTL)
- register route: callbackURL=/dashboard so the verify link lands in
  the cabinet
- login form: distinguish 403 (unverified — tell user a fresh link was
  sent) and 429 (rate limit) from wrong credentials

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 14:01:46 +05:00
parent 619ae393bd
commit 2790a7d24b
4 changed files with 52 additions and 4 deletions
+23 -1
View File
@@ -10,17 +10,27 @@ export function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [needsVerification, setNeedsVerification] = useState(false);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setNeedsVerification(false);
setLoading(true);
const result = await signIn.email({ email, password });
if (result.error) {
setError("Неверный email или пароль");
if (result.error.status === 403) {
// Креды верны, но email не подтверждён. Better Auth уже
// переотправил письмо подтверждения — сообщаем об этом.
setNeedsVerification(true);
} else if (result.error.status === 429) {
setError("Слишком много попыток входа. Подождите минуту и попробуйте снова.");
} else {
setError("Неверный email или пароль");
}
setLoading(false);
return;
}
@@ -82,6 +92,18 @@ export function LoginForm() {
placeholder="••••••••"
/>
</div>
{needsVerification && (
<div
className="text-xs py-3 px-3 space-y-1"
style={{ border: "2px solid var(--border)", color: "var(--foreground)" }}
>
<p className="font-bold">Email не подтверждён</p>
<p style={{ color: "var(--muted-foreground)" }}>
Мы отправили новое письмо со ссылкой подтверждения на <strong>{email}</strong>.
Проверьте почту (и папку «Спам»), затем войдите снова.
</p>
</div>
)}
{error && (
<p className="text-sm" style={{ color: "var(--destructive)" }}>
{error}