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:
@@ -10,17 +10,27 @@ export function LoginForm() {
|
|||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [needsVerification, setNeedsVerification] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
|
setNeedsVerification(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const result = await signIn.email({ email, password });
|
const result = await signIn.email({ email, password });
|
||||||
|
|
||||||
if (result.error) {
|
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);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -82,6 +92,18 @@ export function LoginForm() {
|
|||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{error && (
|
||||||
<p className="text-sm" style={{ color: "var(--destructive)" }}>
|
<p className="text-sm" style={{ color: "var(--destructive)" }}>
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export async function POST(request: NextRequest) {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Origin: baseUrl,
|
Origin: baseUrl,
|
||||||
}),
|
}),
|
||||||
body: JSON.stringify({ name, email, password }),
|
body: JSON.stringify({ name, email, password, callbackURL: "/dashboard" }),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -3,7 +3,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma";
|
|||||||
import { admin } from "better-auth/plugins";
|
import { admin } from "better-auth/plugins";
|
||||||
import { prisma } from "./prisma";
|
import { prisma } from "./prisma";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { sendWelcomeEmail, sendPasswordResetEmail } from "./email";
|
import { sendWelcomeEmail, sendPasswordResetEmail, sendVerificationEmail } from "./email";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: prismaAdapter(prisma, {
|
database: prismaAdapter(prisma, {
|
||||||
@@ -20,6 +20,16 @@ export const auth = betterAuth({
|
|||||||
await sendPasswordResetEmail(user.email, user.name, url);
|
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: {
|
databaseHooks: {
|
||||||
user: {
|
user: {
|
||||||
create: {
|
create: {
|
||||||
|
|||||||
+17
-1
@@ -167,7 +167,7 @@ export async function sendWelcomeEmail(to: string, name: string) {
|
|||||||
subject: `Добро пожаловать в ${school}`,
|
subject: `Добро пожаловать в ${school}`,
|
||||||
html: base(`
|
html: base(`
|
||||||
<p ${p}>Привет, ${name}!</p>
|
<p ${p}>Привет, ${name}!</p>
|
||||||
<p ${p}>Ваш аккаунт на образовательной платформе <strong>${school}</strong> подтверждён.</p>
|
<p ${p}>Ваш аккаунт на образовательной платформе <strong>${school}</strong> создан.</p>
|
||||||
<p ${pLast}>После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
|
<p ${pLast}>После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
|
||||||
${btn(`${BASE_URL}/dashboard`, "Перейти на платформу")}
|
${btn(`${BASE_URL}/dashboard`, "Перейти на платформу")}
|
||||||
`, school),
|
`, school),
|
||||||
@@ -229,6 +229,22 @@ export async function sendPasswordResetEmail(to: string, name: string, resetUrl:
|
|||||||
}).catch((e) => console.error("[email] sendPasswordResetEmail:", e));
|
}).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(
|
export async function sendQuestionCreatedEmail(
|
||||||
to: string,
|
to: string,
|
||||||
recipientName: string,
|
recipientName: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user