diff --git a/src/app/(auth)/register/register-form.tsx b/src/app/(auth)/register/register-form.tsx index 648c8b5..f6c5b38 100644 --- a/src/app/(auth)/register/register-form.tsx +++ b/src/app/(auth)/register/register-form.tsx @@ -35,6 +35,7 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of const [termsAccepted, setTermsAccepted] = useState(false); const [turnstileToken, setTurnstileToken] = useState(null); const [error, setError] = useState(""); + const [emailTaken, setEmailTaken] = useState(false); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); @@ -84,6 +85,13 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of { url: offerUrl, label: "Договор-оферту" }, ].filter((l) => l.url); + function resetTurnstile() { + if (turnstileWidgetId.current) { + window.turnstile?.reset(turnstileWidgetId.current); + setTurnstileToken(null); + } + } + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (showTermsCheckbox && !termsAccepted) { @@ -95,10 +103,13 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of return; } setError(""); + setEmailTaken(false); setLoading(true); + // 1. Сетевой слой: настоящий обрыв связи — отдельно от ошибок сервера + let res: Response; try { - const res = await fetch("/api/register", { + res = await fetch("/api/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -109,24 +120,40 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of cfTurnstileResponse: turnstileToken, }), }); - - const data = await res.json() as { message?: string }; - - if (!res.ok) { - setError(data.message ?? "Ошибка регистрации"); - if (turnstileWidgetId.current) { - window.turnstile?.reset(turnstileWidgetId.current); - setTurnstileToken(null); - } - setLoading(false); - return; - } - - setSuccess(true); } catch { - setError("Ошибка соединения. Попробуйте ещё раз."); + setError("Не удалось связаться с сервером. Проверьте интернет-соединение и попробуйте ещё раз."); + setLoading(false); + return; } + // 2. Тело может быть не-JSON (502 при передеплое, HTML от прокси) + let data: { message?: string; code?: string } = {}; + try { + data = await res.json() as { message?: string; code?: string }; + } catch { + setError( + res.ok + ? "Сервер вернул неожиданный ответ. Попробуйте ещё раз." + : "Сервис временно недоступен. Попробуйте через минуту." + ); + resetTurnstile(); + setLoading(false); + return; + } + + // 3. Осмысленные ошибки сервера + if (!res.ok) { + if (res.status === 409 || data.code === "EMAIL_TAKEN") { + setEmailTaken(true); + } else { + setError(data.message ?? "Не удалось зарегистрироваться. Попробуйте ещё раз."); + } + resetTurnstile(); + setLoading(false); + return; + } + + setSuccess(true); setLoading(false); } @@ -239,7 +266,19 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of )} - {error && ( + {emailTaken && ( +
+

+ Аккаунт с таким email уже зарегистрирован. +

+

+ Войти + {" · "} + Забыли пароль? +

+
+ )} + {error && !emailTaken && (

{error}

diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index 1640946..5d79477 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -1,5 +1,6 @@ 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[]; @@ -21,8 +22,8 @@ async function verifyTurnstile(token: string): Promise { } } -function jsonError(message: string, status: number) { - return new Response(JSON.stringify({ message }), { +function jsonError(message: string, status: number, code?: string) { + return new Response(JSON.stringify({ message, code }), { status, headers: { "Content-Type": "application/json" }, }); @@ -59,6 +60,18 @@ export async function POST(request: NextRequest) { } } + // 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")}`;