remove password strength meter and fix confirm password feedback

The strength meter still scored against the old 8 character and character class rules, so any password with a digit, like test1234, showed a full green "Very Strong" bar next to the failing 12 character requirement. The meter is gone and the length checklist is the only feedback. The password field's form message duplicated the checklist, and editing the password never revalidated the confirm field, so a fixed mismatch kept showing "Passwords do not match". Editing the password now revalidates the confirm field once it has a value, and both dialogs show a live match/mismatch indicator in place of the confirm form message.
This commit is contained in:
Josh Hawkins
2026-11-01 18:35:36 -06:00
parent b92ad23116
commit 8fed6bfa42
4 changed files with 33 additions and 116 deletions
@@ -1086,13 +1086,6 @@
"title": "Confirm Password", "title": "Confirm Password",
"placeholder": "Confirm Password" "placeholder": "Confirm Password"
}, },
"strength": {
"title": "Password strength: ",
"weak": "Weak",
"medium": "Medium",
"strong": "Strong",
"veryStrong": "Very Strong"
},
"requirements": { "requirements": {
"title": "Password requirements:", "title": "Password requirements:",
"length": "At least 12 characters" "length": "At least 12 characters"
@@ -37,12 +37,7 @@ import { useTranslation } from "react-i18next";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { import { getPasswordRequirements } from "@/utils/passwordUtil";
calculatePasswordStrength,
getPasswordRequirements,
getPasswordStrengthLabel,
getPasswordStrengthColor,
} from "@/utils/passwordUtil";
import { import {
MobilePage, MobilePage,
MobilePageContent, MobilePageContent,
@@ -123,12 +118,6 @@ export default function CreateUserDialog({
const passwordsMatch = password === confirmPassword; const passwordsMatch = password === confirmPassword;
const showMatchIndicator = password && confirmPassword; const showMatchIndicator = password && confirmPassword;
// Password strength calculation
const passwordStrength = useMemo(
() => calculatePasswordStrength(password),
[password],
);
const requirements = useMemo( const requirements = useMemo(
() => getPasswordRequirements(password), () => getPasswordRequirements(password),
[password], [password],
@@ -224,6 +213,12 @@ export default function CreateUserDialog({
type={showPasswordVisible ? "text" : "password"} type={showPasswordVisible ? "text" : "password"}
className="h-10 pr-10" className="h-10 pr-10"
{...field} {...field}
onChange={(e) => {
field.onChange(e);
if (form.getValues("confirmPassword")) {
form.trigger("confirmPassword");
}
}}
/> />
<Button <Button
type="button" type="button"
@@ -255,21 +250,6 @@ export default function CreateUserDialog({
{password && ( {password && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-secondary-foreground">
<div
className={`${getPasswordStrengthColor(
password,
)} transition-all duration-300`}
style={{ width: `${passwordStrength * 100}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
{t("users.dialog.form.password.strength.title")}
<span className="font-medium">
{getPasswordStrengthLabel(password, t)}
</span>
</p>
<div className="space-y-1 rounded-md bg-muted/50 p-2"> <div className="space-y-1 rounded-md bg-muted/50 p-2">
<p className="text-xs font-medium text-muted-foreground"> <p className="text-xs font-medium text-muted-foreground">
{t("users.dialog.form.password.requirements.title")} {t("users.dialog.form.password.requirements.title")}
@@ -297,8 +277,6 @@ export default function CreateUserDialog({
</div> </div>
</div> </div>
)} )}
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
@@ -366,7 +344,6 @@ export default function CreateUserDialog({
)} )}
</div> </div>
)} )}
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
@@ -28,12 +28,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
import { import { getPasswordRequirements } from "@/utils/passwordUtil";
calculatePasswordStrength,
getPasswordRequirements,
getPasswordStrengthLabel,
getPasswordStrengthColor,
} from "@/utils/passwordUtil";
type SetPasswordProps = { type SetPasswordProps = {
show: boolean; show: boolean;
@@ -124,12 +119,6 @@ export default function SetPasswordDialog({
const password = form.watch("password"); const password = form.watch("password");
const confirmPassword = form.watch("confirmPassword"); const confirmPassword = form.watch("confirmPassword");
// Password strength calculation
const passwordStrength = useMemo(
() => calculatePasswordStrength(password),
[password],
);
const requirements = useMemo( const requirements = useMemo(
() => getPasswordRequirements(password), () => getPasswordRequirements(password),
[password], [password],
@@ -303,6 +292,12 @@ export default function SetPasswordDialog({
)} )}
className="h-10 pr-10" className="h-10 pr-10"
autoFocus autoFocus
onChange={(e) => {
field.onChange(e);
if (form.getValues("confirmPassword")) {
form.trigger("confirmPassword");
}
}}
/> />
<Button <Button
type="button" type="button"
@@ -334,21 +329,6 @@ export default function SetPasswordDialog({
{password && ( {password && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-secondary-foreground">
<div
className={`${getPasswordStrengthColor(
password,
)} transition-all duration-300`}
style={{ width: `${passwordStrength * 100}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
{t("users.dialog.form.password.strength.title")}
<span className="font-medium">
{getPasswordStrengthLabel(password, t)}
</span>
</p>
<div className="space-y-1 rounded-md bg-muted/50 p-2"> <div className="space-y-1 rounded-md bg-muted/50 p-2">
<p className="text-xs font-medium text-muted-foreground"> <p className="text-xs font-medium text-muted-foreground">
{t("users.dialog.form.password.requirements.title")} {t("users.dialog.form.password.requirements.title")}
@@ -376,8 +356,6 @@ export default function SetPasswordDialog({
</div> </div>
</div> </div>
)} )}
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
@@ -428,18 +406,25 @@ export default function SetPasswordDialog({
</div> </div>
</FormControl> </FormControl>
{password && {password && confirmPassword && (
confirmPassword && <div className="mt-1 flex items-center gap-1.5 text-xs">
password === confirmPassword && ( {password === confirmPassword ? (
<div className="mt-1 flex items-center gap-1.5 text-xs"> <>
<LuCheck className="size-3.5 text-green-500" /> <LuCheck className="size-3.5 text-green-500" />
<span className="text-green-600"> <span className="text-green-600">
{t("users.dialog.form.password.match")} {t("users.dialog.form.password.match")}
</span> </span>
</div> </>
)} ) : (
<>
<FormMessage /> <LuX className="size-3.5 text-red-500" />
<span className="text-red-600">
{t("users.dialog.form.password.notMatch")}
</span>
</>
)}
</div>
)}
</FormItem> </FormItem>
)} )}
/> />
-38
View File
@@ -1,41 +1,3 @@
import { TFunction } from "i18next";
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) => ({ export const getPasswordRequirements = (password: string) => ({
length: password?.length >= 12, length: password?.length >= 12,
}); });
export const getPasswordStrengthLabel = (
password: string,
t: TFunction,
): string => {
const strength = calculatePasswordStrength(password);
if (!password) return "";
if (strength < 1)
return t("users.dialog.form.password.strength.weak", {
ns: "views/settings",
});
return t("users.dialog.form.password.strength.veryStrong", {
ns: "views/settings",
});
};
export const getPasswordStrengthColor = (password: string): string => {
const strength = calculatePasswordStrength(password);
if (!password) return "bg-gray-200";
if (strength === 0) return "bg-red-500";
return "bg-green-500";
};