From fd107888385557ededcf51576c6320a1960690f2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 4 Jun 2026 07:56:34 +0200 Subject: [PATCH] fix(auth): relax password complexity requirements to suggestions-only (#5764) --- .../init/_steps/user/init-user-form.tsx | 12 ++-- .../_components/create-user-stepper.tsx | 32 ++-------- packages/api/src/router/test/user.spec.ts | 58 ++++++++++++++++--- packages/forms-collection/src/index.tsx | 2 + .../user-create-password-fields.tsx | 33 +++++++++++ packages/translation/src/lang/ca.json | 2 +- packages/translation/src/lang/cn.json | 2 +- packages/translation/src/lang/cr.json | 2 +- packages/translation/src/lang/cs.json | 2 +- packages/translation/src/lang/da.json | 2 +- packages/translation/src/lang/de-CH.json | 2 +- packages/translation/src/lang/de.json | 2 +- packages/translation/src/lang/el.json | 2 +- packages/translation/src/lang/en-gb.json | 2 +- packages/translation/src/lang/en.json | 5 +- packages/translation/src/lang/es.json | 2 +- packages/translation/src/lang/et.json | 2 +- packages/translation/src/lang/fr.json | 2 +- packages/translation/src/lang/he.json | 2 +- packages/translation/src/lang/hr.json | 2 +- packages/translation/src/lang/hu.json | 2 +- packages/translation/src/lang/it.json | 2 +- packages/translation/src/lang/ja.json | 2 +- packages/translation/src/lang/ko.json | 2 +- packages/translation/src/lang/lt.json | 2 +- packages/translation/src/lang/lv.json | 2 +- packages/translation/src/lang/nl.json | 2 +- packages/translation/src/lang/no.json | 2 +- packages/translation/src/lang/pl.json | 2 +- packages/translation/src/lang/pt-br.json | 2 +- packages/translation/src/lang/pt.json | 2 +- packages/translation/src/lang/ro.json | 2 +- packages/translation/src/lang/ru.json | 2 +- packages/translation/src/lang/sk.json | 2 +- packages/translation/src/lang/sl.json | 2 +- packages/translation/src/lang/sv.json | 2 +- packages/translation/src/lang/tr.json | 2 +- packages/translation/src/lang/uk.json | 2 +- packages/translation/src/lang/vi.json | 2 +- packages/translation/src/lang/zh.json | 2 +- .../password-input/password-input.tsx | 6 +- .../password-input/password-requirement.tsx | 19 +++--- .../password-requirements-popover.tsx | 19 ++++-- packages/validation/src/user.ts | 18 +----- 44 files changed, 164 insertions(+), 108 deletions(-) create mode 100644 packages/forms-collection/src/user-create/user-create-password-fields.tsx diff --git a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx index 3a659032f..d3f1c665f 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx @@ -1,15 +1,15 @@ "use client"; -import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; +import { Button, Stack, TextInput } from "@mantine/core"; import type { z } from "zod/v4"; import { clientApi } from "@homarr/api/client"; import { signIn } from "@homarr/auth/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; +import { UserCreatePasswordFields } from "@homarr/forms-collection"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; -import { CustomPasswordInput } from "@homarr/ui"; import { userInitSchema } from "@homarr/validation/user"; export const InitUserForm = () => { @@ -59,12 +59,10 @@ export const InitUserForm = () => { > - - diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx index f443ae585..3997bda60 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx @@ -1,20 +1,7 @@ "use client"; import { startTransition, useCallback, useState } from "react"; -import { - Badge, - Button, - Card, - Group, - PasswordInput, - Stack, - Stepper, - Table, - Text, - TextInput, - Title, - Tooltip, -} from "@mantine/core"; +import { Badge, Button, Card, Group, Stack, Stepper, Table, Text, TextInput, Title, Tooltip } from "@mantine/core"; import { useListState } from "@mantine/hooks"; import { IconPlus, IconUserCheck } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -24,10 +11,11 @@ import type { GroupPermissionKey } from "@homarr/definitions"; import { everyoneGroup, groupPermissions } from "@homarr/definitions"; import type { IsValid } from "@homarr/form"; import { useZodForm } from "@homarr/form"; +import { UserCreatePasswordFields } from "@homarr/forms-collection"; import { useModalAction } from "@homarr/modals"; import { showErrorNotification } from "@homarr/notifications"; 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 { userPasswordSchema } from "@homarr/validation/user"; @@ -167,18 +155,10 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
- - diff --git a/packages/api/src/router/test/user.spec.ts b/packages/api/src/router/test/user.spec.ts index b3b262846..16cabfd52 100644 --- a/packages/api/src/router/test/user.spec.ts +++ b/packages/api/src/router/test/user.spec.ts @@ -81,13 +81,7 @@ describe("initUser should initialize the first user", () => { await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch"); }); - it.each([ - ["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) => { + it.each([["aB2%"], ["short"]])("should reject passwords shorter than 8 characters for '%s'", async (password) => { const db = createDb(); await createOnboardingStepAsync(db, "user"); const caller = userRouter.createCaller({ @@ -103,7 +97,55 @@ describe("initUser should initialize the first user", () => { 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(); }); }); diff --git a/packages/forms-collection/src/index.tsx b/packages/forms-collection/src/index.tsx index 218655cb5..7621bed43 100644 --- a/packages/forms-collection/src/index.tsx +++ b/packages/forms-collection/src/index.tsx @@ -5,3 +5,5 @@ export * from "./icon-picker/icon-picker"; export * from "./new-app/icon-matcher"; export * from "./upload-media/upload-media"; + +export * from "./user-create/user-create-password-fields"; diff --git a/packages/forms-collection/src/user-create/user-create-password-fields.tsx b/packages/forms-collection/src/user-create/user-create-password-fields.tsx new file mode 100644 index 000000000..ad7329307 --- /dev/null +++ b/packages/forms-collection/src/user-create/user-create-password-fields.tsx @@ -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 ( + + + + + ); +}; diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index 3e5c14996..01709fa33 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -132,7 +132,7 @@ }, "password": { "label": "Contrasenya", - "requirement": { + "suggestion": { "length": "Inclou com a mínim 8 caràcters", "lowercase": "Inclou minúscules", "uppercase": "Inclou majúscules", diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 05caccbe2..991a93b61 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -132,7 +132,7 @@ }, "password": { "label": "密码", - "requirement": { + "suggestion": { "length": "至少包含 8 个字符", "lowercase": "包含小写字母", "uppercase": "包含大写字母", diff --git a/packages/translation/src/lang/cr.json b/packages/translation/src/lang/cr.json index ab1fec215..f23abcea3 100644 --- a/packages/translation/src/lang/cr.json +++ b/packages/translation/src/lang/cr.json @@ -132,7 +132,7 @@ }, "password": { "label": "crwdns26:0crwdne26:0", - "requirement": { + "suggestion": { "length": "crwdns28:0crwdne28:0", "lowercase": "crwdns30:0crwdne30:0", "uppercase": "crwdns32:0crwdne32:0", diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 73bd6774f..d3db5802f 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -132,7 +132,7 @@ }, "password": { "label": "Heslo", - "requirement": { + "suggestion": { "length": "Obsahuje alespoň 8 znaků", "lowercase": "Obsahuje malé písmeno", "uppercase": "Obsahuje velké písmeno", diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index 25088e56d..bc10f4e93 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -132,7 +132,7 @@ }, "password": { "label": "Adgangskode", - "requirement": { + "suggestion": { "length": "Inkluderer mindst 8 tegn", "lowercase": "Inkluderer små bogstaver", "uppercase": "Inkluderer store bogstaver", diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index 240cc8a8d..49d53f90c 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -132,7 +132,7 @@ }, "password": { "label": "Passwort", - "requirement": { + "suggestion": { "length": "Enthält mindestens 8 Zeichen", "lowercase": "Enthält Kleinbuchstaben", "uppercase": "Enthält Großbuchstaben", diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index 1ac3c8dcb..729a73ab4 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -132,7 +132,7 @@ }, "password": { "label": "Passwort", - "requirement": { + "suggestion": { "length": "Enthält mindestens 8 Zeichen", "lowercase": "Enthält Kleinbuchstaben", "uppercase": "Enthält Großbuchstaben", diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index 2e86228dd..8f08912db 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -132,7 +132,7 @@ }, "password": { "label": "Κωδικός", - "requirement": { + "suggestion": { "length": "", "lowercase": "Περιλαμβάνει πεζό γράμμα", "uppercase": "Περιλαμβάνει κεφαλαίο γράμμα", diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index 04bc5a03c..a54b78e4e 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -132,7 +132,7 @@ }, "password": { "label": "Password", - "requirement": { + "suggestion": { "length": "Includes a minimum of 8 characters", "lowercase": "Includes a lower-case letter", "uppercase": "Includes an upper-case letter", diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index a62728845..2bfe4e223 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -132,8 +132,9 @@ }, "password": { "label": "Password", - "requirement": { - "length": "Includes at least 8 characters", + "suggestionsTitle": "Password suggestions", + "suggestion": { + "length": "At least 8 characters", "lowercase": "Includes lowercase letter", "uppercase": "Includes uppercase letter", "number": "Includes number", diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 148c022dd..3cc753819 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -132,7 +132,7 @@ }, "password": { "label": "Contraseña", - "requirement": { + "suggestion": { "length": "Incluye al menos 8 caracteres", "lowercase": "Incluye al menos una letra minúscula", "uppercase": "Incluye al menos una letra mayúscula", diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index c5e54f495..73952fef8 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -132,7 +132,7 @@ }, "password": { "label": "", - "requirement": { + "suggestion": { "length": "", "lowercase": "", "uppercase": "", diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index 2f173a25c..3f8028496 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -132,7 +132,7 @@ }, "password": { "label": "Mot de passe", - "requirement": { + "suggestion": { "length": "Contient au moins 8 caractères", "lowercase": "Inclut une lettre minuscule", "uppercase": "Inclut une lettre majuscule", diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 86c2e8ee3..7a5461433 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -132,7 +132,7 @@ }, "password": { "label": "סיסמה", - "requirement": { + "suggestion": { "length": "כולל לפחות 8 תווים", "lowercase": "אפשר אותיות קטנות", "uppercase": "אפשר אותיות גדולות", diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index dd637c9a6..c34436f78 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -132,7 +132,7 @@ }, "password": { "label": "Lozinka", - "requirement": { + "suggestion": { "length": "", "lowercase": "Uključuje mala slova", "uppercase": "Uključuje veliko slovo", diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index 1bad936e8..0eeb02f3d 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -132,7 +132,7 @@ }, "password": { "label": "Jelszó", - "requirement": { + "suggestion": { "length": "Tartalmazzon legalább 8 karaktert", "lowercase": "Tartalmazzon kisbetűt", "uppercase": "Tartalmazzon nagybetűt", diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index de53009f1..2420ee584 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -132,7 +132,7 @@ }, "password": { "label": "Password", - "requirement": { + "suggestion": { "length": "Include almeno 8 caratteri", "lowercase": "Include lettera minuscola", "uppercase": "Include lettera maiuscola", diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index aacb952c6..aa945ed76 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -132,7 +132,7 @@ }, "password": { "label": "パスワード", - "requirement": { + "suggestion": { "length": "少なくとも 8 文字以上を含む", "lowercase": "小文字を含む", "uppercase": "大文字を含む", diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 6a8639ffd..41e90f781 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -132,7 +132,7 @@ }, "password": { "label": "비밀번호", - "requirement": { + "suggestion": { "length": "", "lowercase": "소문자 포함", "uppercase": "대문자 포함", diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 6811b62e5..b3b258b83 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -132,7 +132,7 @@ }, "password": { "label": "Slaptažodis", - "requirement": { + "suggestion": { "length": "", "lowercase": "", "uppercase": "", diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index e3afee8da..1e9594c8c 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -132,7 +132,7 @@ }, "password": { "label": "Parole", - "requirement": { + "suggestion": { "length": "", "lowercase": "Ietver mazo burtu", "uppercase": "Ietver lielo burtu", diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index bf16ada09..91cdf8a9c 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -132,7 +132,7 @@ }, "password": { "label": "Wachtwoord", - "requirement": { + "suggestion": { "length": "Bevat ten minste 8 tekens", "lowercase": "Inclusief kleine letter", "uppercase": "Inclusief hoofdletter", diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 4badb0f64..e591f6798 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -132,7 +132,7 @@ }, "password": { "label": "Passord", - "requirement": { + "suggestion": { "length": "Inkluderer minst 8 tegn", "lowercase": "Inkluderer liten bokstav", "uppercase": "Inkluderer stor bokstav", diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index ead2f3653..771b6b915 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -132,7 +132,7 @@ }, "password": { "label": "Hasło", - "requirement": { + "suggestion": { "length": "Zawiera co najmniej 8 znaków", "lowercase": "Zawiera małą literę", "uppercase": "Zawiera wielką literę", diff --git a/packages/translation/src/lang/pt-br.json b/packages/translation/src/lang/pt-br.json index a436eac1d..c6ac42898 100644 --- a/packages/translation/src/lang/pt-br.json +++ b/packages/translation/src/lang/pt-br.json @@ -132,7 +132,7 @@ }, "password": { "label": "Senha", - "requirement": { + "suggestion": { "length": "Inclua pelo menos 8 caracteres", "lowercase": "Inclui letras minúsculas", "uppercase": "Inclui letras maiúsculas", diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index 5d749ae0d..a57d1fc09 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -132,7 +132,7 @@ }, "password": { "label": "", - "requirement": { + "suggestion": { "length": "", "lowercase": "", "uppercase": "", diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index d4244df64..545c53e5b 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -132,7 +132,7 @@ }, "password": { "label": "Parola", - "requirement": { + "suggestion": { "length": "", "lowercase": "Să includă litere mici", "uppercase": "Să includă litere mari", diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index 2be2fdabb..d85c608d6 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -132,7 +132,7 @@ }, "password": { "label": "Пароль", - "requirement": { + "suggestion": { "length": "Содержит не менее 8 символов", "lowercase": "Содержит строчную букву", "uppercase": "Содержит заглавную букву", diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index bc5796623..30e9bf463 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -132,7 +132,7 @@ }, "password": { "label": "Heslo", - "requirement": { + "suggestion": { "length": "Obsahuje aspoň 8 znakov", "lowercase": "Zahŕňa malé písmeno", "uppercase": "Zahŕňa veľké písmená", diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index 51c078175..11153c158 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -132,7 +132,7 @@ }, "password": { "label": "Geslo", - "requirement": { + "suggestion": { "length": "", "lowercase": "Vključuje male črke", "uppercase": "Vključuje velike tiskane črke", diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index efe9758ca..f00064a74 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -132,7 +132,7 @@ }, "password": { "label": "Lösenord", - "requirement": { + "suggestion": { "length": "Använd minst 8 tecken", "lowercase": "Använd liten bokstav", "uppercase": "Använd stor bokstav", diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index 8b622b9dd..8df25fd60 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -132,7 +132,7 @@ }, "password": { "label": "Şifre", - "requirement": { + "suggestion": { "length": "En az 8 karakter içermeli", "lowercase": "Küçük harf içermeli", "uppercase": "Büyük harf içermeli", diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index 9d08afe64..fb1a0f759 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -132,7 +132,7 @@ }, "password": { "label": "Пароль", - "requirement": { + "suggestion": { "length": "Включає принаймні 8 символів", "lowercase": "Включає малу літеру", "uppercase": "Включає велику літеру", diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index a9466e9c8..4dfa92c1f 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -132,7 +132,7 @@ }, "password": { "label": "Mật khẩu", - "requirement": { + "suggestion": { "length": "Bao gồm ít nhất 8 kí tự", "lowercase": "Bao gồm chữ thường", "uppercase": "Bao gồm chữ in hoa", diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 7d15715a2..b5a3ef06b 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -132,7 +132,7 @@ }, "password": { "label": "密碼", - "requirement": { + "suggestion": { "length": "至少包含 8 個字符", "lowercase": "包含小寫字母", "uppercase": "包含大寫字母", diff --git a/packages/ui/src/components/password-input/password-input.tsx b/packages/ui/src/components/password-input/password-input.tsx index 6164ede02..22d4a75dc 100644 --- a/packages/ui/src/components/password-input/password-input.tsx +++ b/packages/ui/src/components/password-input/password-input.tsx @@ -20,15 +20,15 @@ export const CustomPasswordInput = ({ withPasswordRequirements, ...props }: Cust }; const WithPasswordRequirements = (props: PasswordInputProps) => { - const [value, setValue] = useState(""); + const [password, setPassword] = useState(""); const onChange = (event: ChangeEvent) => { - setValue(event.currentTarget.value); + setPassword(event.currentTarget.value); props.onChange?.(event); }; return ( - + ); diff --git a/packages/ui/src/components/password-input/password-requirement.tsx b/packages/ui/src/components/password-input/password-requirement.tsx index 1e1eca197..99d694d31 100644 --- a/packages/ui/src/components/password-input/password-requirement.tsx +++ b/packages/ui/src/components/password-input/password-requirement.tsx @@ -1,14 +1,19 @@ 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 }) { + const display = getRequirementDisplay(meets); + const Icon = display.Icon; + return ( - - {meets ? ( - - ) : ( - - )} + + {label} diff --git a/packages/ui/src/components/password-input/password-requirements-popover.tsx b/packages/ui/src/components/password-input/password-requirements-popover.tsx index 5d10611f9..33cb30e42 100644 --- a/packages/ui/src/components/password-input/password-requirements-popover.tsx +++ b/packages/ui/src/components/password-input/password-requirements-popover.tsx @@ -1,16 +1,24 @@ import type { PropsWithChildren } 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 { passwordRequirements } from "@homarr/validation/user"; 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 }>) => { + const tPassword = useScopedI18n("user.field.password"); const requirements = useRequirements(); const strength = useStrength(password); const [popoverOpened, setPopoverOpened] = useState(false); + const strengthColor = strengthColorMap.find(({ min }) => strength >= min)?.color ?? "dimmed"; const checks = ( <> {requirements.map((requirement) => ( @@ -19,8 +27,6 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi ); - const color = strength === 100 ? "teal" : strength > 50 ? "yellow" : "red"; - return ( @@ -29,7 +35,10 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi - + + + {tPassword("suggestionsTitle")} + {checks} @@ -37,7 +46,7 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi }; 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) })); }; diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index 626ba2e8b..824567fa6 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -19,24 +19,10 @@ export const passwordRequirements = [ { check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" }, ] satisfies { check: (value: string) => boolean; - value: keyof TranslationObject["user"]["field"]["password"]["requirement"]; + value: keyof TranslationObject["user"]["field"]["password"]["suggestion"]; }[]; -export const userPasswordSchema = z - .string() - .min(8) - .max(255) - .refine( - (value) => { - return passwordRequirements.every((requirement) => requirement.check(value)); - }, - { - params: createCustomErrorParams({ - key: "passwordRequirements", - params: {}, - }), - }, - ); +export const userPasswordSchema = z.string().min(8).max(255); const addConfirmPasswordRefinement = < TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>,