From d9fdfabf0047d5b8db185ac5c1d9f70fb0426117 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Tue, 14 Apr 2026 15:43:57 +0200 Subject: [PATCH] refactor: replace strong password hook with mild password implementation and update related components --- .../src/forms/FormChangePassword.tsx | 9 +- apps/infoalloggi/src/forms/FormNewUser.tsx | 11 +- .../src/forms/FormOverridePassword.tsx | 11 +- .../src/forms/FormRstPwFromToken.tsx | 68 ++----- apps/infoalloggi/src/hooks/usePassword.ts | 190 ++++++++++++++++++ .../src/hooks/useStrongPassword.ts | 88 -------- apps/infoalloggi/src/i18n/en.ts | 3 +- apps/infoalloggi/src/i18n/it.ts | 3 +- apps/infoalloggi/src/i18n/locales.ts | 3 +- .../src/pages/auth/accetta-invito/[token].tsx | 11 +- .../src/server/api/routers/auth.ts | 4 +- 11 files changed, 243 insertions(+), 158 deletions(-) create mode 100644 apps/infoalloggi/src/hooks/usePassword.ts delete mode 100644 apps/infoalloggi/src/hooks/useStrongPassword.ts diff --git a/apps/infoalloggi/src/forms/FormChangePassword.tsx b/apps/infoalloggi/src/forms/FormChangePassword.tsx index 357b233..1bdee1f 100644 --- a/apps/infoalloggi/src/forms/FormChangePassword.tsx +++ b/apps/infoalloggi/src/forms/FormChangePassword.tsx @@ -12,7 +12,7 @@ import { } from "~/components/custom_ui/form"; import { Button } from "~/components/ui/button"; import Input from "~/components/ui/input"; -import { useStrongPassword } from "~/hooks/useStrongPassword"; +import { useMildPassword } from "~/hooks/usePassword"; import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; import { useTranslation } from "~/providers/I18nProvider"; @@ -25,8 +25,9 @@ export const ChangePasswordForm = () => { passwordRefine, getStrengthTxt, strengthScore, + maxStrength, toggleVisibility, - } = useStrongPassword(); + } = useMildPassword(); const router = useRouter(); const ChangePasswordFormSchema = z @@ -161,7 +162,7 @@ export const ChangePasswordForm = () => {
{ "h-full transition-all duration-500 ease-out", getStrengthColor(strengthScore), )} - style={{ width: `${(strengthScore / 4) * 100}%` }} + style={{ width: `${(strengthScore / maxStrength) * 100}%` }} />
diff --git a/apps/infoalloggi/src/forms/FormNewUser.tsx b/apps/infoalloggi/src/forms/FormNewUser.tsx index 688bd2d..6b0e4ad 100644 --- a/apps/infoalloggi/src/forms/FormNewUser.tsx +++ b/apps/infoalloggi/src/forms/FormNewUser.tsx @@ -12,7 +12,7 @@ import { import { Button } from "~/components/ui/button"; import Input from "~/components/ui/input"; import { PhoneInput } from "~/components/ui/phone-input"; -import { useStrongPassword } from "~/hooks/useStrongPassword"; +import { useMildPassword } from "~/hooks/usePassword"; import { generatePassword } from "~/lib/basicPwGenerator"; import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; @@ -31,7 +31,8 @@ export const FormNewUser = ({ getStrengthTxt, strengthScore, toggleVisibility, - } = useStrongPassword(); + maxStrength, + } = useMildPassword(); const NewUserSchema = z .object({ @@ -206,7 +207,7 @@ export const FormNewUser = ({
diff --git a/apps/infoalloggi/src/forms/FormOverridePassword.tsx b/apps/infoalloggi/src/forms/FormOverridePassword.tsx index 8e9fc76..5d119ff 100644 --- a/apps/infoalloggi/src/forms/FormOverridePassword.tsx +++ b/apps/infoalloggi/src/forms/FormOverridePassword.tsx @@ -22,7 +22,7 @@ import { import Input from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Separator } from "~/components/ui/separator"; -import { useStrongPassword } from "~/hooks/useStrongPassword"; +import { useMildPassword } from "~/hooks/usePassword"; import { generatePassword } from "~/lib/basicPwGenerator"; import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; @@ -38,7 +38,8 @@ export const OverridePasswordForm = ({ id }: { id: UsersId }) => { getStrengthTxt, strengthScore, toggleVisibility, - } = useStrongPassword(); + maxStrength, + } = useMildPassword(); const OverridePasswordFormSchema = z .object({ @@ -203,7 +204,7 @@ export const OverridePasswordForm = ({ id }: { id: UsersId }) => {
{ "h-full transition-all duration-500 ease-out", getStrengthColor(strengthScore), )} - style={{ width: `${(strengthScore / 4) * 100}%` }} + style={{ + width: `${(strengthScore / maxStrength) * 100}%`, + }} />
diff --git a/apps/infoalloggi/src/forms/FormRstPwFromToken.tsx b/apps/infoalloggi/src/forms/FormRstPwFromToken.tsx index 73377f8..598cb77 100644 --- a/apps/infoalloggi/src/forms/FormRstPwFromToken.tsx +++ b/apps/infoalloggi/src/forms/FormRstPwFromToken.tsx @@ -1,6 +1,5 @@ import { Eye, EyeOff } from "lucide-react"; import { useRouter } from "next/router"; -import { useState } from "react"; import toast from "react-hot-toast"; import { z } from "zod/v4"; import { @@ -12,6 +11,7 @@ import { FormMessage, } from "~/components/custom_ui/form"; import Input from "~/components/ui/input"; +import { useMildPassword } from "~/hooks/usePassword"; import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; import { useTranslation } from "~/providers/I18nProvider"; @@ -20,38 +20,16 @@ import { api } from "~/utils/api"; export const FormRstPwFromToken = (props: { token: string }) => { const { locale, t } = useTranslation(); const router = useRouter(); - const [strengthScore, setStrengthScore] = useState(0); - const [isVisible, setIsVisible] = useState(false); - const toggleVisibility = () => setIsVisible((prevState) => !prevState); - 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 { + isVisible, + getStrengthColor, + passwordRefine, + getStrengthTxt, + strengthScore, + toggleVisibility, + maxStrength, + } = useMildPassword(); const ResetFormSchema = z .object({ @@ -59,21 +37,11 @@ export const FormRstPwFromToken = (props: { token: string }) => { repeat_password: z.string(), }) .superRefine(({ password, repeat_password }, refinementContext) => { - 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: ["password"], - }); - } - setStrengthScore(complexity.filter((req) => req.met).length); - + passwordRefine({ + password, + path: "password", + refinementContext, + }); if (password !== repeat_password) { refinementContext.issues.push({ code: "custom", @@ -164,7 +132,7 @@ export const FormRstPwFromToken = (props: { token: string }) => {
{ "h-full transition-all duration-500 ease-out", getStrengthColor(strengthScore), )} - style={{ width: `${(strengthScore / 4) * 100}%` }} + style={{ + width: `${(strengthScore / maxStrength) * 100}%`, + }} />
diff --git a/apps/infoalloggi/src/hooks/usePassword.ts b/apps/infoalloggi/src/hooks/usePassword.ts new file mode 100644 index 0000000..113415b --- /dev/null +++ b/apps/infoalloggi/src/hooks/usePassword.ts @@ -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(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(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, + }; +}; diff --git a/apps/infoalloggi/src/hooks/useStrongPassword.ts b/apps/infoalloggi/src/hooks/useStrongPassword.ts deleted file mode 100644 index 2748e73..0000000 --- a/apps/infoalloggi/src/hooks/useStrongPassword.ts +++ /dev/null @@ -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(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, - }; -}; diff --git a/apps/infoalloggi/src/i18n/en.ts b/apps/infoalloggi/src/i18n/en.ts index 7dd980a..3b5f545 100644 --- a/apps/infoalloggi/src/i18n/en.ts +++ b/apps/infoalloggi/src/i18n/en.ts @@ -369,7 +369,8 @@ export const en: LangDict = { nome: "Name", password: "Password", pw: { - lenght: "At least 8 characters", + lenght8: "At least 8 characters", + lenght6: "At least 6 characters", lowercase: "At least 1 lowercase letter", number: "At least 1 number", uppercase: "At least 1 uppercase letter", diff --git a/apps/infoalloggi/src/i18n/it.ts b/apps/infoalloggi/src/i18n/it.ts index 55c1911..c409c5c 100644 --- a/apps/infoalloggi/src/i18n/it.ts +++ b/apps/infoalloggi/src/i18n/it.ts @@ -374,7 +374,8 @@ export const it: LangDict = { nome: "Nome", password: "Password", pw: { - lenght: "Almeno 8 caratteri", + lenght8: "Almeno 8 caratteri", + lenght6: "Almeno 6 caratteri", lowercase: "Almeno 1 lettera minuscola", number: "Almeno 1 numero", uppercase: "Almeno 1 lettera maiuscola", diff --git a/apps/infoalloggi/src/i18n/locales.ts b/apps/infoalloggi/src/i18n/locales.ts index faca6a2..584c308 100644 --- a/apps/infoalloggi/src/i18n/locales.ts +++ b/apps/infoalloggi/src/i18n/locales.ts @@ -550,7 +550,8 @@ export type LangDict = { success: string; fail: string; pw: { - lenght: string; + lenght6: string; + lenght8: string; number: string; lowercase: string; uppercase: string; diff --git a/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx b/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx index 9c49bdb..41acbbd 100644 --- a/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx +++ b/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx @@ -30,7 +30,7 @@ import { } from "~/components/ui/alert-dialog"; import { Button } from "~/components/ui/button"; import Input from "~/components/ui/input"; -import { useStrongPassword } from "~/hooks/useStrongPassword"; +import { useMildPassword } from "~/hooks/usePassword"; import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; import type { NextPageWithLayout } from "~/pages/_app"; @@ -64,7 +64,8 @@ const AcceptInvitePage: NextPageWithLayout = ({ getStrengthTxt, strengthScore, toggleVisibility, - } = useStrongPassword(); + maxStrength, + } = useMildPassword(); const router = useRouter(); const schema = z @@ -189,7 +190,7 @@ const AcceptInvitePage: NextPageWithLayout = ({
= ({ "h-full transition-all duration-500 ease-out", getStrengthColor(strengthScore), )} - style={{ width: `${(strengthScore / 4) * 100}%` }} + style={{ + width: `${(strengthScore / maxStrength) * 100}%`, + }} />
diff --git a/apps/infoalloggi/src/server/api/routers/auth.ts b/apps/infoalloggi/src/server/api/routers/auth.ts index b961874..b29abe1 100644 --- a/apps/infoalloggi/src/server/api/routers/auth.ts +++ b/apps/infoalloggi/src/server/api/routers/auth.ts @@ -1,5 +1,5 @@ import { z } from "zod/v4"; -import { zStrongPassword } from "~/hooks/useStrongPassword"; +import { zMildPassword } from "~/hooks/usePassword"; import { adminProcedure, createTRPCRouter, @@ -38,7 +38,7 @@ export const authRouter = createTRPCRouter({ cognome: z.string().nonempty("Inserisci un cognome valido"), email: z.email(), nome: z.string().nonempty("Inserisci un nome valido"), - password: zStrongPassword, + password: zMildPassword, telefono: z.string().nonempty("Inserisci un numero di telefono"), }), )