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:
2026-06-26 00:00:29 -04:00
co-authored by Cursor
parent 463b1f503e
commit af1f4a5721
3 changed files with 120 additions and 34 deletions
+89 -22
View File
@@ -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" },
+27 -10
View File
@@ -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 (
+2
View File
@@ -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: {