From 8b426743c2c329915c0565415889245402856dbe Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:47:43 -0600 Subject: [PATCH] add password requirements to new user dialog --- .../components/overlay/CreateUserDialog.tsx | 223 ++++++++++++++++-- .../components/overlay/SetPasswordDialog.tsx | 53 ++--- web/src/utils/passwordUtil.ts | 41 ++++ 3 files changed, 263 insertions(+), 54 deletions(-) create mode 100644 web/src/utils/passwordUtil.ts diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx index 6f2b3ecf3..c11865973 100644 --- a/web/src/components/overlay/CreateUserDialog.tsx +++ b/web/src/components/overlay/CreateUserDialog.tsx @@ -32,11 +32,17 @@ import { SelectValue, } from "../ui/select"; import { Shield, User } from "lucide-react"; -import { LuCheck, LuX } from "react-icons/lu"; +import { LuCheck, LuX, LuEye, LuEyeOff } from "react-icons/lu"; import { useTranslation } from "react-i18next"; import { isDesktop, isMobile } from "react-device-detect"; import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; +import { + calculatePasswordStrength, + getPasswordRequirements, + getPasswordStrengthLabel, + getPasswordStrengthColor, +} from "@/utils/passwordUtil"; import { MobilePage, MobilePageContent, @@ -59,6 +65,10 @@ export default function CreateUserDialog({ const { data: config } = useSWR("config"); const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); + const [showPasswordVisible, setShowPasswordVisible] = + useState(false); + const [showConfirmPassword, setShowConfirmPassword] = + useState(false); const roles = useMemo(() => { const existingRoles = config ? Object.keys(config.auth?.roles || {}) : []; @@ -73,7 +83,15 @@ export default function CreateUserDialog({ .regex(/^[A-Za-z0-9._]+$/, { message: t("users.dialog.createUser.usernameOnlyInclude"), }), - password: z.string().min(1, t("users.dialog.form.passwordIsRequired")), + password: z + .string() + .min(8, t("users.dialog.form.password.requirements.length")) + .regex(/[A-Z]/, t("users.dialog.form.password.requirements.uppercase")) + .regex(/\d/, t("users.dialog.form.password.requirements.digit")) + .regex( + /[!@#$%^&*(),.?":{}|<>]/, + t("users.dialog.form.password.requirements.special"), + ), confirmPassword: z .string() .min(1, t("users.dialog.createUser.confirmPassword")), @@ -108,13 +126,27 @@ export default function CreateUserDialog({ const passwordsMatch = password === confirmPassword; const showMatchIndicator = password && confirmPassword; + // Password strength calculation + const passwordStrength = useMemo( + () => calculatePasswordStrength(password), + [password], + ); + + const requirements = useMemo( + () => getPasswordRequirements(password), + [password], + ); + useEffect(() => { if (!show) { form.reset({ user: "", password: "", + confirmPassword: "", role: "viewer", }); + setShowPasswordVisible(false); + setShowConfirmPassword(false); } }, [show, form]); @@ -122,8 +154,11 @@ export default function CreateUserDialog({ form.reset({ user: "", password: "", + confirmPassword: "", role: "viewer", }); + setShowPasswordVisible(false); + setShowConfirmPassword(false); onCancel(); }; @@ -184,13 +219,142 @@ export default function CreateUserDialog({ {t("users.dialog.form.password.title")} - +
+ + +
+ + {password && ( +
+
+
+
+

+ {t("users.dialog.form.password.strength.title")} + + {getPasswordStrengthLabel(password, t)} + +

+ +
+

+ {t("users.dialog.form.password.requirements.title")} +

+
    +
  • + {requirements.length ? ( + + ) : ( + + )} + + {t( + "users.dialog.form.password.requirements.length", + )} + +
  • +
  • + {requirements.uppercase ? ( + + ) : ( + + )} + + {t( + "users.dialog.form.password.requirements.uppercase", + )} + +
  • +
  • + {requirements.digit ? ( + + ) : ( + + )} + + {t( + "users.dialog.form.password.requirements.digit", + )} + +
  • +
  • + {requirements.special ? ( + + ) : ( + + )} + + {t( + "users.dialog.form.password.requirements.special", + )} + +
  • +
+
+
+ )} + )} @@ -204,14 +368,41 @@ export default function CreateUserDialog({ {t("users.dialog.form.password.confirm.title")} - +
+ + +
{showMatchIndicator && (
diff --git a/web/src/components/overlay/SetPasswordDialog.tsx b/web/src/components/overlay/SetPasswordDialog.tsx index 7708201aa..25fb99ad8 100644 --- a/web/src/components/overlay/SetPasswordDialog.tsx +++ b/web/src/components/overlay/SetPasswordDialog.tsx @@ -28,6 +28,12 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { useIsAdmin } from "@/hooks/use-is-admin"; +import { + calculatePasswordStrength, + getPasswordRequirements, + getPasswordStrengthLabel, + getPasswordStrengthColor, +} from "@/utils/passwordUtil"; type SetPasswordProps = { show: boolean; @@ -125,25 +131,13 @@ export default function SetPasswordDialog({ const confirmPassword = form.watch("confirmPassword"); // Password strength calculation - const passwordStrength = useMemo(() => { - if (!password) return 0; - - let strength = 0; - if (password.length >= 8) strength += 1; - if (/\d/.test(password)) strength += 1; - if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 1; - if (/[A-Z]/.test(password)) strength += 1; - - return strength; - }, [password]); + const passwordStrength = useMemo( + () => calculatePasswordStrength(password), + [password], + ); const requirements = useMemo( - () => ({ - length: password?.length >= 8, - uppercase: /[A-Z]/.test(password || ""), - digit: /\d/.test(password || ""), - special: /[!@#$%^&*(),.?":{}|<>]/.test(password || ""), - }), + () => getPasswordRequirements(password), [password], ); @@ -196,25 +190,6 @@ export default function SetPasswordDialog({ onSave(values.password, oldPassword); }; - const getStrengthLabel = () => { - if (!password) return ""; - if (passwordStrength <= 1) - return t("users.dialog.form.password.strength.weak"); - if (passwordStrength === 2) - return t("users.dialog.form.password.strength.medium"); - if (passwordStrength === 3) - return t("users.dialog.form.password.strength.strong"); - return t("users.dialog.form.password.strength.veryStrong"); - }; - - const getStrengthColor = () => { - if (!password) return "bg-gray-200"; - if (passwordStrength <= 1) return "bg-red-500"; - if (passwordStrength === 2) return "bg-yellow-500"; - if (passwordStrength === 3) return "bg-green-500"; - return "bg-green-600"; - }; - return ( @@ -367,14 +342,16 @@ export default function SetPasswordDialog({

{t("users.dialog.form.password.strength.title")} - {getStrengthLabel()} + {getPasswordStrengthLabel(password, t)}

diff --git a/web/src/utils/passwordUtil.ts b/web/src/utils/passwordUtil.ts new file mode 100644 index 000000000..8ece16003 --- /dev/null +++ b/web/src/utils/passwordUtil.ts @@ -0,0 +1,41 @@ +export const calculatePasswordStrength = (password: string): number => { + if (!password) return 0; + + let strength = 0; + if (password.length >= 8) strength += 1; + if (/\d/.test(password)) strength += 1; + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 1; + if (/[A-Z]/.test(password)) strength += 1; + + return strength; +}; + +export const getPasswordRequirements = (password: string) => ({ + length: password?.length >= 8, + uppercase: /[A-Z]/.test(password || ""), + digit: /\d/.test(password || ""), + special: /[!@#$%^&*(),.?":{}|<>]/.test(password || ""), +}); + +export const getPasswordStrengthLabel = ( + password: string, + t: (key: string) => string, +): string => { + const strength = calculatePasswordStrength(password); + + if (!password) return ""; + if (strength <= 1) return t("users.dialog.form.password.strength.weak"); + if (strength === 2) return t("users.dialog.form.password.strength.medium"); + if (strength === 3) return t("users.dialog.form.password.strength.strong"); + return t("users.dialog.form.password.strength.veryStrong"); +}; + +export const getPasswordStrengthColor = (password: string): string => { + const strength = calculatePasswordStrength(password); + + if (!password) return "bg-gray-200"; + if (strength <= 1) return "bg-red-500"; + if (strength === 2) return "bg-yellow-500"; + if (strength === 3) return "bg-green-500"; + return "bg-green-600"; +};