feat(auth): integrate zod validation for login and registration
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 1m14s
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 1m14s
This commit is contained in:
parent
5a45643331
commit
3812afab5a
5 changed files with 91 additions and 71 deletions
|
|
@ -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
48
lib/schemas/auth.ts
Normal 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
4
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue