mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-28 09:21:00 +01:00
feat: add password requirements (#988)
* feat: add password requirements * fix: format issue * fix: unexpected empty string in component jsx * test: adjust unit test passwords
This commit is contained in:
@@ -7,3 +7,4 @@ export { TablePagination } from "./table-pagination";
|
||||
export { TextMultiSelect } from "./text-multi-select";
|
||||
export { UserAvatar } from "./user-avatar";
|
||||
export { UserAvatarGroup } from "./user-avatar-group";
|
||||
export { CustomPasswordInput } from "./password-input/password-input";
|
||||
|
||||
35
packages/ui/src/components/password-input/password-input.tsx
Normal file
35
packages/ui/src/components/password-input/password-input.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import { useState } from "react";
|
||||
import { PasswordInput } from "@mantine/core";
|
||||
import type { PasswordInputProps } from "@mantine/core";
|
||||
|
||||
import { PasswordRequirementsPopover } from "./password-requirements-popover";
|
||||
|
||||
interface CustomPasswordInputProps extends PasswordInputProps {
|
||||
withPasswordRequirements?: boolean;
|
||||
}
|
||||
|
||||
export const CustomPasswordInput = ({ withPasswordRequirements, ...props }: CustomPasswordInputProps) => {
|
||||
if (withPasswordRequirements) {
|
||||
return <WithPasswordRequirements {...props} />;
|
||||
}
|
||||
|
||||
return <PasswordInput {...props} />;
|
||||
};
|
||||
|
||||
const WithPasswordRequirements = (props: PasswordInputProps) => {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(event.currentTarget.value);
|
||||
props.onChange?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<PasswordRequirementsPopover password={value}>
|
||||
<PasswordInput {...props} onChange={onChange} />
|
||||
</PasswordRequirementsPopover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { rem, Text } from "@mantine/core";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
||||
return (
|
||||
<Text c={meets ? "teal" : "red"} display="flex" style={{ alignItems: "center" }} size="sm">
|
||||
{meets ? (
|
||||
<IconCheck style={{ width: rem(14), height: rem(14) }} />
|
||||
) : (
|
||||
<IconX style={{ width: rem(14), height: rem(14) }} />
|
||||
)}
|
||||
<Text span ml={10}>
|
||||
{label}
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { useState } from "react";
|
||||
import { Popover, Progress } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { passwordRequirements } from "@homarr/validation";
|
||||
|
||||
import { PasswordRequirement } from "./password-requirement";
|
||||
|
||||
export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => {
|
||||
const requirements = useRequirements();
|
||||
const strength = useStrength(password);
|
||||
const [popoverOpened, setPopoverOpened] = useState(false);
|
||||
const checks = (
|
||||
<>
|
||||
{requirements.map((requirement) => (
|
||||
<PasswordRequirement key={requirement.label} label={requirement.label} meets={requirement.check(password)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const color = strength === 100 ? "teal" : strength > 50 ? "yellow" : "red";
|
||||
|
||||
return (
|
||||
<Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}>
|
||||
<Popover.Target>
|
||||
<div onFocusCapture={() => setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)}>
|
||||
{children}
|
||||
</div>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Progress color={color} value={strength} size={5} mb="xs" />
|
||||
{checks}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const useRequirements = () => {
|
||||
const t = useScopedI18n("user.field.password.requirement");
|
||||
|
||||
return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) }));
|
||||
};
|
||||
|
||||
function useStrength(password: string) {
|
||||
const requirements = useRequirements();
|
||||
|
||||
return (100 / requirements.length) * requirements.filter(({ check }) => check(password)).length;
|
||||
}
|
||||
Reference in New Issue
Block a user