diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 0d872c3..8953fe2 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -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"), - email: z.string().email("Invalid email address"), - password: z.string().min(8, "Password must be at least 8 characters"), -}); +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 = { 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" }, diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index 3b7f436..76876e9 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -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 ( diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 761cf6b..fb34f13 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -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: {