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}