refactor: replace strong password hook with mild password implementation and update related components

This commit is contained in:
Marco Pedone 2026-04-14 15:43:57 +02:00
parent e0353bb6f9
commit d9fdfabf00
11 changed files with 243 additions and 158 deletions

View file

@ -12,7 +12,7 @@ import {
} from "~/components/custom_ui/form"; } from "~/components/custom_ui/form";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
import { useStrongPassword } from "~/hooks/useStrongPassword"; import { useMildPassword } from "~/hooks/usePassword";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm"; import { useZodForm } from "~/lib/zodForm";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
@ -25,8 +25,9 @@ export const ChangePasswordForm = () => {
passwordRefine, passwordRefine,
getStrengthTxt, getStrengthTxt,
strengthScore, strengthScore,
maxStrength,
toggleVisibility, toggleVisibility,
} = useStrongPassword(); } = useMildPassword();
const router = useRouter(); const router = useRouter();
const ChangePasswordFormSchema = z const ChangePasswordFormSchema = z
@ -161,7 +162,7 @@ export const ChangePasswordForm = () => {
</button> </button>
<div <div
aria-label="Password strength" aria-label="Password strength"
aria-valuemax={4} aria-valuemax={maxStrength}
aria-valuemin={0} aria-valuemin={0}
aria-valuenow={strengthScore} aria-valuenow={strengthScore}
className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border" className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border"
@ -172,7 +173,7 @@ export const ChangePasswordForm = () => {
"h-full transition-all duration-500 ease-out", "h-full transition-all duration-500 ease-out",
getStrengthColor(strengthScore), getStrengthColor(strengthScore),
)} )}
style={{ width: `${(strengthScore / 4) * 100}%` }} style={{ width: `${(strengthScore / maxStrength) * 100}%` }}
/> />
</div> </div>
</div> </div>

View file

@ -12,7 +12,7 @@ import {
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
import { PhoneInput } from "~/components/ui/phone-input"; import { PhoneInput } from "~/components/ui/phone-input";
import { useStrongPassword } from "~/hooks/useStrongPassword"; import { useMildPassword } from "~/hooks/usePassword";
import { generatePassword } from "~/lib/basicPwGenerator"; import { generatePassword } from "~/lib/basicPwGenerator";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm"; import { useZodForm } from "~/lib/zodForm";
@ -31,7 +31,8 @@ export const FormNewUser = ({
getStrengthTxt, getStrengthTxt,
strengthScore, strengthScore,
toggleVisibility, toggleVisibility,
} = useStrongPassword(); maxStrength,
} = useMildPassword();
const NewUserSchema = z const NewUserSchema = z
.object({ .object({
@ -206,7 +207,7 @@ export const FormNewUser = ({
</button> </button>
<div <div
aria-label="Password strength" aria-label="Password strength"
aria-valuemax={4} aria-valuemax={maxStrength}
aria-valuemin={0} aria-valuemin={0}
aria-valuenow={strengthScore} aria-valuenow={strengthScore}
className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border" className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border"
@ -217,7 +218,9 @@ export const FormNewUser = ({
"h-full transition-all duration-500 ease-out", "h-full transition-all duration-500 ease-out",
getStrengthColor(strengthScore), getStrengthColor(strengthScore),
)} )}
style={{ width: `${(strengthScore / 4) * 100}%` }} style={{
width: `${(strengthScore / maxStrength) * 100}%`,
}}
/> />
</div> </div>
</div> </div>

View file

@ -22,7 +22,7 @@ import {
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Separator } from "~/components/ui/separator"; import { Separator } from "~/components/ui/separator";
import { useStrongPassword } from "~/hooks/useStrongPassword"; import { useMildPassword } from "~/hooks/usePassword";
import { generatePassword } from "~/lib/basicPwGenerator"; import { generatePassword } from "~/lib/basicPwGenerator";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm"; import { useZodForm } from "~/lib/zodForm";
@ -38,7 +38,8 @@ export const OverridePasswordForm = ({ id }: { id: UsersId }) => {
getStrengthTxt, getStrengthTxt,
strengthScore, strengthScore,
toggleVisibility, toggleVisibility,
} = useStrongPassword(); maxStrength,
} = useMildPassword();
const OverridePasswordFormSchema = z const OverridePasswordFormSchema = z
.object({ .object({
@ -203,7 +204,7 @@ export const OverridePasswordForm = ({ id }: { id: UsersId }) => {
</button> </button>
<div <div
aria-label="Password strength" aria-label="Password strength"
aria-valuemax={4} aria-valuemax={maxStrength}
aria-valuemin={0} aria-valuemin={0}
aria-valuenow={strengthScore} aria-valuenow={strengthScore}
className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border" className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border"
@ -214,7 +215,9 @@ export const OverridePasswordForm = ({ id }: { id: UsersId }) => {
"h-full transition-all duration-500 ease-out", "h-full transition-all duration-500 ease-out",
getStrengthColor(strengthScore), getStrengthColor(strengthScore),
)} )}
style={{ width: `${(strengthScore / 4) * 100}%` }} style={{
width: `${(strengthScore / maxStrength) * 100}%`,
}}
/> />
</div> </div>
</div> </div>

View file

@ -1,6 +1,5 @@
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { import {
@ -12,6 +11,7 @@ import {
FormMessage, FormMessage,
} from "~/components/custom_ui/form"; } from "~/components/custom_ui/form";
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
import { useMildPassword } from "~/hooks/usePassword";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm"; import { useZodForm } from "~/lib/zodForm";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
@ -20,38 +20,16 @@ import { api } from "~/utils/api";
export const FormRstPwFromToken = (props: { token: string }) => { export const FormRstPwFromToken = (props: { token: string }) => {
const { locale, t } = useTranslation(); const { locale, t } = useTranslation();
const router = useRouter(); const router = useRouter();
const [strengthScore, setStrengthScore] = useState(0);
const [isVisible, setIsVisible] = useState<boolean>(false);
const toggleVisibility = () => setIsVisible((prevState) => !prevState);
const checkStrength = (pass: string) => { const {
const requirements = [ isVisible,
{ regex: /.{8,}/, text: t.auth.signup.pw.lenght }, getStrengthColor,
{ regex: /[0-9]/, text: t.auth.signup.pw.number }, passwordRefine,
{ regex: /[a-z]/, text: t.auth.signup.pw.lowercase }, getStrengthTxt,
{ regex: /[A-Z]/, text: t.auth.signup.pw.uppercase }, strengthScore,
]; toggleVisibility,
maxStrength,
return requirements.map((req) => ({ } = useMildPassword();
met: req.regex.test(pass),
text: req.text,
}));
};
const getStrengthColor = (score: number) => {
if (score === 0) return "bg-border";
if (score <= 1) return "bg-red-500";
if (score <= 2) return "bg-orange-500";
if (score === 3) return "bg-amber-500";
return "bg-emerald-500";
};
const getStrengthTxt = (score: number) => {
if (score === 0) return "text-border";
if (score <= 1) return "text-red-500";
if (score <= 2) return "text-orange-500";
if (score === 3) return "text-amber-500";
return "text-emerald-500";
};
const ResetFormSchema = z const ResetFormSchema = z
.object({ .object({
@ -59,21 +37,11 @@ export const FormRstPwFromToken = (props: { token: string }) => {
repeat_password: z.string(), repeat_password: z.string(),
}) })
.superRefine(({ password, repeat_password }, refinementContext) => { .superRefine(({ password, repeat_password }, refinementContext) => {
const complexity = checkStrength(password); passwordRefine({
password,
if (complexity.filter((req) => req.met).length !== complexity.length) { path: "password",
refinementContext.issues.push({ refinementContext,
code: "custom", });
input: "",
message: complexity
.filter((req) => !req.met)
.map((req) => req.text)
.join("\n"),
path: ["password"],
});
}
setStrengthScore(complexity.filter((req) => req.met).length);
if (password !== repeat_password) { if (password !== repeat_password) {
refinementContext.issues.push({ refinementContext.issues.push({
code: "custom", code: "custom",
@ -164,7 +132,7 @@ export const FormRstPwFromToken = (props: { token: string }) => {
</button> </button>
<div <div
aria-label="Password strength" aria-label="Password strength"
aria-valuemax={4} aria-valuemax={maxStrength}
aria-valuemin={0} aria-valuemin={0}
aria-valuenow={strengthScore} aria-valuenow={strengthScore}
className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border" className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border"
@ -175,7 +143,9 @@ export const FormRstPwFromToken = (props: { token: string }) => {
"h-full transition-all duration-500 ease-out", "h-full transition-all duration-500 ease-out",
getStrengthColor(strengthScore), getStrengthColor(strengthScore),
)} )}
style={{ width: `${(strengthScore / 4) * 100}%` }} style={{
width: `${(strengthScore / maxStrength) * 100}%`,
}}
/> />
</div> </div>
</div> </div>

View file

@ -0,0 +1,190 @@
import { useState } from "react";
import { type RefinementCtx, z } from "zod/v4";
import { useTranslation } from "~/providers/I18nProvider";
export const zStrongPassword = z.string().superRefine((password, ctx) => {
const test = [/.{8,}/, /[0-9]/, /[a-z]/, /[A-Z]/].every((regex) =>
regex.test(password),
);
if (!test) {
ctx.addIssue({
code: "custom",
message:
"La password deve contenere almeno 8 caratteri, un numero, una lettera minuscola e una maiuscola.",
});
}
});
type PasswordManager = {
getStrengthColor: (score: number) => string;
getStrengthTxt: (score: number) => string;
isVisible: boolean;
passwordRefine: ({
password,
refinementContext,
path,
}: {
password: string;
refinementContext: RefinementCtx;
path: string;
}) => void;
strengthScore: number;
toggleVisibility: () => void;
maxStrength: number;
};
export const useStrongPassword = (): PasswordManager => {
const { t } = useTranslation();
const [isVisible, setIsVisible] = useState<boolean>(false);
const [strengthScore, setStrengthScore] = useState(0);
const checkStrength = (pass: string) => {
const requirements = [
{ regex: /.{8,}/, text: t.auth.signup.pw.lenght8 },
{ regex: /[0-9]/, text: t.auth.signup.pw.number },
{ regex: /[a-z]/, text: t.auth.signup.pw.lowercase },
{ regex: /[A-Z]/, text: t.auth.signup.pw.uppercase },
];
return requirements.map((req) => ({
met: req.regex.test(pass),
text: req.text,
}));
};
const getStrengthColor = (score: number) => {
if (score === 0) return "bg-border";
if (score <= 1) return "bg-red-500";
if (score <= 2) return "bg-orange-500";
if (score === 3) return "bg-amber-500";
return "bg-emerald-500";
};
const getStrengthTxt = (score: number) => {
if (score === 0) return "text-border";
if (score <= 1) return "text-red-500";
if (score <= 2) return "text-orange-500";
if (score === 3) return "text-amber-500";
return "text-emerald-500";
};
const toggleVisibility = () => setIsVisible((prevState) => !prevState);
const passwordRefine = ({
password,
refinementContext,
path,
}: {
password: string;
refinementContext: RefinementCtx;
path: string;
}) => {
const complexity = checkStrength(password);
if (complexity.filter((req) => req.met).length !== complexity.length) {
refinementContext.issues.push({
code: "custom",
input: "",
message: complexity
.filter((req) => !req.met)
.map((req) => req.text)
.join("\n"),
path: [path],
});
}
setStrengthScore(complexity.filter((req) => req.met).length);
};
return {
getStrengthColor,
getStrengthTxt,
isVisible,
passwordRefine,
strengthScore,
toggleVisibility,
maxStrength: 4,
};
};
export const zMildPassword = z.string().superRefine((password, ctx) => {
const test = [/.{6,}/, /[a-z]/, /[A-Z]/].every((regex) =>
regex.test(password),
);
if (!test) {
ctx.addIssue({
code: "custom",
message:
"La password deve contenere almeno 6 caratteri, una lettera minuscola e una maiuscola.",
});
}
});
export const useMildPassword = (): PasswordManager => {
const { t } = useTranslation();
const [isVisible, setIsVisible] = useState<boolean>(false);
const [strengthScore, setStrengthScore] = useState(0);
const checkStrength = (pass: string) => {
const requirements = [
{ regex: /.{6,}/, text: t.auth.signup.pw.lenght6 },
{ regex: /[a-z]/, text: t.auth.signup.pw.lowercase },
{ regex: /[A-Z]/, text: t.auth.signup.pw.uppercase },
];
return requirements.map((req) => ({
met: req.regex.test(pass),
text: req.text,
}));
};
const getStrengthColor = (score: number) => {
if (score === 0) return "bg-border";
if (score <= 1) return "bg-red-500";
if (score === 2) return "bg-amber-500";
return "bg-emerald-500";
};
const getStrengthTxt = (score: number) => {
if (score === 0) return "text-border";
if (score <= 1) return "text-red-500";
if (score === 2) return "text-amber-500";
return "text-emerald-500";
};
const toggleVisibility = () => setIsVisible((prevState) => !prevState);
const passwordRefine = ({
password,
refinementContext,
path,
}: {
password: string;
refinementContext: RefinementCtx;
path: string;
}) => {
const complexity = checkStrength(password);
if (complexity.filter((req) => req.met).length !== complexity.length) {
refinementContext.issues.push({
code: "custom",
input: "",
message: complexity
.filter((req) => !req.met)
.map((req) => req.text)
.join("\n"),
path: [path],
});
}
setStrengthScore(complexity.filter((req) => req.met).length);
};
return {
getStrengthColor,
getStrengthTxt,
isVisible,
passwordRefine,
strengthScore,
toggleVisibility,
maxStrength: 3,
};
};

View file

@ -1,88 +0,0 @@
import { useState } from "react";
import { type RefinementCtx, z } from "zod/v4";
import { useTranslation } from "~/providers/I18nProvider";
export const zStrongPassword = z.string().superRefine((password, ctx) => {
const test = [/.{8,}/, /[0-9]/, /[a-z]/, /[A-Z]/].every((regex) =>
regex.test(password),
);
if (!test) {
ctx.addIssue({
code: "custom",
message:
"La password deve contenere almeno 8 caratteri, un numero, una lettera minuscola e una maiuscola.",
});
}
});
export const useStrongPassword = () => {
const { t } = useTranslation();
const [isVisible, setIsVisible] = useState<boolean>(false);
const [strengthScore, setStrengthScore] = useState(0);
const checkStrength = (pass: string) => {
const requirements = [
{ regex: /.{8,}/, text: t.auth.signup.pw.lenght },
{ regex: /[0-9]/, text: t.auth.signup.pw.number },
{ regex: /[a-z]/, text: t.auth.signup.pw.lowercase },
{ regex: /[A-Z]/, text: t.auth.signup.pw.uppercase },
];
return requirements.map((req) => ({
met: req.regex.test(pass),
text: req.text,
}));
};
const getStrengthColor = (score: number) => {
if (score === 0) return "bg-border";
if (score <= 1) return "bg-red-500";
if (score <= 2) return "bg-orange-500";
if (score === 3) return "bg-amber-500";
return "bg-emerald-500";
};
const getStrengthTxt = (score: number) => {
if (score === 0) return "text-border";
if (score <= 1) return "text-red-500";
if (score <= 2) return "text-orange-500";
if (score === 3) return "text-amber-500";
return "text-emerald-500";
};
const toggleVisibility = () => setIsVisible((prevState) => !prevState);
const passwordRefine = ({
password,
refinementContext,
path,
}: {
password: string;
refinementContext: RefinementCtx;
path: string;
}) => {
const complexity = checkStrength(password);
if (complexity.filter((req) => req.met).length !== complexity.length) {
refinementContext.issues.push({
code: "custom",
input: "",
message: complexity
.filter((req) => !req.met)
.map((req) => req.text)
.join("\n"),
path: [path],
});
}
setStrengthScore(complexity.filter((req) => req.met).length);
};
return {
getStrengthColor,
getStrengthTxt,
isVisible,
passwordRefine,
strengthScore,
toggleVisibility,
};
};

View file

@ -369,7 +369,8 @@ export const en: LangDict = {
nome: "Name", nome: "Name",
password: "Password", password: "Password",
pw: { pw: {
lenght: "At least 8 characters", lenght8: "At least 8 characters",
lenght6: "At least 6 characters",
lowercase: "At least 1 lowercase letter", lowercase: "At least 1 lowercase letter",
number: "At least 1 number", number: "At least 1 number",
uppercase: "At least 1 uppercase letter", uppercase: "At least 1 uppercase letter",

View file

@ -374,7 +374,8 @@ export const it: LangDict = {
nome: "Nome", nome: "Nome",
password: "Password", password: "Password",
pw: { pw: {
lenght: "Almeno 8 caratteri", lenght8: "Almeno 8 caratteri",
lenght6: "Almeno 6 caratteri",
lowercase: "Almeno 1 lettera minuscola", lowercase: "Almeno 1 lettera minuscola",
number: "Almeno 1 numero", number: "Almeno 1 numero",
uppercase: "Almeno 1 lettera maiuscola", uppercase: "Almeno 1 lettera maiuscola",

View file

@ -550,7 +550,8 @@ export type LangDict = {
success: string; success: string;
fail: string; fail: string;
pw: { pw: {
lenght: string; lenght6: string;
lenght8: string;
number: string; number: string;
lowercase: string; lowercase: string;
uppercase: string; uppercase: string;

View file

@ -30,7 +30,7 @@ import {
} from "~/components/ui/alert-dialog"; } from "~/components/ui/alert-dialog";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
import { useStrongPassword } from "~/hooks/useStrongPassword"; import { useMildPassword } from "~/hooks/usePassword";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm"; import { useZodForm } from "~/lib/zodForm";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
@ -64,7 +64,8 @@ const AcceptInvitePage: NextPageWithLayout<InviteTokenProps> = ({
getStrengthTxt, getStrengthTxt,
strengthScore, strengthScore,
toggleVisibility, toggleVisibility,
} = useStrongPassword(); maxStrength,
} = useMildPassword();
const router = useRouter(); const router = useRouter();
const schema = z const schema = z
@ -189,7 +190,7 @@ const AcceptInvitePage: NextPageWithLayout<InviteTokenProps> = ({
</button> </button>
<div <div
aria-label="Password strength" aria-label="Password strength"
aria-valuemax={4} aria-valuemax={maxStrength}
aria-valuemin={0} aria-valuemin={0}
aria-valuenow={strengthScore} aria-valuenow={strengthScore}
className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border" className="mt-3 mb-4 h-1 w-full overflow-hidden rounded-full bg-border"
@ -200,7 +201,9 @@ const AcceptInvitePage: NextPageWithLayout<InviteTokenProps> = ({
"h-full transition-all duration-500 ease-out", "h-full transition-all duration-500 ease-out",
getStrengthColor(strengthScore), getStrengthColor(strengthScore),
)} )}
style={{ width: `${(strengthScore / 4) * 100}%` }} style={{
width: `${(strengthScore / maxStrength) * 100}%`,
}}
/> />
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { zStrongPassword } from "~/hooks/useStrongPassword"; import { zMildPassword } from "~/hooks/usePassword";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
@ -38,7 +38,7 @@ export const authRouter = createTRPCRouter({
cognome: z.string().nonempty("Inserisci un cognome valido"), cognome: z.string().nonempty("Inserisci un cognome valido"),
email: z.email(), email: z.email(),
nome: z.string().nonempty("Inserisci un nome valido"), nome: z.string().nonempty("Inserisci un nome valido"),
password: zStrongPassword, password: zMildPassword,
telefono: z.string().nonempty("Inserisci un numero di telefono"), telefono: z.string().nonempty("Inserisci un numero di telefono"),
}), }),
) )