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 { db } from "~/server/db";
|
||||||
import { accounts, users } from "~/server/db/schema";
|
import { accounts, users } from "~/server/db/schema";
|
||||||
|
|
||||||
const registerSchema = z.object({
|
const registerSchema = z
|
||||||
firstName: z.string().trim().min(1, "First name is required"),
|
.object({
|
||||||
lastName: z.string().trim().min(1, "Last name is required"),
|
firstName: z.string().trim().optional(),
|
||||||
|
lastName: z.string().trim().optional(),
|
||||||
|
name: z.string().trim().optional(),
|
||||||
email: z.string().email("Invalid email address"),
|
email: z.string().email("Invalid email address"),
|
||||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
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> = {
|
const fieldLabels: Record<string, string> = {
|
||||||
firstName: "First name",
|
firstName: "First name",
|
||||||
lastName: "Last name",
|
lastName: "Last name",
|
||||||
|
name: "Name",
|
||||||
email: "Email address",
|
email: "Email address",
|
||||||
password: "Password",
|
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) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
if (env.DISABLE_SIGNUPS === true) {
|
if (env.DISABLE_SIGNUPS === true) {
|
||||||
@@ -29,11 +88,34 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = (await request.json()) as unknown;
|
let body: unknown;
|
||||||
const { firstName, lastName, email, password } = registerSchema.parse(body);
|
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();
|
const normalizedEmail = email.toLowerCase();
|
||||||
|
|
||||||
// Check if user already exists
|
|
||||||
const existingUser = await db.query.users.findFirst({
|
const existingUser = await db.query.users.findFirst({
|
||||||
where: eq(users.email, normalizedEmail),
|
where: eq(users.email, normalizedEmail),
|
||||||
});
|
});
|
||||||
@@ -45,7 +127,6 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
@@ -75,20 +156,6 @@ export async function POST(request: NextRequest) {
|
|||||||
{ status: 201 },
|
{ status: 201 },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} 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);
|
console.error("Registration error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Label } from "~/components/ui/label";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Logo } from "~/components/branding/logo";
|
import { Logo } from "~/components/branding/logo";
|
||||||
import { LegalAgreementNotice } from "~/components/legal/legal-links";
|
import { LegalAgreementNotice } from "~/components/legal/legal-links";
|
||||||
|
import { authClient } from "~/lib/auth-client";
|
||||||
import { Mail, Lock, ArrowRight, User } from "lucide-react";
|
import { Mail, Lock, ArrowRight, User } from "lucide-react";
|
||||||
|
|
||||||
function RegisterForm() {
|
function RegisterForm() {
|
||||||
@@ -22,23 +23,39 @@ function RegisterForm() {
|
|||||||
|
|
||||||
async function handleRegister(e: React.FormEvent) {
|
async function handleRegister(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
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);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await fetch("/api/auth/register", {
|
const { error } = await authClient.signUp.email({
|
||||||
method: "POST",
|
email: trimmedEmail,
|
||||||
headers: { "Content-Type": "application/json" },
|
password,
|
||||||
body: JSON.stringify({ firstName, lastName, email, password }),
|
name: `${trimmedFirstName} ${trimmedLastName}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (res.ok) {
|
if (error) {
|
||||||
toast.success("Account created successfully! Please sign in.");
|
toast.error(error.message ?? "Registration failed");
|
||||||
router.push("/auth/signin");
|
return;
|
||||||
} else {
|
|
||||||
const data = (await res.json()) as { error?: string };
|
|
||||||
toast.error(data.error ?? "Registration failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.success("Account created successfully!");
|
||||||
|
router.push("/dashboard");
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const authentikOrigin =
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL,
|
||||||
|
secret: process.env.AUTH_SECRET,
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema: {
|
schema: {
|
||||||
|
|||||||
Reference in New Issue
Block a user