From 597beada9b1c8a5d13e582476fe3fe2b0f20e4d4 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Sat, 23 May 2026 23:54:09 +0530 Subject: [PATCH] fix: align frontend username validation with backend Zod schema Fixes #413 The frontend signup form validated usernames against /^[A-Za-z\s]+$/, which only accepted plain letters and spaces. The backend Zod schema in backend/validators/authValidator.js accepts letters, digits, and underscores (/^[a-zA-Z0-9_]+$/) and enforces a minimum length of 3 and a maximum of 30. These two rulesets directly contradicted each other in two ways: 1. Valid usernames containing digits or underscores (e.g. dev_user1, john99) were blocked by the frontend with the misleading error "Only letters are allowed", even though the backend would accept them. 2. Usernames containing spaces (e.g. "john doe") passed the frontend check but were rejected by the backend, producing a confusing server-side error after the network request was already made. Additionally, the frontend had no length checks, so a one-character username would pass client-side validation but be rejected by the backend's min(3) rule. Changes in src/pages/Signup/Signup.tsx: Added named module-level constants (USERNAME_REGEX, USERNAME_MIN, USERNAME_MAX, USERNAME_ERROR) that document the backend rule and serve as a single source to update when the backend changes. The comment references the validator file explicitly to make the coupling visible to future maintainers. Updated the username validation block in handleChange and handleSubmit to: - Check min length (3 characters) with a clear error message - Check max length (30 characters) with a clear error message - Test the corrected regex /^[a-zA-Z0-9_]+$/ against the trimmed value - Use USERNAME_ERROR as the error message to match the backend exactly The handleSubmit block now trims the username once into a local variable before the ternary chain, avoiding repeated .trim() calls. No other validation logic was modified. --- src/pages/Signup/Signup.tsx | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/pages/Signup/Signup.tsx b/src/pages/Signup/Signup.tsx index 2ac51dcc..a4046420 100644 --- a/src/pages/Signup/Signup.tsx +++ b/src/pages/Signup/Signup.tsx @@ -8,6 +8,14 @@ import type { ThemeContextType } from "../../context/ThemeContext"; const backendUrl = import.meta.env.VITE_BACKEND_URL; +// Username rules -- must mirror backend/validators/authValidator.js exactly. +// Accepted characters: letters (a-z, A-Z), digits (0-9), and underscores. +// Length: minimum 3, maximum 30 characters. +const USERNAME_REGEX = /^[a-zA-Z0-9_]+$/; +const USERNAME_MIN = 3; +const USERNAME_MAX = 30; +const USERNAME_ERROR = "Username can only contain letters, numbers, and underscores"; + interface SignUpFormData { username: string; email: string; @@ -39,8 +47,12 @@ const SignUp: React.FC = () => { if (name === "username") { if (!value.trim()) { errorMessage = "Username is required"; - } else if (!/^[A-Za-z\s]+$/.test(value)) { - errorMessage = "Only letters are allowed"; + } else if (value.trim().length < USERNAME_MIN) { + errorMessage = `Username must be at least ${USERNAME_MIN} characters long`; + } else if (value.trim().length > USERNAME_MAX) { + errorMessage = `Username must be at most ${USERNAME_MAX} characters long`; + } else if (!USERNAME_REGEX.test(value.trim())) { + errorMessage = USERNAME_ERROR; } } if (name === "email") { @@ -62,10 +74,15 @@ const SignUp: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const usernameError = !formData.username.trim() + const trimmedUsername = formData.username.trim(); + const usernameError = !trimmedUsername ? "Username is required" - : !/^[A-Za-z\s]+$/.test(formData.username) - ? "Only letters are allowed" + : trimmedUsername.length < USERNAME_MIN + ? `Username must be at least ${USERNAME_MIN} characters long` + : trimmedUsername.length > USERNAME_MAX + ? `Username must be at most ${USERNAME_MAX} characters long` + : !USERNAME_REGEX.test(trimmedUsername) + ? USERNAME_ERROR : ""; const emailError = !formData.email.trim() ? "Email is required"