mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-19 23:08:08 +02:00
Miscellaneous fixes (0.17 beta) (#21867)
* Adjust title prompt to have less rigidity * Improve motion boxes handling for features that don't require motion * Improve handling of classes starting with digits * Improve vehicle nuance * tweak lpr docs * Improve grammar * Don't allow # in face name * add password requirements to new user dialog * change password requirements * Clenaup --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user