From ad36141d61a3103c0d0f8b47a3b59d2af8004e95 Mon Sep 17 00:00:00 2001 From: AA1-34-Ganesh Date: Mon, 5 Jan 2026 19:39:10 +0530 Subject: [PATCH 1/5] Replace auth password field with reusable PasswordInput component --- client/src/components/auth/AuthForm.tsx | 22 ++--- client/src/components/ui/PasswordInput.tsx | 108 +++++++++++++++++++++ 2 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 client/src/components/ui/PasswordInput.tsx diff --git a/client/src/components/auth/AuthForm.tsx b/client/src/components/auth/AuthForm.tsx index 9e91cfa..8e0326a 100644 --- a/client/src/components/auth/AuthForm.tsx +++ b/client/src/components/auth/AuthForm.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { User, Lock, UserPlus, LogIn, Mail, Shield } from "lucide-react"; import { login, signup, sendOTP } from "../../utils/api"; - +import PasswordInput from "../ui/PasswordInput"; interface AuthFormProps { onAuthChange: () => void; } @@ -210,20 +210,12 @@ const AuthForm: React.FC = ({ onAuthChange }) => { -
- - setPassword(e.target.value)} - className="alien-input w-full pl-10" - placeholder="Enter your password" - required - /> -
+ setPassword(e.target.value)} + className="alien-input" // This keeps your project's green/black theme + placeholder="Enter your password" + /> {error && ( diff --git a/client/src/components/ui/PasswordInput.tsx b/client/src/components/ui/PasswordInput.tsx new file mode 100644 index 0000000..5971ad0 --- /dev/null +++ b/client/src/components/ui/PasswordInput.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { Eye, EyeOff, KeyRound } from 'lucide-react'; + +interface PasswordInputProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; + name?: string; + id?: string; + className?: string; +} + +const PasswordInput: React.FC = ({ + value, + onChange, + placeholder = "Enter password", + name, + id, + className = "" +}) => { + const [showPassword, setShowPassword] = useState(false); + const [strength, setStrength] = useState(0); + + const calculateStrength = (password: string) => { + let score = 0; + if (!password) return 0; + if (password.length > 8) score++; + if (/[A-Z]/.test(password)) score++; + if (/[0-9]/.test(password)) score++; + if (/[^A-Za-z0-9]/.test(password)) score++; + return score; + }; + + const handleInputChange = (e: React.ChangeEvent) => { + onChange(e); + setStrength(calculateStrength(e.target.value)); + }; + + const generatePassword = () => { + const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; + let generated = ""; + for (let i = 0; i < 12; i++) { + generated += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + const event = { + target: { name, value: generated } + } as React.ChangeEvent; + + handleInputChange(event); + }; + + const getStrengthColor = (score: number) => { + if (score < 2) return 'bg-red-500'; + if (score < 3) return 'bg-yellow-500'; + return 'bg-green-500'; + }; + + const getStrengthLabel = (score: number) => { + const labels = ['Weak', 'Fair', 'Good', 'Strong', 'Excellent']; + return labels[score] || 'Weak'; + }; + + return ( +
+
+ + +
+ +
+ {value ? ( +
+
+ + {getStrengthLabel(strength)} + +
+ ) :
} + + +
+
+ ); +}; + +export default PasswordInput; \ No newline at end of file From 5e5e8d67d132e2cff8c11f8e244774d86e9f6baf Mon Sep 17 00:00:00 2001 From: AA1-34-Ganesh Date: Mon, 5 Jan 2026 19:53:03 +0530 Subject: [PATCH 2/5] Added the comments, because explicitly mentioned in PR template --- client/src/components/ui/PasswordInput.tsx | 48 +++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/client/src/components/ui/PasswordInput.tsx b/client/src/components/ui/PasswordInput.tsx index 5971ad0..ce2b19d 100644 --- a/client/src/components/ui/PasswordInput.tsx +++ b/client/src/components/ui/PasswordInput.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import { Eye, EyeOff, KeyRound } from 'lucide-react'; +// Define the shape of props this component expects. +// This ensures TypeScript warns us if we forget to pass required data. interface PasswordInputProps { value: string; onChange: (e: React.ChangeEvent) => void; @@ -10,6 +12,10 @@ interface PasswordInputProps { className?: string; } +/** + * Reusable Password Input Component + * Features: Visibility toggle, Strength meter, and Random password generator. + */ const PasswordInput: React.FC = ({ value, onChange, @@ -18,24 +24,42 @@ const PasswordInput: React.FC = ({ id, className = "" }) => { + // State to toggle between "text" (visible) and "password" (masked) types const [showPassword, setShowPassword] = useState(false); + // State to track password strength score (0 to 4) const [strength, setStrength] = useState(0); + /** + * Calculates a simple heuristic score for password strength. + * Score ranges from 0 (Weak) to 4 (Strong). + */ const calculateStrength = (password: string) => { let score = 0; if (!password) return 0; + + // Criteria 1: Length check (longer than 8 chars) if (password.length > 8) score++; + // Criteria 2: Contains at least one uppercase letter if (/[A-Z]/.test(password)) score++; + // Criteria 3: Contains at least one number if (/[0-9]/.test(password)) score++; + // Criteria 4: Contains special characters (symbols) if (/[^A-Za-z0-9]/.test(password)) score++; + return score; }; + // Wrapper function to handle input changes. + // We need this to update the local strength meter alongside the parent's state. const handleInputChange = (e: React.ChangeEvent) => { - onChange(e); - setStrength(calculateStrength(e.target.value)); + onChange(e); // Propagate change to parent + setStrength(calculateStrength(e.target.value)); // Update local strength }; + /** + * Generates a secure random 12-character password. + * Creates a synthetic event to update the parent form state smoothly. + */ const generatePassword = () => { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; let generated = ""; @@ -43,6 +67,8 @@ const PasswordInput: React.FC = ({ generated += chars.charAt(Math.floor(Math.random() * chars.length)); } + // Create a mock event object to mimic a real user typing. + // This allows us to reuse the existing `handleInputChange` function. const event = { target: { name, value: generated } } as React.ChangeEvent; @@ -50,12 +76,14 @@ const PasswordInput: React.FC = ({ handleInputChange(event); }; + // Helper to determine the color of the strength bar based on score const getStrengthColor = (score: number) => { - if (score < 2) return 'bg-red-500'; - if (score < 3) return 'bg-yellow-500'; - return 'bg-green-500'; + if (score < 2) return 'bg-red-500'; // Weak + if (score < 3) return 'bg-yellow-500'; // Fair/Good + return 'bg-green-500'; // Strong }; + // Helper to get the text label for the strength score const getStrengthLabel = (score: number) => { const labels = ['Weak', 'Fair', 'Good', 'Strong', 'Excellent']; return labels[score] || 'Weak'; @@ -63,6 +91,7 @@ const PasswordInput: React.FC = ({ return (
+ {/* Input Field Container */}
= ({ value={value} onChange={handleInputChange} placeholder={placeholder} + // Combine base styles with any custom className passed via props className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${className}`} /> + + {/* Visibility Toggle Button (Eye Icon) */}
+ {/* Strength Meter & Generator Actions */}
+ {/* Only show strength meter if the user has typed something */} {value ? (
+ {/* Visual Progress Bar */}
{getStrengthLabel(strength)}
- ) :
} + ) :
} {/* Empty div to maintain spacing when hidden */} + {/* Suggest Password Button */}
- {error && (
{error} From 20a57aeee10f873ea0b33599640702ec6c80a657 Mon Sep 17 00:00:00 2001 From: AA1-34-Ganesh Date: Mon, 5 Jan 2026 20:03:08 +0530 Subject: [PATCH 4/5] Deleted the extra comments --- client/src/components/ui/PasswordInput.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/client/src/components/ui/PasswordInput.tsx b/client/src/components/ui/PasswordInput.tsx index ce2b19d..4104a4d 100644 --- a/client/src/components/ui/PasswordInput.tsx +++ b/client/src/components/ui/PasswordInput.tsx @@ -14,7 +14,7 @@ interface PasswordInputProps { /** * Reusable Password Input Component - * Features: Visibility toggle, Strength meter, and Random password generator. + * */ const PasswordInput: React.FC = ({ value, @@ -29,10 +29,7 @@ const PasswordInput: React.FC = ({ // State to track password strength score (0 to 4) const [strength, setStrength] = useState(0); - /** - * Calculates a simple heuristic score for password strength. - * Score ranges from 0 (Weak) to 4 (Strong). - */ + const calculateStrength = (password: string) => { let score = 0; if (!password) return 0; @@ -49,16 +46,16 @@ const PasswordInput: React.FC = ({ return score; }; - // Wrapper function to handle input changes. + // We need this to update the local strength meter alongside the parent's state. const handleInputChange = (e: React.ChangeEvent) => { - onChange(e); // Propagate change to parent - setStrength(calculateStrength(e.target.value)); // Update local strength + onChange(e); + setStrength(calculateStrength(e.target.value)); }; /** * Generates a secure random 12-character password. - * Creates a synthetic event to update the parent form state smoothly. + * */ const generatePassword = () => { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; @@ -67,7 +64,7 @@ const PasswordInput: React.FC = ({ generated += chars.charAt(Math.floor(Math.random() * chars.length)); } - // Create a mock event object to mimic a real user typing. + // This allows us to reuse the existing `handleInputChange` function. const event = { target: { name, value: generated } @@ -91,7 +88,6 @@ const PasswordInput: React.FC = ({ return (
- {/* Input Field Container */}
Date: Wed, 7 Jan 2026 21:31:26 +0530 Subject: [PATCH 5/5] align frontend validation with backend Zod schema and fix generator --- client/package-lock.json | 12 +- client/package.json | 3 +- client/src/components/ui/PasswordInput.tsx | 124 ++++++++++++--------- client/src/schemas/auth.schema.ts | 61 ++++++++++ 4 files changed, 148 insertions(+), 52 deletions(-) create mode 100644 client/src/schemas/auth.schema.ts diff --git a/client/package-lock.json b/client/package-lock.json index 43f9372..3caa722 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -32,7 +32,8 @@ "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "socket.io-client": "^4.8.1" + "socket.io-client": "^4.8.1", + "zod": "^4.3.5" }, "devDependencies": { "@eslint/js": "^9.33.0", @@ -6659,6 +6660,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/client/package.json b/client/package.json index e90ea5b..561e523 100644 --- a/client/package.json +++ b/client/package.json @@ -35,7 +35,8 @@ "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "socket.io-client": "^4.8.1" + "socket.io-client": "^4.8.1", + "zod": "^4.3.5" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/client/src/components/ui/PasswordInput.tsx b/client/src/components/ui/PasswordInput.tsx index 4104a4d..14aacd1 100644 --- a/client/src/components/ui/PasswordInput.tsx +++ b/client/src/components/ui/PasswordInput.tsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Eye, EyeOff, KeyRound } from 'lucide-react'; -// Define the shape of props this component expects. -// This ensures TypeScript warns us if we forget to pass required data. +import { passwordSchema } from '../../schemas/auth.schema'; + interface PasswordInputProps { value: string; onChange: (e: React.ChangeEvent) => void; @@ -12,10 +12,7 @@ interface PasswordInputProps { className?: string; } -/** - * Reusable Password Input Component - * - */ + const PasswordInput: React.FC = ({ value, onChange, @@ -24,48 +21,68 @@ const PasswordInput: React.FC = ({ id, className = "" }) => { - // State to toggle between "text" (visible) and "password" (masked) types const [showPassword, setShowPassword] = useState(false); - // State to track password strength score (0 to 4) const [strength, setStrength] = useState(0); + const [errors, setErrors] = useState([]); // New state for Zod errors - - const calculateStrength = (password: string) => { + + const calculateVisualStrength = (password: string) => { let score = 0; if (!password) return 0; - - // Criteria 1: Length check (longer than 8 chars) if (password.length > 8) score++; - // Criteria 2: Contains at least one uppercase letter if (/[A-Z]/.test(password)) score++; - // Criteria 3: Contains at least one number if (/[0-9]/.test(password)) score++; - // Criteria 4: Contains special characters (symbols) if (/[^A-Za-z0-9]/.test(password)) score++; - return score; }; - - // We need this to update the local strength meter alongside the parent's state. + + useEffect(() => { + // Run the text against the shared schema + const result = passwordSchema.safeParse(value); + + if (!result.success) { + // If invalid, extract the specific error messages + const errorMessages = result.error.issues.map((issue) => issue.message); + setErrors(errorMessages); + } else { + // If valid, clear errors + setErrors([]); + } + + // Update the visual strength meter + setStrength(calculateVisualStrength(value)); + }, [value]); + const handleInputChange = (e: React.ChangeEvent) => { - onChange(e); - setStrength(calculateStrength(e.target.value)); + onChange(e); + // Validation is handled by the useEffect above }; - /** - * Generates a secure random 12-character password. - * - */ const generatePassword = () => { - const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; + // 1. Define sets + const lower = "abcdefghijklmnopqrstuvwxyz"; + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const nums = "0123456789"; + const special = "!@#$%^&*"; + const allChars = lower + upper + nums + special; + let generated = ""; - for (let i = 0; i < 12; i++) { - generated += chars.charAt(Math.floor(Math.random() * chars.length)); + + // 2. GUARANTEE one of each required type + generated += lower[Math.floor(Math.random() * lower.length)]; + generated += upper[Math.floor(Math.random() * upper.length)]; + generated += nums[Math.floor(Math.random() * nums.length)]; + generated += special[Math.floor(Math.random() * special.length)]; + + // 3. Fill the rest (aiming for length 12-14) + for (let i = generated.length; i < 14; i++) { + generated += allChars[Math.floor(Math.random() * allChars.length)]; } - - // This allows us to reuse the existing `handleInputChange` function. + // 4. Shuffle the result so the pattern isn't predictable + generated = generated.split('').sort(() => 0.5 - Math.random()).join(''); + const event = { target: { name, value: generated } } as React.ChangeEvent; @@ -73,14 +90,12 @@ const PasswordInput: React.FC = ({ handleInputChange(event); }; - // Helper to determine the color of the strength bar based on score const getStrengthColor = (score: number) => { - if (score < 2) return 'bg-red-500'; // Weak - if (score < 3) return 'bg-yellow-500'; // Fair/Good - return 'bg-green-500'; // Strong + if (score < 2) return 'bg-red-500'; + if (score < 3) return 'bg-yellow-500'; + return 'bg-green-500'; }; - // Helper to get the text label for the strength score const getStrengthLabel = (score: number) => { const labels = ['Weak', 'Fair', 'Good', 'Strong', 'Excellent']; return labels[score] || 'Weak'; @@ -96,11 +111,12 @@ const PasswordInput: React.FC = ({ value={value} onChange={handleInputChange} placeholder={placeholder} - // Combine base styles with any custom className passed via props - className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${className}`} + // Add red border if there are validation errors + className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${ + errors.length > 0 && value.length > 0 ? 'border-red-500 focus:ring-red-500' : 'border-gray-300' + } ${className}`} /> - {/* Visibility Toggle Button (Eye Icon) */}
- {/* Strength Meter & Generator Actions */} -
- {/* Only show strength meter if the user has typed something */} +
{value ? ( -
- {/* Visual Progress Bar */} -
- - {getStrengthLabel(strength)} - +
+ {/* Visual Bar */} +
+
+ + {getStrengthLabel(strength)} + +
+ + {/* Zod Error Messages Displayed Here */} + {errors.length > 0 && ( +
    + {errors.slice(0, 3).map((err, idx) => ( // Limit to showing first 3 errors to save space +
  • {err}
  • + ))} +
+ )}
- ) :
} {/* Empty div to maintain spacing when hidden */} + ) :
} - {/* Suggest Password Button */}