feat(auth): integrate zod validation for login and registration
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 1m14s

This commit is contained in:
Chneemann 2026-08-23 10:37:14 +02:00
parent 5a45643331
commit 3812afab5a
No known key found for this signature in database
5 changed files with 91 additions and 71 deletions

View file

@ -1,6 +1,6 @@
/**
* @file route.ts
* @description API route handler for user registration, managing name validation, email normalization, password matching, hashing, and auto sign-in.
* @file api/auth/register/route.ts
* @description API route handler for user registration utilizing Zod for validation, bcrypt hashing, and auto sign-in.
*/
import { NextResponse } from "next/server";
@ -10,15 +10,15 @@ import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { signIn } from "@/auth";
import { AVAILABLE_COLORS } from "@/types/user";
import { registerSchema } from "@/lib/schemas/auth";
/**
* Handles POST requests for new user registration.
* Validates input fields, checks password confirmation, normalizes email, checks for existing users,
* assigns a random profile color, hashes the password, saves the user to the database, and performs an automatic sign-in.
* Handles POST requests to register a new user.
* Validates request payload via Zod, checks for email uniqueness, hashes the password, creates the user record, and attempts auto sign-in.
*
* @async
* @param {Request} request - The incoming HTTP request containing the registration payload in JSON format.
* @returns {Promise<NextResponse>} A JSON response indicating registration success or an error message with the appropriate HTTP status code.
* @param {Request} request - The incoming HTTP request containing registration data.
* @returns {Promise<NextResponse>} A JSON response indicating registration success or an error message.
*/
export async function POST(request: Request) {
try {
@ -31,61 +31,23 @@ export async function POST(request: Request) {
);
}
// 1. Validation with Zod (checks types, lengths, email format, and password matching)
const validationResult = registerSchema.safeParse(body);
if (!validationResult.success) {
const errorMessage = validationResult.error.issues[0].message;
return NextResponse.json({ message: errorMessage }, { status: 400 });
}
const {
firstName,
lastName,
email: rawEmail,
password,
confirmPassword,
} = body;
// Validation of all required fields
if (
!firstName ||
typeof firstName !== "string" ||
!lastName ||
typeof lastName !== "string" ||
!rawEmail ||
typeof rawEmail !== "string" ||
!password ||
typeof password !== "string" ||
!confirmPassword ||
typeof confirmPassword !== "string"
) {
return NextResponse.json(
{ message: "All fields are required" },
{ status: 400 },
);
}
// Check if passwords match
if (password !== confirmPassword) {
return NextResponse.json(
{ message: "Passwords do not match" },
{ status: 400 },
);
}
// Normalize email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
} = validationResult.data;
const email = rawEmail.toLowerCase().trim();
if (!emailRegex.test(email)) {
return NextResponse.json(
{ message: "Please provide a valid email address" },
{ status: 400 },
);
}
// Password length check
if (password.length < 8) {
return NextResponse.json(
{ message: "Password must be at least 8 characters long" },
{ status: 400 },
);
}
// Check existing user
// 2. Check whether the email address already exists
const existing = await db.query.usersTable.findFirst({
where: eq(usersTable.email, email),
});
@ -97,11 +59,11 @@ export async function POST(request: Request) {
);
}
// Select a random default color from the palette
// 3. Select a random profile color
const randomColor =
AVAILABLE_COLORS[Math.floor(Math.random() * AVAILABLE_COLORS.length)];
// Hash password & save user
// 4. Hash the password and store it in the database
const hashedPassword = await bcrypt.hash(password, 10);
await db.insert(usersTable).values({
firstName: firstName.trim(),
@ -111,7 +73,7 @@ export async function POST(request: Request) {
color: randomColor,
});
// Auto sign-in
// 5. Automatic Login After Successful Registration
const signInResult = await signIn("credentials", {
email,
password,

48
lib/schemas/auth.ts Normal file
View file

@ -0,0 +1,48 @@
/**
* @file lib/schemas/auth.ts
* @description Zod validation schemas for authentication forms, including login and user registration data constraints.
*/
import { z } from "zod";
/**
* Zod validation schema for user login credentials.
*/
export const loginSchema = z.object({
email: z
.string()
.max(255, "Email is too long")
.pipe(z.email("Please provide a valid email address")),
password: z
.string()
.min(1, "Password is required")
.max(72, "Password is too long"),
});
/**
* Zod validation schema for new user registration payloads, including password confirmation matching.
*/
export const registerSchema = z
.object({
firstName: z
.string()
.min(1, "First name is required")
.max(50, "First name is too long"),
lastName: z
.string()
.min(1, "Last name is required")
.max(50, "Last name is too long"),
email: z
.string()
.max(255, "Email is too long")
.pipe(z.email("Please provide a valid email address")),
password: z
.string()
.min(8, "Password must be at least 8 characters long")
.max(72, "Password is too long (max 72 characters)"),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});

4
package-lock.json generated
View file

@ -16,7 +16,8 @@
"postgres": "^3.4.9",
"react": "19.2.4",
"react-dom": "19.2.4",
"swr": "^2.5.0"
"swr": "^2.5.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -8595,7 +8596,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View file

@ -17,7 +17,8 @@
"postgres": "^3.4.9",
"react": "19.2.4",
"react-dom": "19.2.4",
"swr": "^2.5.0"
"swr": "^2.5.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

View file

@ -3,6 +3,7 @@
* @description Authentication service providing helper functions for guest credentials, sign-in, and registration.
*/
import { loginSchema, registerSchema } from "@/lib/schemas/auth";
import { signIn } from "next-auth/react";
/**
@ -23,18 +24,24 @@ export async function getGuestCredentials() {
}
/**
* Authenticates a user using credentials via NextAuth.
* Authenticates a user using email and password credentials, performing client-side validation first.
*
* @async
* @param {string} email - The user's email address.
* @param {string} password - The user's account password.
* @returns {Promise<{ success?: boolean; error?: string }>} An object indicating success or describing the authentication error.
* @returns {Promise<{ success?: boolean; error?: string }>} An object indicating sign-in success or an error message.
*/
export async function loginUser(email: string, password: string) {
const validationResult = loginSchema.safeParse({ email, password });
if (!validationResult.success) {
return { error: validationResult.error.issues[0].message };
}
try {
const res = await signIn("credentials", {
email,
password,
email: validationResult.data.email,
password: validationResult.data.password,
redirect: false,
});
@ -59,22 +66,24 @@ export async function loginUser(email: string, password: string) {
}
/**
* Registers a new user account via the registration API endpoint.
* Registers a new user account by validating the form data and submitting it to the backend registration route.
*
* @async
* @param {Record<string, any>} payload - The user registration form payload containing user details and passwords.
* @returns {Promise<{ success?: boolean; error?: string }>} An object indicating success or describing the registration error.
* @param {Record<string, any>} payload - The user registration form payload containing credentials and user details.
* @returns {Promise<{ success?: boolean; error?: string }>} An object indicating successful registration or an error message.
*/
export async function registerUser(payload: Record<string, any>) {
if (payload.password !== payload.confirmPassword) {
return { error: "Passwords do not match" };
const validationResult = registerSchema.safeParse(payload);
if (!validationResult.success) {
return { error: validationResult.error.issues[0].message };
}
try {
const response = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
body: JSON.stringify(validationResult.data),
});
const contentType = response.headers.get("content-type");