diff --git a/server/src/routes/auth.routes.ts b/server/src/routes/auth.routes.ts index 7165601..f03cd84 100644 --- a/server/src/routes/auth.routes.ts +++ b/server/src/routes/auth.routes.ts @@ -14,7 +14,12 @@ const roleSchema = z.preprocess( const registerSchema = z.object({ email: z.string().trim().email(), - password: z.string().min(6), + password: z + .string() + .min(8, 'Password must be at least 8 characters') + .regex(/[A-Z]/, 'Password must contain at least 1 uppercase letter') + .regex(/[0-9]/, 'Password must contain at least 1 number') + .regex(/[!@#$%^&*]/, 'Password must contain at least 1 special character (!@#$%^&*)'), displayName: z.string().trim().min(1).max(100).optional(), name: z.string().trim().min(1).max(100).optional(), role: roleSchema, diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 3218252..9d835b4 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -1,6 +1,7 @@ "use client"; import { FormEvent, useEffect, useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; @@ -17,6 +18,7 @@ export default function LoginPage() { const router = useRouter(); const [email, setEmail] = useState("eleanor@writerspub.com"); const [password, setPassword] = useState("password123"); + const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); @@ -61,14 +63,25 @@ export default function LoginPage() { className="w-full px-4 py-3 rounded-xl bg-[#4a5033]/5 border border-[#4a5033]/10" placeholder="Email" /> - setPassword(e.target.value)} - required - className="w-full px-4 py-3 rounded-xl bg-[#4a5033]/5 border border-[#4a5033]/10" - placeholder="Password" - /> +
+ setPassword(e.target.value)} + required + className="w-full px-4 py-3 pr-12 rounded-xl bg-[#4a5033]/5 border border-[#4a5033]/10" + placeholder="Password" + /> + + +
{error ?

{error}

: null} {loading ? "Signing in..." : "Sign In"} diff --git a/web/src/app/signup/page.tsx b/web/src/app/signup/page.tsx index d8ad846..38c33ca 100644 --- a/web/src/app/signup/page.tsx +++ b/web/src/app/signup/page.tsx @@ -15,6 +15,49 @@ interface RegisterResponse { tokens: { accessToken: string; refreshToken: string }; } +interface PasswordRule { + label: string; + message: string; + test: (value: string) => boolean; +} + +const passwordRules: PasswordRule[] = [ + { + label: "8+ characters", + message: "Password must be at least 8 characters", + test: (value) => value.length >= 8, + }, + { + label: "Uppercase letter", + message: "Password must contain at least 1 uppercase letter", + test: (value) => /[A-Z]/.test(value), + }, + { + label: "Number", + message: "Password must contain at least 1 number", + test: (value) => /[0-9]/.test(value), + }, + { + label: "Special character", + message: "Password must contain at least 1 special character (!@#$%^&*)", + test: (value) => /[!@#$%^&*]/.test(value), + }, +]; + +function getPasswordStrength(password: string) { + const passedRules = passwordRules.filter((rule) => rule.test(password)).length; + + if (passedRules <= 1) { + return { label: "Weak", barClass: "bg-rose-500", textClass: "text-rose-600" }; + } + + if (passedRules <= 3) { + return { label: "Medium", barClass: "bg-amber-500", textClass: "text-amber-600" }; + } + + return { label: "Strong", barClass: "bg-emerald-500", textClass: "text-emerald-600" }; +} + export default function SignupPage() { const router = useRouter(); const [displayName, setDisplayName] = useState(""); @@ -23,9 +66,22 @@ export default function SignupPage() { const [role, setRole] = useState("writer"); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); + const [passwordTouched, setPasswordTouched] = useState(false); + + const passwordStrength = getPasswordStrength(password); + const passwordErrors = passwordRules + .filter((rule) => !rule.test(password)) + .map((rule) => rule.message); + + const showPasswordFeedback = passwordTouched || password.length > 0; const onSubmit = async (e: FormEvent) => { e.preventDefault(); + setPasswordTouched(true); + if (passwordErrors.length > 0) { + setError(passwordErrors[0]); + return; + } setLoading(true); setError(""); try { @@ -76,12 +132,48 @@ export default function SignupPage() { setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + setPasswordTouched(true); + setError(""); + }} + onBlur={() => setPasswordTouched(true)} required - minLength={6} + minLength={8} className="w-full px-4 py-3 rounded-xl bg-[#4a5033]/5 border border-[#4a5033]/10" placeholder="Password" /> + {showPasswordFeedback ? ( +
+
+ Password strength + {passwordStrength.label} +
+
+
+
+
    + {passwordRules.map((rule) => { + const isValid = rule.test(password); + return ( +
  • + {isValid ? "✓" : "•"} {rule.message} +
  • + ); + })} +
+
+ ) : null}