Fix signup by using better-auth signUp and clearer register errors.
Route web registration through authClient.signUp.email, set explicit better-auth baseURL/secret for Docker, and harden the mobile register API with safeParse and readable validation messages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,20 +6,79 @@ import { env } from "~/env";
|
||||
import { db } from "~/server/db";
|
||||
import { accounts, users } from "~/server/db/schema";
|
||||
|
||||
const registerSchema = z.object({
|
||||
firstName: z.string().trim().min(1, "First name is required"),
|
||||
lastName: z.string().trim().min(1, "Last name is required"),
|
||||
const registerSchema = z
|
||||
.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
name: z.string().trim().optional(),
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const hasSplitName =
|
||||
Boolean(data.firstName?.length) && Boolean(data.lastName?.length);
|
||||
const hasFullName = Boolean(data.name?.length);
|
||||
|
||||
if (!hasSplitName && !hasFullName) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "First and last name are required",
|
||||
path: ["firstName"],
|
||||
});
|
||||
}
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.name?.length) {
|
||||
const parts = data.name.trim().split(/\s+/);
|
||||
const firstName = parts[0] ?? "";
|
||||
const lastName = parts.slice(1).join(" ") || firstName;
|
||||
return {
|
||||
firstName,
|
||||
lastName,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
firstName: data.firstName!.trim(),
|
||||
lastName: data.lastName!.trim(),
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
};
|
||||
});
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
firstName: "First name",
|
||||
lastName: "Last name",
|
||||
name: "Name",
|
||||
email: "Email address",
|
||||
password: "Password",
|
||||
};
|
||||
|
||||
function formatRegisterError(error: z.ZodError): string {
|
||||
const issue = error.issues[0] ?? error.errors[0];
|
||||
if (!issue) return "Please check the registration form";
|
||||
|
||||
const field = issue.path[0];
|
||||
const label =
|
||||
typeof field === "string" ? (fieldLabels[field] ?? field) : "Field";
|
||||
|
||||
if (
|
||||
issue.code === "invalid_type" &&
|
||||
"received" in issue &&
|
||||
issue.received === "undefined"
|
||||
) {
|
||||
return `${label} is required`;
|
||||
}
|
||||
|
||||
if (issue.message && issue.message !== "Required") {
|
||||
return issue.message;
|
||||
}
|
||||
|
||||
return `${label} is required`;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (env.DISABLE_SIGNUPS === true) {
|
||||
@@ -29,11 +88,34 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const body = (await request.json()) as unknown;
|
||||
const { firstName, lastName, email, password } = registerSchema.parse(body);
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid request body. Please try again." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!body || typeof body !== "object") {
|
||||
return NextResponse.json(
|
||||
{ error: "Registration details are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = registerSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: formatRegisterError(parsed.error) },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const { firstName, lastName, email, password } = parsed.data;
|
||||
const normalizedEmail = email.toLowerCase();
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.email, normalizedEmail),
|
||||
});
|
||||
@@ -45,7 +127,6 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -75,20 +156,6 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const issue = error.errors[0];
|
||||
const field = issue?.path[0];
|
||||
const fallback =
|
||||
typeof field === "string"
|
||||
? `${fieldLabels[field] ?? field} is required`
|
||||
: "Please check the registration form";
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: issue?.message === "Required" ? fallback : issue?.message },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
console.error("Registration error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Label } from "~/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { Logo } from "~/components/branding/logo";
|
||||
import { LegalAgreementNotice } from "~/components/legal/legal-links";
|
||||
import { authClient } from "~/lib/auth-client";
|
||||
import { Mail, Lock, ArrowRight, User } from "lucide-react";
|
||||
|
||||
function RegisterForm() {
|
||||
@@ -22,23 +23,39 @@ function RegisterForm() {
|
||||
|
||||
async function handleRegister(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const trimmedFirstName = firstName.trim();
|
||||
const trimmedLastName = lastName.trim();
|
||||
const trimmedEmail = email.trim();
|
||||
|
||||
if (!trimmedFirstName || !trimmedLastName || !trimmedEmail) {
|
||||
toast.error("Please enter your first name, last name, and email.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
toast.error("Password must be at least 8 characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ firstName, lastName, email, password }),
|
||||
const { error } = await authClient.signUp.email({
|
||||
email: trimmedEmail,
|
||||
password,
|
||||
name: `${trimmedFirstName} ${trimmedLastName}`,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (res.ok) {
|
||||
toast.success("Account created successfully! Please sign in.");
|
||||
router.push("/auth/signin");
|
||||
} else {
|
||||
const data = (await res.json()) as { error?: string };
|
||||
toast.error(data.error ?? "Registration failed");
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Registration failed");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Account created successfully!");
|
||||
router.push("/dashboard");
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,8 @@ const authentikOrigin =
|
||||
: null;
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: process.env.BETTER_AUTH_URL,
|
||||
secret: process.env.AUTH_SECRET,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: {
|
||||
|
||||
Reference in New Issue
Block a user