mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-21 23:58:22 +03:00
Compare commits
3 Commits
aaa61ae2e0
...
a8cccaaf1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8cccaaf1c | ||
|
|
8b426743c2 | ||
|
|
ac6dda370d |
@ -29,6 +29,10 @@ auth:
|
||||
reset_admin_password: true
|
||||
```
|
||||
|
||||
## Password guidance
|
||||
|
||||
Constructing secure passwords and managing them properly is important. Frigate requires a minimum length of 12 characters. For guidance on password standards see [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html). To learn what makes a password truly secure, read this [article](https://medium.com/peerio/how-to-build-a-billion-dollar-password-3d92568d9277).
|
||||
|
||||
## Login failure rate limiting
|
||||
|
||||
In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with SlowApi, and the string notation for valid values is available in [the documentation](https://limits.readthedocs.io/en/stable/quickstart.html#examples).
|
||||
|
||||
@ -350,21 +350,15 @@ def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
||||
Validate password strength.
|
||||
|
||||
Returns a tuple of (is_valid, error_message).
|
||||
|
||||
Longer passwords are harder to crack than shorter complex ones.
|
||||
https://pages.nist.gov/800-63-3/sp800-63b.html
|
||||
"""
|
||||
if not password:
|
||||
return False, "Password cannot be empty"
|
||||
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
|
||||
if not any(c.isupper() for c in password):
|
||||
return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
if not any(c.isdigit() for c in password):
|
||||
return False, "Password must contain at least one digit"
|
||||
|
||||
if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password):
|
||||
return False, "Password must contain at least one special character"
|
||||
if len(password) < 12:
|
||||
return False, "Password must be at least 12 characters long"
|
||||
|
||||
return True, None
|
||||
|
||||
@ -800,7 +794,7 @@ def get_users():
|
||||
"/users",
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Create new user",
|
||||
description='Creates a new user with the specified username, password, and role. Requires admin role. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?":{} |<>).',
|
||||
description="Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long.",
|
||||
)
|
||||
def create_user(
|
||||
request: Request,
|
||||
@ -817,6 +811,15 @@ def create_user(
|
||||
content={"message": f"Role must be one of: {', '.join(config_roles)}"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Validate password strength
|
||||
is_valid, error_message = validate_password_strength(body.password)
|
||||
if not is_valid:
|
||||
return JSONResponse(
|
||||
content={"message": error_message},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
role = body.role or "viewer"
|
||||
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
||||
User.insert(
|
||||
@ -851,7 +854,7 @@ def delete_user(request: Request, username: str):
|
||||
"/users/{username}/password",
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Update user password",
|
||||
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?\":{} |<>). If user changes their own password, a new JWT cookie is automatically issued.",
|
||||
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued.",
|
||||
)
|
||||
async def update_password(
|
||||
request: Request,
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"addFace": "Add a new collection to the Face Library by uploading your first image.",
|
||||
"placeholder": "Enter a name for this collection",
|
||||
"invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens."
|
||||
"invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens.",
|
||||
"nameCannotContainHash": "Name cannot contain #."
|
||||
},
|
||||
"details": {
|
||||
"timestamp": "Timestamp",
|
||||
|
||||
@ -728,10 +728,7 @@
|
||||
},
|
||||
"requirements": {
|
||||
"title": "Password requirements:",
|
||||
"length": "At least 8 characters",
|
||||
"uppercase": "At least one uppercase letter",
|
||||
"digit": "At least one digit",
|
||||
"special": "At least one special character (!@#$%^&*(),.?\":{}|<>)"
|
||||
"length": "At least 12 characters"
|
||||
},
|
||||
"match": "Passwords match",
|
||||
"notMatch": "Passwords don't match"
|
||||
|
||||
@ -20,6 +20,8 @@ type TextEntryProps = {
|
||||
children?: React.ReactNode;
|
||||
regexPattern?: RegExp;
|
||||
regexErrorMessage?: string;
|
||||
forbiddenPattern?: RegExp;
|
||||
forbiddenErrorMessage?: string;
|
||||
};
|
||||
|
||||
export default function TextEntry({
|
||||
@ -30,11 +32,16 @@ export default function TextEntry({
|
||||
children,
|
||||
regexPattern,
|
||||
regexErrorMessage = "Input does not match the required format",
|
||||
forbiddenPattern,
|
||||
forbiddenErrorMessage = "Input contains invalid characters",
|
||||
}: TextEntryProps) {
|
||||
const formSchema = z.object({
|
||||
text: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((val) => !val || !forbiddenPattern?.test(val), {
|
||||
message: forbiddenErrorMessage,
|
||||
})
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!allowEmpty && !val) return false;
|
||||
|
||||
@ -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<FrigateConfig>("config");
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [showPasswordVisible, setShowPasswordVisible] =
|
||||
useState<boolean>(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const roles = useMemo(() => {
|
||||
const existingRoles = config ? Object.keys(config.auth?.roles || {}) : [];
|
||||
@ -73,7 +83,9 @@ 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(12, t("users.dialog.form.password.requirements.length")),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, t("users.dialog.createUser.confirmPassword")),
|
||||
@ -108,13 +120,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 +148,11 @@ export default function CreateUserDialog({
|
||||
form.reset({
|
||||
user: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
role: "viewer",
|
||||
});
|
||||
setShowPasswordVisible(false);
|
||||
setShowConfirmPassword(false);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
@ -184,13 +213,88 @@ export default function CreateUserDialog({
|
||||
{t("users.dialog.form.password.title")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("users.dialog.form.password.placeholder")}
|
||||
type="password"
|
||||
className="h-10"
|
||||
{...field}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={t(
|
||||
"users.dialog.form.password.placeholder",
|
||||
)}
|
||||
type={showPasswordVisible ? "text" : "password"}
|
||||
className="h-10 pr-10"
|
||||
{...field}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
aria-label={
|
||||
showPasswordVisible
|
||||
? t("users.dialog.form.password.hide", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("users.dialog.form.password.show", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
}
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() =>
|
||||
setShowPasswordVisible(!showPasswordVisible)
|
||||
}
|
||||
>
|
||||
{showPasswordVisible ? (
|
||||
<LuEyeOff className="size-4" />
|
||||
) : (
|
||||
<LuEye className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
{password && (
|
||||
<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">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
{t("users.dialog.form.password.requirements.title")}
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.length ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.length
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.length",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@ -204,14 +308,41 @@ export default function CreateUserDialog({
|
||||
{t("users.dialog.form.password.confirm.title")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t(
|
||||
"users.dialog.form.password.confirm.placeholder",
|
||||
)}
|
||||
type="password"
|
||||
className="h-10"
|
||||
{...field}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={t(
|
||||
"users.dialog.form.password.confirm.placeholder",
|
||||
)}
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
className="h-10 pr-10"
|
||||
{...field}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
aria-label={
|
||||
showConfirmPassword
|
||||
? t("users.dialog.form.password.hide", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
: t("users.dialog.form.password.show", {
|
||||
ns: "views/settings",
|
||||
})
|
||||
}
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() =>
|
||||
setShowConfirmPassword(!showConfirmPassword)
|
||||
}
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<LuEyeOff className="size-4" />
|
||||
) : (
|
||||
<LuEye className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
{showMatchIndicator && (
|
||||
<div className="mt-1 flex items-center gap-1.5 text-xs">
|
||||
|
||||
@ -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;
|
||||
@ -70,13 +76,7 @@ export default function SetPasswordDialog({
|
||||
const baseSchema = {
|
||||
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"),
|
||||
),
|
||||
.min(12, t("users.dialog.form.password.requirements.length")),
|
||||
confirmPassword: z.string(),
|
||||
};
|
||||
|
||||
@ -125,25 +125,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 +184,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 (
|
||||
<Dialog open={show} onOpenChange={onCancel}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
@ -367,14 +336,16 @@ export default function SetPasswordDialog({
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-secondary-foreground">
|
||||
<div
|
||||
className={`${getStrengthColor()} transition-all duration-300`}
|
||||
style={{ width: `${(passwordStrength / 4) * 100}%` }}
|
||||
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">
|
||||
{getStrengthLabel()}
|
||||
{getPasswordStrengthLabel(password, t)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -401,60 +372,6 @@ export default function SetPasswordDialog({
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.uppercase ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.uppercase
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.uppercase",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.digit ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.digit
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.digit",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.special ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.special
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.special",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -128,6 +128,8 @@ export default function CreateFaceWizardDialog({
|
||||
}}
|
||||
regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u}
|
||||
regexErrorMessage={t("description.invalidName")}
|
||||
forbiddenPattern={/#/}
|
||||
forbiddenErrorMessage={t("description.nameCannotContainHash")}
|
||||
>
|
||||
<div className="flex justify-end py-2">
|
||||
<Button variant="select" type="submit">
|
||||
|
||||
@ -22,6 +22,8 @@ type TextEntryDialogProps = {
|
||||
allowEmpty?: boolean;
|
||||
regexPattern?: RegExp;
|
||||
regexErrorMessage?: string;
|
||||
forbiddenPattern?: RegExp;
|
||||
forbiddenErrorMessage?: string;
|
||||
};
|
||||
|
||||
export default function TextEntryDialog({
|
||||
@ -34,6 +36,8 @@ export default function TextEntryDialog({
|
||||
allowEmpty = false,
|
||||
regexPattern,
|
||||
regexErrorMessage,
|
||||
forbiddenPattern,
|
||||
forbiddenErrorMessage,
|
||||
}: TextEntryDialogProps) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
@ -50,6 +54,8 @@ export default function TextEntryDialog({
|
||||
onSave={onSave}
|
||||
regexPattern={regexPattern}
|
||||
regexErrorMessage={regexErrorMessage}
|
||||
forbiddenPattern={forbiddenPattern}
|
||||
forbiddenErrorMessage={forbiddenErrorMessage}
|
||||
>
|
||||
<DialogFooter className={cn("pt-4", isMobile && "gap-2")}>
|
||||
<Button type="button" onClick={() => setOpen(false)}>
|
||||
|
||||
@ -560,6 +560,8 @@ function LibrarySelector({
|
||||
defaultValue={renameFace || ""}
|
||||
regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u}
|
||||
regexErrorMessage={t("description.invalidName")}
|
||||
forbiddenPattern={/#/}
|
||||
forbiddenErrorMessage={t("description.nameCannotContainHash")}
|
||||
/>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
|
||||
34
web/src/utils/passwordUtil.ts
Normal file
34
web/src/utils/passwordUtil.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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 >= 12,
|
||||
});
|
||||
|
||||
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");
|
||||
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 === 0) return "bg-red-500";
|
||||
return "bg-green-500";
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user