Make registration errors informative
- Backend: explicitly return 409 EMAIL_TAKEN when the email already exists, instead of Better Auth's silent 200 (common after migration). - Frontend: split error handling — network failure, non-JSON body (502/proxy), and meaningful server errors get distinct messages; the "email already registered" case shows login / reset-password links instead of a generic "connection error". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of
|
|||||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||||
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
|
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [emailTaken, setEmailTaken] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
@@ -84,6 +85,13 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of
|
|||||||
{ url: offerUrl, label: "Договор-оферту" },
|
{ url: offerUrl, label: "Договор-оферту" },
|
||||||
].filter((l) => l.url);
|
].filter((l) => l.url);
|
||||||
|
|
||||||
|
function resetTurnstile() {
|
||||||
|
if (turnstileWidgetId.current) {
|
||||||
|
window.turnstile?.reset(turnstileWidgetId.current);
|
||||||
|
setTurnstileToken(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (showTermsCheckbox && !termsAccepted) {
|
if (showTermsCheckbox && !termsAccepted) {
|
||||||
@@ -95,10 +103,13 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError("");
|
setError("");
|
||||||
|
setEmailTaken(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
// 1. Сетевой слой: настоящий обрыв связи — отдельно от ошибок сервера
|
||||||
|
let res: Response;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/register", {
|
res = await fetch("/api/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -109,24 +120,40 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of
|
|||||||
cfTurnstileResponse: turnstileToken,
|
cfTurnstileResponse: turnstileToken,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
const data = await res.json() as { message?: string };
|
setError("Не удалось связаться с сервером. Проверьте интернет-соединение и попробуйте ещё раз.");
|
||||||
|
setLoading(false);
|
||||||
if (!res.ok) {
|
return;
|
||||||
setError(data.message ?? "Ошибка регистрации");
|
|
||||||
if (turnstileWidgetId.current) {
|
|
||||||
window.turnstile?.reset(turnstileWidgetId.current);
|
|
||||||
setTurnstileToken(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
} catch {
|
|
||||||
setError("Ошибка соединения. Попробуйте ещё раз.");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +266,19 @@ export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, of
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{emailTaken && (
|
||||||
|
<div className="text-xs py-3 px-3 space-y-2" style={{ border: "2px solid oklch(0.577 0.245 27.325)" }}>
|
||||||
|
<p style={{ color: "oklch(0.577 0.245 27.325)" }}>
|
||||||
|
Аккаунт с таким email уже зарегистрирован.
|
||||||
|
</p>
|
||||||
|
<p style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
<Link href="/login" className="underline" style={{ color: "var(--foreground)" }}>Войти</Link>
|
||||||
|
{" · "}
|
||||||
|
<Link href="/forgot-password" className="underline" style={{ color: "var(--foreground)" }}>Забыли пароль?</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && !emailTaken && (
|
||||||
<p className="text-xs py-2 px-3" style={{ border: "2px solid oklch(0.577 0.245 27.325)", color: "oklch(0.577 0.245 27.325)" }}>
|
<p className="text-xs py-2 px-3" style={{ border: "2px solid oklch(0.577 0.245 27.325)", color: "oklch(0.577 0.245 27.325)" }}>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const disposableDomains = require("disposable-email-domains") as string[];
|
const disposableDomains = require("disposable-email-domains") as string[];
|
||||||
@@ -21,8 +22,8 @@ async function verifyTurnstile(token: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsonError(message: string, status: number) {
|
function jsonError(message: string, status: number, code?: string) {
|
||||||
return new Response(JSON.stringify({ message }), {
|
return new Response(JSON.stringify({ message, code }), {
|
||||||
status,
|
status,
|
||||||
headers: { "Content-Type": "application/json" },
|
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 =
|
const baseUrl =
|
||||||
process.env.BETTER_AUTH_URL ??
|
process.env.BETTER_AUTH_URL ??
|
||||||
`https://${request.headers.get("host")}`;
|
`https://${request.headers.get("host")}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user