Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc45d1e0de | ||
|
|
5c0fd755cc | ||
|
|
9bf6d9851c | ||
|
|
f1ed293b79 | ||
|
|
0df64f05e5 | ||
|
|
d2ce64252c | ||
|
|
63d50ed5fb | ||
|
|
27325326da |
No files matched your search
@@ -1,15 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, Stack, TextInput } from "@mantine/core";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { signIn } from "@homarr/auth/client";
|
import { signIn } from "@homarr/auth/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
|
import { UserCreatePasswordFields } from "@homarr/forms-collection";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput } from "@homarr/ui";
|
|
||||||
import { userInitSchema } from "@homarr/validation/user";
|
import { userInitSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
export const InitUserForm = () => {
|
export const InitUserForm = () => {
|
||||||
@@ -59,12 +59,10 @@ export const InitUserForm = () => {
|
|||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<TextInput label={t("field.username.label")} {...form.getInputProps("username")} />
|
<TextInput label={t("field.username.label")} {...form.getInputProps("username")} />
|
||||||
<CustomPasswordInput
|
<UserCreatePasswordFields
|
||||||
withPasswordRequirements
|
passwordInputProps={form.getInputProps("password")}
|
||||||
label={t("field.password.label")}
|
confirmPasswordInputProps={form.getInputProps("confirmPassword")}
|
||||||
{...form.getInputProps("password")}
|
|
||||||
/>
|
/>
|
||||||
<PasswordInput label={t("field.passwordConfirm.label")} {...form.getInputProps("confirmPassword")} />
|
|
||||||
<Button type="submit" fullWidth loading={isPending}>
|
<Button type="submit" fullWidth loading={isPending}>
|
||||||
{t("action.create")}
|
{t("action.create")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+6
-26
@@ -1,20 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { startTransition, useCallback, useState } from "react";
|
import { startTransition, useCallback, useState } from "react";
|
||||||
import {
|
import { Badge, Button, Card, Group, Stack, Stepper, Table, Text, TextInput, Title, Tooltip } from "@mantine/core";
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
PasswordInput,
|
|
||||||
Stack,
|
|
||||||
Stepper,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useListState } from "@mantine/hooks";
|
import { useListState } from "@mantine/hooks";
|
||||||
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
@@ -24,10 +11,11 @@ import type { GroupPermissionKey } from "@homarr/definitions";
|
|||||||
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
||||||
import type { IsValid } from "@homarr/form";
|
import type { IsValid } from "@homarr/form";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
|
import { UserCreatePasswordFields } from "@homarr/forms-collection";
|
||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { showErrorNotification } from "@homarr/notifications";
|
import { showErrorNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
|
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
|
||||||
import { userPasswordSchema } from "@homarr/validation/user";
|
import { userPasswordSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
@@ -167,18 +155,10 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
|||||||
<form>
|
<form>
|
||||||
<Card p="xl">
|
<Card p="xl">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<CustomPasswordInput
|
<UserCreatePasswordFields
|
||||||
withPasswordRequirements
|
|
||||||
label={tUserField("password.label")}
|
|
||||||
variant="filled"
|
variant="filled"
|
||||||
withAsterisk
|
passwordInputProps={securityForm.getInputProps("password")}
|
||||||
{...securityForm.getInputProps("password")}
|
confirmPasswordInputProps={securityForm.getInputProps("confirmPassword")}
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={tUserField("passwordConfirm.label")}
|
|
||||||
variant="filled"
|
|
||||||
withAsterisk
|
|
||||||
{...securityForm.getInputProps("confirmPassword")}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -81,13 +81,7 @@ describe("initUser should initialize the first user", () => {
|
|||||||
await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch");
|
await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([["aB2%"], ["short"]])("should reject passwords shorter than 8 characters for '%s'", async (password) => {
|
||||||
["aB2%"], // too short
|
|
||||||
["abc123DEF"], // does not contain special characters
|
|
||||||
["abcDEFghi+"], // does not contain numbers
|
|
||||||
["ABC123+/-"], // does not contain lowercase
|
|
||||||
["abc123+/-"], // does not contain uppercase
|
|
||||||
])("should throw error that password requirements do not match for '%s' as password", async (password) => {
|
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
await createOnboardingStepAsync(db, "user");
|
await createOnboardingStepAsync(db, "user");
|
||||||
const caller = userRouter.createCaller({
|
const caller = userRouter.createCaller({
|
||||||
@@ -103,7 +97,55 @@ describe("initUser should initialize the first user", () => {
|
|||||||
confirmPassword: password,
|
confirmPassword: password,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(actAsync()).rejects.toThrow("passwordRequirements");
|
await expect(actAsync()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept passwords without complexity requirements", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
await createOnboardingStepAsync(db, "user");
|
||||||
|
const caller = userRouter.createCaller({
|
||||||
|
db,
|
||||||
|
deviceType: undefined,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await caller.initUser({
|
||||||
|
username: "test",
|
||||||
|
password: "abc123DEF",
|
||||||
|
confirmPassword: "abc123DEF",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept passwords with special characters like LoveHomarr<3", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
await createOnboardingStepAsync(db, "user");
|
||||||
|
const caller = userRouter.createCaller({
|
||||||
|
db,
|
||||||
|
deviceType: undefined,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await caller.initUser({
|
||||||
|
username: "test",
|
||||||
|
password: "LoveHomarr<3",
|
||||||
|
confirmPassword: "LoveHomarr<3",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ export * from "./icon-picker/icon-picker";
|
|||||||
export * from "./new-app/icon-matcher";
|
export * from "./new-app/icon-matcher";
|
||||||
|
|
||||||
export * from "./upload-media/upload-media";
|
export * from "./upload-media/upload-media";
|
||||||
|
|
||||||
|
export * from "./user-create/user-create-password-fields";
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { PasswordInput, Stack } from "@mantine/core";
|
||||||
|
import type { PasswordInputProps } from "@mantine/core";
|
||||||
|
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
import { CustomPasswordInput } from "@homarr/ui";
|
||||||
|
|
||||||
|
interface UserCreatePasswordFieldsProps {
|
||||||
|
passwordInputProps: PasswordInputProps;
|
||||||
|
confirmPasswordInputProps: PasswordInputProps;
|
||||||
|
variant?: PasswordInputProps["variant"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserCreatePasswordFields = ({
|
||||||
|
passwordInputProps,
|
||||||
|
confirmPasswordInputProps,
|
||||||
|
variant,
|
||||||
|
}: UserCreatePasswordFieldsProps) => {
|
||||||
|
const t = useScopedI18n("user.field");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<CustomPasswordInput
|
||||||
|
withPasswordRequirements
|
||||||
|
label={t("password.label")}
|
||||||
|
variant={variant}
|
||||||
|
{...passwordInputProps}
|
||||||
|
/>
|
||||||
|
<PasswordInput label={t("passwordConfirm.label")} variant={variant} {...confirmPasswordInputProps} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Contrasenya",
|
"label": "Contrasenya",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Inclou com a mínim 8 caràcters",
|
"length": "Inclou com a mínim 8 caràcters",
|
||||||
"lowercase": "Inclou minúscules",
|
"lowercase": "Inclou minúscules",
|
||||||
"uppercase": "Inclou majúscules",
|
"uppercase": "Inclou majúscules",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "密码",
|
"label": "密码",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "至少包含 8 个字符",
|
"length": "至少包含 8 个字符",
|
||||||
"lowercase": "包含小写字母",
|
"lowercase": "包含小写字母",
|
||||||
"uppercase": "包含大写字母",
|
"uppercase": "包含大写字母",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "crwdns26:0crwdne26:0",
|
"label": "crwdns26:0crwdne26:0",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "crwdns28:0crwdne28:0",
|
"length": "crwdns28:0crwdne28:0",
|
||||||
"lowercase": "crwdns30:0crwdne30:0",
|
"lowercase": "crwdns30:0crwdne30:0",
|
||||||
"uppercase": "crwdns32:0crwdne32:0",
|
"uppercase": "crwdns32:0crwdne32:0",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Heslo",
|
"label": "Heslo",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Obsahuje alespoň 8 znaků",
|
"length": "Obsahuje alespoň 8 znaků",
|
||||||
"lowercase": "Obsahuje malé písmeno",
|
"lowercase": "Obsahuje malé písmeno",
|
||||||
"uppercase": "Obsahuje velké písmeno",
|
"uppercase": "Obsahuje velké písmeno",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Adgangskode",
|
"label": "Adgangskode",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Inkluderer mindst 8 tegn",
|
"length": "Inkluderer mindst 8 tegn",
|
||||||
"lowercase": "Inkluderer små bogstaver",
|
"lowercase": "Inkluderer små bogstaver",
|
||||||
"uppercase": "Inkluderer store bogstaver",
|
"uppercase": "Inkluderer store bogstaver",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Passwort",
|
"label": "Passwort",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Enthält mindestens 8 Zeichen",
|
"length": "Enthält mindestens 8 Zeichen",
|
||||||
"lowercase": "Enthält Kleinbuchstaben",
|
"lowercase": "Enthält Kleinbuchstaben",
|
||||||
"uppercase": "Enthält Großbuchstaben",
|
"uppercase": "Enthält Großbuchstaben",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Passwort",
|
"label": "Passwort",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Enthält mindestens 8 Zeichen",
|
"length": "Enthält mindestens 8 Zeichen",
|
||||||
"lowercase": "Enthält Kleinbuchstaben",
|
"lowercase": "Enthält Kleinbuchstaben",
|
||||||
"uppercase": "Enthält Großbuchstaben",
|
"uppercase": "Enthält Großbuchstaben",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Κωδικός",
|
"label": "Κωδικός",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "Περιλαμβάνει πεζό γράμμα",
|
"lowercase": "Περιλαμβάνει πεζό γράμμα",
|
||||||
"uppercase": "Περιλαμβάνει κεφαλαίο γράμμα",
|
"uppercase": "Περιλαμβάνει κεφαλαίο γράμμα",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Includes a minimum of 8 characters",
|
"length": "Includes a minimum of 8 characters",
|
||||||
"lowercase": "Includes a lower-case letter",
|
"lowercase": "Includes a lower-case letter",
|
||||||
"uppercase": "Includes an upper-case letter",
|
"uppercase": "Includes an upper-case letter",
|
||||||
|
|||||||
@@ -132,8 +132,9 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"requirement": {
|
"suggestionsTitle": "Password suggestions",
|
||||||
"length": "Includes at least 8 characters",
|
"suggestion": {
|
||||||
|
"length": "At least 8 characters",
|
||||||
"lowercase": "Includes lowercase letter",
|
"lowercase": "Includes lowercase letter",
|
||||||
"uppercase": "Includes uppercase letter",
|
"uppercase": "Includes uppercase letter",
|
||||||
"number": "Includes number",
|
"number": "Includes number",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Contraseña",
|
"label": "Contraseña",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Incluye al menos 8 caracteres",
|
"length": "Incluye al menos 8 caracteres",
|
||||||
"lowercase": "Incluye al menos una letra minúscula",
|
"lowercase": "Incluye al menos una letra minúscula",
|
||||||
"uppercase": "Incluye al menos una letra mayúscula",
|
"uppercase": "Incluye al menos una letra mayúscula",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "",
|
"lowercase": "",
|
||||||
"uppercase": "",
|
"uppercase": "",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Mot de passe",
|
"label": "Mot de passe",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Contient au moins 8 caractères",
|
"length": "Contient au moins 8 caractères",
|
||||||
"lowercase": "Inclut une lettre minuscule",
|
"lowercase": "Inclut une lettre minuscule",
|
||||||
"uppercase": "Inclut une lettre majuscule",
|
"uppercase": "Inclut une lettre majuscule",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "סיסמה",
|
"label": "סיסמה",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "כולל לפחות 8 תווים",
|
"length": "כולל לפחות 8 תווים",
|
||||||
"lowercase": "אפשר אותיות קטנות",
|
"lowercase": "אפשר אותיות קטנות",
|
||||||
"uppercase": "אפשר אותיות גדולות",
|
"uppercase": "אפשר אותיות גדולות",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Lozinka",
|
"label": "Lozinka",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "Uključuje mala slova",
|
"lowercase": "Uključuje mala slova",
|
||||||
"uppercase": "Uključuje veliko slovo",
|
"uppercase": "Uključuje veliko slovo",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Jelszó",
|
"label": "Jelszó",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Tartalmazzon legalább 8 karaktert",
|
"length": "Tartalmazzon legalább 8 karaktert",
|
||||||
"lowercase": "Tartalmazzon kisbetűt",
|
"lowercase": "Tartalmazzon kisbetűt",
|
||||||
"uppercase": "Tartalmazzon nagybetűt",
|
"uppercase": "Tartalmazzon nagybetűt",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Include almeno 8 caratteri",
|
"length": "Include almeno 8 caratteri",
|
||||||
"lowercase": "Include lettera minuscola",
|
"lowercase": "Include lettera minuscola",
|
||||||
"uppercase": "Include lettera maiuscola",
|
"uppercase": "Include lettera maiuscola",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "パスワード",
|
"label": "パスワード",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "少なくとも 8 文字以上を含む",
|
"length": "少なくとも 8 文字以上を含む",
|
||||||
"lowercase": "小文字を含む",
|
"lowercase": "小文字を含む",
|
||||||
"uppercase": "大文字を含む",
|
"uppercase": "大文字を含む",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "비밀번호",
|
"label": "비밀번호",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "소문자 포함",
|
"lowercase": "소문자 포함",
|
||||||
"uppercase": "대문자 포함",
|
"uppercase": "대문자 포함",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Slaptažodis",
|
"label": "Slaptažodis",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "",
|
"lowercase": "",
|
||||||
"uppercase": "",
|
"uppercase": "",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Parole",
|
"label": "Parole",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "Ietver mazo burtu",
|
"lowercase": "Ietver mazo burtu",
|
||||||
"uppercase": "Ietver lielo burtu",
|
"uppercase": "Ietver lielo burtu",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Wachtwoord",
|
"label": "Wachtwoord",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Bevat ten minste 8 tekens",
|
"length": "Bevat ten minste 8 tekens",
|
||||||
"lowercase": "Inclusief kleine letter",
|
"lowercase": "Inclusief kleine letter",
|
||||||
"uppercase": "Inclusief hoofdletter",
|
"uppercase": "Inclusief hoofdletter",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Passord",
|
"label": "Passord",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Inkluderer minst 8 tegn",
|
"length": "Inkluderer minst 8 tegn",
|
||||||
"lowercase": "Inkluderer liten bokstav",
|
"lowercase": "Inkluderer liten bokstav",
|
||||||
"uppercase": "Inkluderer stor bokstav",
|
"uppercase": "Inkluderer stor bokstav",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Hasło",
|
"label": "Hasło",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Zawiera co najmniej 8 znaków",
|
"length": "Zawiera co najmniej 8 znaków",
|
||||||
"lowercase": "Zawiera małą literę",
|
"lowercase": "Zawiera małą literę",
|
||||||
"uppercase": "Zawiera wielką literę",
|
"uppercase": "Zawiera wielką literę",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Senha",
|
"label": "Senha",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Inclua pelo menos 8 caracteres",
|
"length": "Inclua pelo menos 8 caracteres",
|
||||||
"lowercase": "Inclui letras minúsculas",
|
"lowercase": "Inclui letras minúsculas",
|
||||||
"uppercase": "Inclui letras maiúsculas",
|
"uppercase": "Inclui letras maiúsculas",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "",
|
"lowercase": "",
|
||||||
"uppercase": "",
|
"uppercase": "",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Parola",
|
"label": "Parola",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "Să includă litere mici",
|
"lowercase": "Să includă litere mici",
|
||||||
"uppercase": "Să includă litere mari",
|
"uppercase": "Să includă litere mari",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Пароль",
|
"label": "Пароль",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Содержит не менее 8 символов",
|
"length": "Содержит не менее 8 символов",
|
||||||
"lowercase": "Содержит строчную букву",
|
"lowercase": "Содержит строчную букву",
|
||||||
"uppercase": "Содержит заглавную букву",
|
"uppercase": "Содержит заглавную букву",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Heslo",
|
"label": "Heslo",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Obsahuje aspoň 8 znakov",
|
"length": "Obsahuje aspoň 8 znakov",
|
||||||
"lowercase": "Zahŕňa malé písmeno",
|
"lowercase": "Zahŕňa malé písmeno",
|
||||||
"uppercase": "Zahŕňa veľké písmená",
|
"uppercase": "Zahŕňa veľké písmená",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Geslo",
|
"label": "Geslo",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "",
|
"length": "",
|
||||||
"lowercase": "Vključuje male črke",
|
"lowercase": "Vključuje male črke",
|
||||||
"uppercase": "Vključuje velike tiskane črke",
|
"uppercase": "Vključuje velike tiskane črke",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Lösenord",
|
"label": "Lösenord",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Använd minst 8 tecken",
|
"length": "Använd minst 8 tecken",
|
||||||
"lowercase": "Använd liten bokstav",
|
"lowercase": "Använd liten bokstav",
|
||||||
"uppercase": "Använd stor bokstav",
|
"uppercase": "Använd stor bokstav",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Şifre",
|
"label": "Şifre",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "En az 8 karakter içermeli",
|
"length": "En az 8 karakter içermeli",
|
||||||
"lowercase": "Küçük harf içermeli",
|
"lowercase": "Küçük harf içermeli",
|
||||||
"uppercase": "Büyük harf içermeli",
|
"uppercase": "Büyük harf içermeli",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Пароль",
|
"label": "Пароль",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Включає принаймні 8 символів",
|
"length": "Включає принаймні 8 символів",
|
||||||
"lowercase": "Включає малу літеру",
|
"lowercase": "Включає малу літеру",
|
||||||
"uppercase": "Включає велику літеру",
|
"uppercase": "Включає велику літеру",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Mật khẩu",
|
"label": "Mật khẩu",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "Bao gồm ít nhất 8 kí tự",
|
"length": "Bao gồm ít nhất 8 kí tự",
|
||||||
"lowercase": "Bao gồm chữ thường",
|
"lowercase": "Bao gồm chữ thường",
|
||||||
"uppercase": "Bao gồm chữ in hoa",
|
"uppercase": "Bao gồm chữ in hoa",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "密碼",
|
"label": "密碼",
|
||||||
"requirement": {
|
"suggestion": {
|
||||||
"length": "至少包含 8 個字符",
|
"length": "至少包含 8 個字符",
|
||||||
"lowercase": "包含小寫字母",
|
"lowercase": "包含小寫字母",
|
||||||
"uppercase": "包含大寫字母",
|
"uppercase": "包含大寫字母",
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ export const CustomPasswordInput = ({ withPasswordRequirements, ...props }: Cust
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WithPasswordRequirements = (props: PasswordInputProps) => {
|
const WithPasswordRequirements = (props: PasswordInputProps) => {
|
||||||
const [value, setValue] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(event.currentTarget.value);
|
setPassword(event.currentTarget.value);
|
||||||
props.onChange?.(event);
|
props.onChange?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PasswordRequirementsPopover password={value}>
|
<PasswordRequirementsPopover password={password}>
|
||||||
<PasswordInput {...props} onChange={onChange} />
|
<PasswordInput {...props} onChange={onChange} />
|
||||||
</PasswordRequirementsPopover>
|
</PasswordRequirementsPopover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { rem, Text } from "@mantine/core";
|
import { rem, Text } from "@mantine/core";
|
||||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
import { IconCheck, IconPoint } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const getRequirementDisplay = (meets: boolean) =>
|
||||||
|
({
|
||||||
|
true: { color: "teal", Icon: IconCheck },
|
||||||
|
false: { color: "dimmed", Icon: IconPoint },
|
||||||
|
})[String(meets) as "true" | "false"];
|
||||||
|
|
||||||
export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
||||||
|
const display = getRequirementDisplay(meets);
|
||||||
|
const Icon = display.Icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text c={meets ? "teal" : "red"} display="flex" style={{ alignItems: "center" }} size="sm">
|
<Text c={display.color} display="flex" style={{ alignItems: "center" }} size="sm">
|
||||||
{meets ? (
|
<Icon style={{ width: rem(14), height: rem(14) }} />
|
||||||
<IconCheck style={{ width: rem(14), height: rem(14) }} />
|
|
||||||
) : (
|
|
||||||
<IconX style={{ width: rem(14), height: rem(14) }} />
|
|
||||||
)}
|
|
||||||
<Text span ml={10}>
|
<Text span ml={10}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Popover, Progress } from "@mantine/core";
|
import { Popover, Progress, Text } from "@mantine/core";
|
||||||
|
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { passwordRequirements } from "@homarr/validation/user";
|
import { passwordRequirements } from "@homarr/validation/user";
|
||||||
|
|
||||||
import { PasswordRequirement } from "./password-requirement";
|
import { PasswordRequirement } from "./password-requirement";
|
||||||
|
|
||||||
|
const strengthColorMap = [
|
||||||
|
{ min: 100, color: "teal" },
|
||||||
|
{ min: 51, color: "yellow" },
|
||||||
|
{ min: 0, color: "dimmed" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => {
|
export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => {
|
||||||
|
const tPassword = useScopedI18n("user.field.password");
|
||||||
const requirements = useRequirements();
|
const requirements = useRequirements();
|
||||||
const strength = useStrength(password);
|
const strength = useStrength(password);
|
||||||
const [popoverOpened, setPopoverOpened] = useState(false);
|
const [popoverOpened, setPopoverOpened] = useState(false);
|
||||||
|
const strengthColor = strengthColorMap.find(({ min }) => strength >= min)?.color ?? "dimmed";
|
||||||
const checks = (
|
const checks = (
|
||||||
<>
|
<>
|
||||||
{requirements.map((requirement) => (
|
{requirements.map((requirement) => (
|
||||||
@@ -19,8 +27,6 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const color = strength === 100 ? "teal" : strength > 50 ? "yellow" : "red";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}>
|
<Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
@@ -29,7 +35,10 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
|
|||||||
</div>
|
</div>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<Progress color={color} value={strength} size={5} mb="xs" />
|
<Progress color={strengthColor} value={strength} size={5} mb="xs" />
|
||||||
|
<Text size="sm" c="dimmed" mb="xs">
|
||||||
|
{tPassword("suggestionsTitle")}
|
||||||
|
</Text>
|
||||||
{checks}
|
{checks}
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -37,7 +46,7 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useRequirements = () => {
|
const useRequirements = () => {
|
||||||
const t = useScopedI18n("user.field.password.requirement");
|
const t = useScopedI18n("user.field.password.suggestion");
|
||||||
|
|
||||||
return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) }));
|
return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) }));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,24 +19,10 @@ export const passwordRequirements = [
|
|||||||
{ check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" },
|
{ check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" },
|
||||||
] satisfies {
|
] satisfies {
|
||||||
check: (value: string) => boolean;
|
check: (value: string) => boolean;
|
||||||
value: keyof TranslationObject["user"]["field"]["password"]["requirement"];
|
value: keyof TranslationObject["user"]["field"]["password"]["suggestion"];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export const userPasswordSchema = z
|
export const userPasswordSchema = z.string().min(8).max(255);
|
||||||
.string()
|
|
||||||
.min(8)
|
|
||||||
.max(255)
|
|
||||||
.refine(
|
|
||||||
(value) => {
|
|
||||||
return passwordRequirements.every((requirement) => requirement.check(value));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: createCustomErrorParams({
|
|
||||||
key: "passwordRequirements",
|
|
||||||
params: {},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const addConfirmPasswordRefinement = <
|
const addConfirmPasswordRefinement = <
|
||||||
TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>,
|
TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>,
|
||||||
|
|||||||
Reference in new issue
Block a user