From fc448460fd8616a3e6e8a42ee7d31a18e8299459 Mon Sep 17 00:00:00 2001 From: Manel BELHADJ Date: Sun, 22 Dec 2024 11:26:58 +0100 Subject: [PATCH 1/3] feat: integrate `zxcvbn-ts` for password strength evaluation in sign-in and reset password forms --- package.json | 5 ++- pnpm-lock.yaml | 32 +++++++++++++++++ .../app/PasswordStrength/PasswordStrength.tsx | 6 ++-- src/routes/_unauthenticated/signin.tsx | 34 +++++++++++++++++-- src/utils/usePasswordStrength.ts | 27 +++++++++++++++ 5 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 src/utils/usePasswordStrength.ts diff --git a/package.json b/package.json index 06c3cc9..17e752c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,9 @@ "@tiptap/extension-typography": "^2.10.3", "@tiptap/pm": "^2.10.3", "@tiptap/react": "^2.10.3", + "@zxcvbn-ts/core": "^3.0.4", + "@zxcvbn-ts/language-common": "^3.0.4", + "@zxcvbn-ts/language-en": "^3.0.2", "convex": "^1.17.3", "convex-helpers": "^0.1.67", "lucide-react": "^0.468.0", @@ -131,4 +134,4 @@ "vitest": "^2.1.8" }, "packageManager": "pnpm@9.11.0" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09960b4..6a45e76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,15 @@ importers: '@tiptap/react': specifier: ^2.10.3 version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 + '@zxcvbn-ts/language-common': + specifier: ^3.0.4 + version: 3.0.4 + '@zxcvbn-ts/language-en': + specifier: ^3.0.2 + version: 3.0.2 convex: specifier: ^1.17.3 version: 1.17.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -2577,6 +2586,15 @@ packages: '@vitest/utils@2.1.8': resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + + '@zxcvbn-ts/language-common@3.0.4': + resolution: {integrity: sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==} + + '@zxcvbn-ts/language-en@3.0.2': + resolution: {integrity: sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -3261,6 +3279,10 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -8023,6 +8045,14 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 + '@zxcvbn-ts/core@3.0.4': + dependencies: + fastest-levenshtein: 1.0.16 + + '@zxcvbn-ts/language-common@3.0.4': {} + + '@zxcvbn-ts/language-en@3.0.2': {} + abbrev@1.1.1: optional: true @@ -8848,6 +8878,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fastest-levenshtein@1.0.16: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 diff --git a/src/components/app/PasswordStrength/PasswordStrength.tsx b/src/components/app/PasswordStrength/PasswordStrength.tsx index c04bc33..9e51473 100644 --- a/src/components/app/PasswordStrength/PasswordStrength.tsx +++ b/src/components/app/PasswordStrength/PasswordStrength.tsx @@ -140,9 +140,9 @@ export function PasswordStrength({ )} {warning && {warning}} - {suggestions && ( + {suggestions && suggestions.length > 0 && ( -

+ To fix this:

-

+
)} diff --git a/src/routes/_unauthenticated/signin.tsx b/src/routes/_unauthenticated/signin.tsx index fef3317..f1934de 100644 --- a/src/routes/_unauthenticated/signin.tsx +++ b/src/routes/_unauthenticated/signin.tsx @@ -1,4 +1,4 @@ -import { Logo } from "@/components/app"; +import { Logo, PasswordStrength } from "@/components/app"; import { AnimateChangeInHeight, Banner, @@ -14,8 +14,10 @@ import { Tooltip, TooltipTrigger, } from "@/components/common"; +import { usePasswordStrength } from "@/utils/usePasswordStrength"; import { useAuthActions } from "@convex-dev/auth/react"; import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; + import { ConvexError } from "convex/values"; import { ChevronLeft } from "lucide-react"; import { useState } from "react"; @@ -38,6 +40,7 @@ const SignIn = () => { const [error, setError] = useState(null); const navigate = useNavigate({ from: "/signin" }); const isClosed = process.env.NODE_ENV === "production"; + const passwordState = usePasswordStrength(password); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -137,7 +140,20 @@ const SignIn = () => { value={password} onChange={setPassword} /> -

@@ -179,6 +195,7 @@ const ForgotPassword = ({ const [step, setStep] = useState<"forgot" | { email: string }>("forgot"); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); + const passwordState = usePasswordStrength(newPassword); return step === "forgot" ? (

-
diff --git a/src/utils/usePasswordStrength.ts b/src/utils/usePasswordStrength.ts new file mode 100644 index 0000000..3ecf35f --- /dev/null +++ b/src/utils/usePasswordStrength.ts @@ -0,0 +1,27 @@ +import { type ZxcvbnResult, zxcvbnAsync } from "@zxcvbn-ts/core"; +import { zxcvbnOptions } from "@zxcvbn-ts/core"; +import * as zxcvbnCommonPackage from "@zxcvbn-ts/language-common"; +import * as zxcvbnEnPackage from "@zxcvbn-ts/language-en"; +import { useDeferredValue, useEffect, useState } from "react"; + +const options = { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + translations: zxcvbnEnPackage.translations, +}; +zxcvbnOptions.setOptions(options); + +export function usePasswordStrength(password: string) { + const [result, setResult] = useState(); + const deferredPassword = useDeferredValue(password); + + useEffect(() => { + if (password) { + zxcvbnAsync(deferredPassword).then((response) => setResult(response)); + } + }, [password, deferredPassword]); + + return result; +} From f0602907caccd72393b6b82738a12ec61976da9c Mon Sep 17 00:00:00 2001 From: Manel BELHADJ Date: Sun, 22 Dec 2024 11:57:34 +0100 Subject: [PATCH 2/3] fix: formatting issue --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17e752c..c69899b 100644 --- a/package.json +++ b/package.json @@ -134,4 +134,4 @@ "vitest": "^2.1.8" }, "packageManager": "pnpm@9.11.0" -} \ No newline at end of file +} From aeb3772b008cb48e62ad93d7debbd69d27fd2098 Mon Sep 17 00:00:00 2001 From: Manel BELHADJ Date: Sun, 22 Dec 2024 12:00:48 +0100 Subject: [PATCH 3/3] feat: add zxcvbn dictionaries lazy loading to improve performance --- src/utils/usePasswordStrength.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/utils/usePasswordStrength.ts b/src/utils/usePasswordStrength.ts index 3ecf35f..5708771 100644 --- a/src/utils/usePasswordStrength.ts +++ b/src/utils/usePasswordStrength.ts @@ -1,17 +1,26 @@ import { type ZxcvbnResult, zxcvbnAsync } from "@zxcvbn-ts/core"; import { zxcvbnOptions } from "@zxcvbn-ts/core"; -import * as zxcvbnCommonPackage from "@zxcvbn-ts/language-common"; -import * as zxcvbnEnPackage from "@zxcvbn-ts/language-en"; import { useDeferredValue, useEffect, useState } from "react"; -const options = { - dictionary: { - ...zxcvbnCommonPackage.dictionary, - ...zxcvbnEnPackage.dictionary, - }, - translations: zxcvbnEnPackage.translations, +const loadOptions = async () => { + const zxcvbnCommonPackage = await import( + /* webpackChunkName: "zxcvbnCommonPackage" */ "@zxcvbn-ts/language-common" + ); + const zxcvbnEnPackage = await import( + /* webpackChunkName: "zxcvbnEnPackage" */ "@zxcvbn-ts/language-en" + ); + return { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + translations: zxcvbnEnPackage.translations, + }; }; -zxcvbnOptions.setOptions(options); + +loadOptions().then((options) => { + zxcvbnOptions.setOptions(options); +}); export function usePasswordStrength(password: string) { const [result, setResult] = useState();