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
+11 -1
View File
@@ -3,7 +3,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma";
import { admin } from "better-auth/plugins";
import { prisma } from "./prisma";
import bcrypt from "bcryptjs";
import { sendWelcomeEmail, sendPasswordResetEmail } from "./email";
import { sendWelcomeEmail, sendPasswordResetEmail, sendVerificationEmail } from "./email";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
@@ -20,6 +20,16 @@ export const auth = betterAuth({
await sendPasswordResetEmail(user.email, user.name, url);
},
},
emailVerification: {
// Без этого блока requireEmailVerification блокирует вход,
// а письмо подтверждения никто не отправляет — тупик для новых юзеров.
sendVerificationEmail: async ({ user, url }) => {
await sendVerificationEmail(user.email, user.name, url);
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 60 * 60 * 24, // 24h
},
databaseHooks: {
user: {
create: {
+17 -1
View File
@@ -167,7 +167,7 @@ export async function sendWelcomeEmail(to: string, name: string) {
subject: `Добро пожаловать в ${school}`,
html: base(`
<p ${p}>Привет, ${name}!</p>
<p ${p}>Ваш аккаунт на образовательной платформе <strong>${school}</strong> подтверждён.</p>
<p ${p}>Ваш аккаунт на образовательной платформе <strong>${school}</strong> создан.</p>
<p ${pLast}>После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
${btn(`${BASE_URL}/dashboard`, "Перейти на платформу")}
`, school),
@@ -229,6 +229,22 @@ export async function sendPasswordResetEmail(to: string, name: string, resetUrl:
}).catch((e) => console.error("[email] sendPasswordResetEmail:", e));
}
export async function sendVerificationEmail(to: string, name: string, verifyUrl: string) {
const school = await getSchoolName();
await getResend().emails.send({
from: FROM,
to,
subject: `Подтвердите email — ${school}`,
html: base(`
<p ${p}>Привет, ${name}!</p>
<p ${p}>Вы зарегистрировались на образовательной платформе <strong>${school}</strong>.</p>
<p ${p}>Нажмите на кнопку ниже, чтобы подтвердить email и активировать аккаунт. Ссылка действительна <strong>24 часа</strong>.</p>
<p ${pLast}>Если вы не регистрировались — просто проигнорируйте это письмо.</p>
${btn(verifyUrl, "Подтвердить email")}
`, school),
}).catch((e) => console.error("[email] sendVerificationEmail:", e));
}
export async function sendQuestionCreatedEmail(
to: string,
recipientName: string,