Files
lms-sb/src/app/api/register/route.ts
T
admins 2790a7d24b 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>
2026-06-12 14:01:46 +05:00

90 lines
2.9 KiB
TypeScript

import { NextRequest } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const disposableDomains = require("disposable-email-domains") as string[];
async function verifyTurnstile(token: string): Promise<boolean> {
try {
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET_KEY,
response: token,
}),
});
const data = await res.json() as { success: boolean };
return data.success;
} catch {
return false;
}
}
function jsonError(message: string, status: number, code?: string) {
return new Response(JSON.stringify({ message, code }), {
status,
headers: { "Content-Type": "application/json" },
});
}
export async function POST(request: NextRequest) {
let body: Record<string, string>;
try {
body = await request.json() as Record<string, string>;
} catch {
return jsonError("Неверный формат запроса", 400);
}
const { name, email, password, website, cfTurnstileResponse } = body;
// Honeypot — bots fill this field, humans don't see it
if (website) {
return jsonError("Registration failed", 400);
}
// Disposable email check
const domain = email?.split("@")[1]?.toLowerCase();
if (domain && disposableDomains.includes(domain)) {
return jsonError("Пожалуйста, используйте постоянный email-адрес", 400);
}
// Turnstile check
if (process.env.TURNSTILE_SECRET_KEY) {
if (!cfTurnstileResponse) {
return jsonError("Пожалуйста, подтвердите, что вы не робот", 400);
}
if (!(await verifyTurnstile(cfTurnstileResponse))) {
return jsonError("Проверка капчи не пройдена. Попробуйте ещё раз", 400);
}
}
// Email уже зарегистрирован? Сообщаем явно, а не молчаливым «успехом»
// от Better Auth — частый случай после миграции (у людей уже есть аккаунт).
if (email) {
const existing = await prisma.user.findFirst({
where: { email: { equals: email, mode: "insensitive" } },
select: { id: true },
});
if (existing) {
return jsonError("Аккаунт с таким email уже зарегистрирован", 409, "EMAIL_TAKEN");
}
}
const baseUrl =
process.env.BETTER_AUTH_URL ??
`https://${request.headers.get("host")}`;
return auth.handler(
new Request(`${baseUrl}/api/auth/sign-up/email`, {
method: "POST",
headers: new Headers({
"Content-Type": "application/json",
Origin: baseUrl,
}),
body: JSON.stringify({ name, email, password, callbackURL: "/dashboard" }),
})
);
}