From 95887332785a44ab076aab1120444a3c09da55a3 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 26 Apr 2026 01:12:03 +0800 Subject: [PATCH] feat: add auth form validation schemas and password strength utility --- lib/auth/password-strength.ts | 93 +++++++++++++++++++++++++++++++++++ lib/auth/validation.ts | 49 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 lib/auth/password-strength.ts create mode 100644 lib/auth/validation.ts diff --git a/lib/auth/password-strength.ts b/lib/auth/password-strength.ts new file mode 100644 index 0000000..598fb61 --- /dev/null +++ b/lib/auth/password-strength.ts @@ -0,0 +1,93 @@ +export type StrengthLevel = "weak" | "fair" | "good" | "strong" | "excellent" + +export interface PasswordStrength { + score: number + level: StrengthLevel + feedback: string[] +} + +const COMMON_PATTERNS = [ + "password", + "123456", + "12345678", + "qwerty", + "abc123", + "monkey", + "master", + "dragon", + "login", + "admin", + "letmein", + "welcome", + "shadow", + "sunshine", + "trustno1", + "iloveyou", +] + +export function checkPasswordStrength(password: string): PasswordStrength { + if (!password) { + return { score: 0, level: "weak", feedback: ["Enter a password"] } + } + + let score = 0 + const feedback: string[] = [] + + // Length scoring + if (password.length >= 10) score += 20 + else feedback.push("Use at least 10 characters") + + if (password.length >= 14) score += 10 + else if (password.length >= 10) feedback.push("Use 14+ characters for extra security") + + if (password.length >= 18) score += 10 + + // Character variety + const hasUpper = /[A-Z]/.test(password) + const hasLower = /[a-z]/.test(password) + const hasNumber = /[0-9]/.test(password) + const hasSpecial = /[^A-Za-z0-9]/.test(password) + + if (hasUpper) score += 15 + else feedback.push("Add an uppercase letter") + + if (hasLower) score += 15 + else feedback.push("Add a lowercase letter") + + if (hasNumber) score += 15 + else feedback.push("Add a number") + + if (hasSpecial) score += 15 + else feedback.push("Add a special character (!@#$%^&*)") + + // Common pattern check + const lower = password.toLowerCase() + const isCommon = COMMON_PATTERNS.some((p) => lower.includes(p)) + if (!isCommon) score += 10 + else feedback.push("Avoid common passwords") + + // Repeated characters + const hasRepeated = /(.)\1{2,}/.test(password) + if (!hasRepeated) score += 5 + else feedback.push("Avoid repeated characters") + + // Mixed positions (not all numbers at end, not all caps at start) + const endsWithNumbers = /[0-9]+$/.test(password) && !/[0-9]/.test(password.slice(0, -3)) + const startsWithCaps = /^[A-Z]{3,}/.test(password) && !/[A-Z]/.test(password.slice(3)) + if (!endsWithNumbers && !startsWithCaps) score += 5 + + // Determine level + let level: StrengthLevel + if (score <= 20) level = "weak" + else if (score <= 40) level = "fair" + else if (score <= 60) level = "good" + else if (score <= 80) level = "strong" + else level = "excellent" + + // If all checks pass, clear feedback + if (feedback.length === 0) { + feedback.push("Great password!") + } + + return { score, level, feedback } +} diff --git a/lib/auth/validation.ts b/lib/auth/validation.ts new file mode 100644 index 0000000..f173e37 --- /dev/null +++ b/lib/auth/validation.ts @@ -0,0 +1,49 @@ +import { z } from "zod" + +export const loginEmailSchema = z.object({ + email: z.string().email("Please enter a valid email address"), +}) + +export const loginSchema = z.object({ + email: z.string().email("Please enter a valid email address"), + password: z.string().min(1, "Password is required"), +}) + +export const signupSchema = z.object({ + name: z + .string() + .min(1, "Name is required") + .max(100, "Name must be 100 characters or less"), + email: z.string().email("Please enter a valid email address"), + password: z + .string() + .min(10, "Password must be at least 10 characters"), +}) + +export const otpSchema = z.object({ + otp: z.string().length(6, "OTP must be exactly 6 digits"), +}) + +export const forgotPasswordSchema = z.object({ + email: z.string().email("Please enter a valid email address"), +}) + +export const resetPasswordSchema = z + .object({ + otp: z.string().length(6, "OTP must be exactly 6 digits"), + newPassword: z + .string() + .min(10, "Password must be at least 10 characters"), + confirmPassword: z.string().min(1, "Please confirm your password"), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: "Passwords do not match", + path: ["confirmPassword"], + }) + +export type LoginEmailInput = z.infer +export type LoginInput = z.infer +export type SignupInput = z.infer +export type OtpInput = z.infer +export type ForgotPasswordInput = z.infer +export type ResetPasswordInput = z.infer