diff --git a/admin/components/common/password-strength-meter.tsx b/admin/components/common/password-strength-meter.tsx index 004a927b2..342f77efb 100644 --- a/admin/components/common/password-strength-meter.tsx +++ b/admin/components/common/password-strength-meter.tsx @@ -1,69 +1,94 @@ "use client"; +import { FC, useMemo } from "react"; +// import { CircleCheck } from "lucide-react"; // helpers -import { CircleCheck } from "lucide-react"; import { cn } from "@/helpers/common.helper"; -import { getPasswordStrength } from "@/helpers/password.helper"; -// icons +import { + E_PASSWORD_STRENGTH, + // PASSWORD_CRITERIA, + getPasswordStrength, +} from "@/helpers/password.helper"; -type Props = { +type TPasswordStrengthMeter = { password: string; + isFocused?: boolean; }; -export const PasswordStrengthMeter: React.FC = (props: Props) => { - const { password } = props; +export const PasswordStrengthMeter: FC = (props) => { + const { password, isFocused = false } = props; + // derived values + const strength = useMemo(() => getPasswordStrength(password), [password]); + const strengthBars = useMemo(() => { + switch (strength) { + case E_PASSWORD_STRENGTH.EMPTY: { + return { + bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Please enter your password.", + textColor: "text-custom-text-100", + }; + } + case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: { + return { + bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Password length should me more than 8 characters.", + textColor: "text-red-500", + }; + } + case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: { + return { + bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Password is weak.", + textColor: "text-red-500", + }; + } + case E_PASSWORD_STRENGTH.STRENGTH_VALID: { + return { + bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`], + text: "Password is strong.", + textColor: "text-green-500", + }; + } + default: { + return { + bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Please enter your password.", + textColor: "text-custom-text-100", + }; + } + } + }, [strength]); - const strength = getPasswordStrength(password); - let bars = []; - let text = ""; - let textColor = ""; - - if (password.length === 0) { - bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; - text = "Password requirements"; - } else if (password.length < 8) { - bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; - text = "Password is too short"; - textColor = `text-[#DC3E42]`; - } else if (strength < 3) { - bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`]; - text = "Password is weak"; - textColor = `text-[#FFBA18]`; - } else { - bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`]; - text = "Password is strong"; - textColor = `text-[#3E9B4F]`; - } - - const criteria = [ - { label: "Min 8 characters", isValid: password.length >= 8 }, - { label: "Min 1 upper-case letter", isValid: /[A-Z]/.test(password) }, - { label: "Min 1 number", isValid: /\d/.test(password) }, - { label: "Min 1 special character", isValid: /[!@#$%^&*]/.test(password) }, - ]; + const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true; + if (!isPasswordMeterVisible) return <>; return ( -
-
- {bars.map((color, index) => ( -
- ))} +
+
+
+ {strengthBars?.bars.map((color, index) => ( +
+ ))} +
+
+ {strengthBars?.text} +
-

{text}

-
- {criteria.map((criterion, index) => ( + + {/*
+ {PASSWORD_CRITERIA.map((criteria) => (
- {criterion.label} + {criteria.label}
))} -
+
*/}
); }; diff --git a/admin/components/instance/setup-form.tsx b/admin/components/instance/setup-form.tsx index aa2350894..ec3919896 100644 --- a/admin/components/instance/setup-form.tsx +++ b/admin/components/instance/setup-form.tsx @@ -10,7 +10,7 @@ import { Button, Checkbox, Input, Spinner } from "@plane/ui"; import { Banner, PasswordStrengthMeter } from "@/components/common"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; -import { getPasswordStrength } from "@/helpers/password.helper"; +import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // services import { AuthService } from "@/services/auth.service"; @@ -121,7 +121,7 @@ export const InstanceSetupForm: FC = (props) => { formData.first_name && formData.email && formData.password && - getPasswordStrength(formData.password) >= 3 && + getPasswordStrength(formData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID && formData.password === formData.confirm_password ? false : true, @@ -271,7 +271,7 @@ export const InstanceSetupForm: FC = (props) => { {errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (

{errorData.message}

)} - {isPasswordInputFocused && } +
diff --git a/admin/helpers/password.helper.ts b/admin/helpers/password.helper.ts index 8d80b3402..dfe9a5c65 100644 --- a/admin/helpers/password.helper.ts +++ b/admin/helpers/password.helper.ts @@ -1,16 +1,67 @@ import zxcvbn from "zxcvbn"; -export const isPasswordCriteriaMet = (password: string) => { - const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)]; +export enum E_PASSWORD_STRENGTH { + EMPTY = "empty", + LENGTH_NOT_VALID = "length_not_valid", + STRENGTH_NOT_VALID = "strength_not_valid", + STRENGTH_VALID = "strength_valid", +} - return criteria.every((criterion) => criterion); -}; - -export const getPasswordStrength = (password: string) => { - if (password.length === 0) return 0; - if (password.length < 8) return 1; - if (!isPasswordCriteriaMet(password)) return 2; - - const result = zxcvbn(password); - return result.score; +const PASSWORD_MIN_LENGTH = 8; +// const PASSWORD_NUMBER_REGEX = /\d/; +// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/; +// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/; + +export const PASSWORD_CRITERIA = [ + { + key: "min_8_char", + label: "Min 8 characters", + isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH, + }, + // { + // key: "min_1_upper_case", + // label: "Min 1 upper-case letter", + // isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password), + // }, + // { + // key: "min_1_number", + // label: "Min 1 number", + // isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password), + // }, + // { + // key: "min_1_special_char", + // label: "Min 1 special character", + // isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password), + // }, +]; + +export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => { + let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY; + + if (!password || password === "" || password.length <= 0) { + return passwordStrength; + } + + if (password.length >= PASSWORD_MIN_LENGTH) { + passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID; + } else { + passwordStrength = E_PASSWORD_STRENGTH.LENGTH_NOT_VALID; + return passwordStrength; + } + + const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every( + (criterion) => criterion + ); + const passwordStrengthScore = zxcvbn(password).score; + + if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) { + passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID; + return passwordStrength; + } + + if (passwordCriteriaValidation === true && passwordStrengthScore >= 3) { + passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_VALID; + } + + return passwordStrength; }; diff --git a/space/core/components/account/auth-forms/password.tsx b/space/core/components/account/auth-forms/password.tsx index f2ef95d98..c3a5e9c31 100644 --- a/space/core/components/account/auth-forms/password.tsx +++ b/space/core/components/account/auth-forms/password.tsx @@ -8,7 +8,7 @@ import { Button, Input, Spinner } from "@plane/ui"; import { PasswordStrengthMeter } from "@/components/account"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; -import { getPasswordStrength } from "@/helpers/password.helper"; +import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // services import { AuthService } from "@/services/auth.service"; // types @@ -67,8 +67,8 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { const passwordSupport = passwordFormData.password.length > 0 && mode === EAuthModes.SIGN_UP && - (getPasswordStrength(passwordFormData.password) < 3 || isPasswordInputFocused) && ( - + getPasswordStrength(passwordFormData.password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && ( + ); const isButtonDisabled = useMemo( @@ -76,7 +76,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { !isSubmitting && !!passwordFormData.password && (mode === EAuthModes.SIGN_UP - ? getPasswordStrength(passwordFormData.password) >= 3 && + ? getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID && passwordFormData.password === passwordFormData.confirm_password : true) ? false diff --git a/space/core/components/account/helpers/password-strength-meter.tsx b/space/core/components/account/helpers/password-strength-meter.tsx index c12d78421..342f77efb 100644 --- a/space/core/components/account/helpers/password-strength-meter.tsx +++ b/space/core/components/account/helpers/password-strength-meter.tsx @@ -1,69 +1,94 @@ "use client"; -// icons -import { CircleCheck } from "lucide-react"; +import { FC, useMemo } from "react"; +// import { CircleCheck } from "lucide-react"; // helpers import { cn } from "@/helpers/common.helper"; -import { getPasswordStrength } from "@/helpers/password.helper"; +import { + E_PASSWORD_STRENGTH, + // PASSWORD_CRITERIA, + getPasswordStrength, +} from "@/helpers/password.helper"; -type Props = { +type TPasswordStrengthMeter = { password: string; + isFocused?: boolean; }; -export const PasswordStrengthMeter: React.FC = (props: Props) => { - const { password } = props; +export const PasswordStrengthMeter: FC = (props) => { + const { password, isFocused = false } = props; + // derived values + const strength = useMemo(() => getPasswordStrength(password), [password]); + const strengthBars = useMemo(() => { + switch (strength) { + case E_PASSWORD_STRENGTH.EMPTY: { + return { + bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Please enter your password.", + textColor: "text-custom-text-100", + }; + } + case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: { + return { + bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Password length should me more than 8 characters.", + textColor: "text-red-500", + }; + } + case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: { + return { + bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Password is weak.", + textColor: "text-red-500", + }; + } + case E_PASSWORD_STRENGTH.STRENGTH_VALID: { + return { + bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`], + text: "Password is strong.", + textColor: "text-green-500", + }; + } + default: { + return { + bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`], + text: "Please enter your password.", + textColor: "text-custom-text-100", + }; + } + } + }, [strength]); - const strength = getPasswordStrength(password); - let bars = []; - let text = ""; - let textColor = ""; - - if (password.length === 0) { - bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; - text = "Password requirements"; - } else if (password.length < 8) { - bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; - text = "Password is too short"; - textColor = `text-[#DC3E42]`; - } else if (strength < 3) { - bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`]; - text = "Password is weak"; - textColor = `text-[#FFBA18]`; - } else { - bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`]; - text = "Password is strong"; - textColor = `text-[#3E9B4F]`; - } - - const criteria = [ - { label: "Min 8 characters", isValid: password.length >= 8 }, - { label: "Min 1 upper-case letter", isValid: /[A-Z]/.test(password) }, - { label: "Min 1 number", isValid: /\d/.test(password) }, - { label: "Min 1 special character", isValid: /[!@#$%^&*]/.test(password) }, - ]; + const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true; + if (!isPasswordMeterVisible) return <>; return ( -
-
- {bars.map((color, index) => ( -
- ))} +
+
+
+ {strengthBars?.bars.map((color, index) => ( +
+ ))} +
+
+ {strengthBars?.text} +
-

{text}

-
- {criteria.map((criterion, index) => ( + + {/*
+ {PASSWORD_CRITERIA.map((criteria) => (
- {criterion.label} + {criteria.label}
))} -
+
*/}
); }; diff --git a/space/helpers/password.helper.ts b/space/helpers/password.helper.ts index 8d80b3402..dfe9a5c65 100644 --- a/space/helpers/password.helper.ts +++ b/space/helpers/password.helper.ts @@ -1,16 +1,67 @@ import zxcvbn from "zxcvbn"; -export const isPasswordCriteriaMet = (password: string) => { - const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)]; +export enum E_PASSWORD_STRENGTH { + EMPTY = "empty", + LENGTH_NOT_VALID = "length_not_valid", + STRENGTH_NOT_VALID = "strength_not_valid", + STRENGTH_VALID = "strength_valid", +} - return criteria.every((criterion) => criterion); -}; - -export const getPasswordStrength = (password: string) => { - if (password.length === 0) return 0; - if (password.length < 8) return 1; - if (!isPasswordCriteriaMet(password)) return 2; - - const result = zxcvbn(password); - return result.score; +const PASSWORD_MIN_LENGTH = 8; +// const PASSWORD_NUMBER_REGEX = /\d/; +// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/; +// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/; + +export const PASSWORD_CRITERIA = [ + { + key: "min_8_char", + label: "Min 8 characters", + isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH, + }, + // { + // key: "min_1_upper_case", + // label: "Min 1 upper-case letter", + // isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password), + // }, + // { + // key: "min_1_number", + // label: "Min 1 number", + // isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password), + // }, + // { + // key: "min_1_special_char", + // label: "Min 1 special character", + // isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password), + // }, +]; + +export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => { + let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY; + + if (!password || password === "" || password.length <= 0) { + return passwordStrength; + } + + if (password.length >= PASSWORD_MIN_LENGTH) { + passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID; + } else { + passwordStrength = E_PASSWORD_STRENGTH.LENGTH_NOT_VALID; + return passwordStrength; + } + + const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every( + (criterion) => criterion + ); + const passwordStrengthScore = zxcvbn(password).score; + + if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) { + passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID; + return passwordStrength; + } + + if (passwordCriteriaValidation === true && passwordStrengthScore >= 3) { + passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_VALID; + } + + return passwordStrength; };