From 01d785b9a90adb38d3cd9cd6516f230ead84620a Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 20 Jun 2024 16:58:26 +0530 Subject: [PATCH] [WEB-1681] chore: handled password strength validation and improved the acceptable char (#4891) * chore: handled password validation on onboarding screen * chore: updated is password focused --- web/app/accounts/reset-password/page.tsx | 6 +- web/app/accounts/set-password/page.tsx | 6 +- web/app/profile/security/page.tsx | 13 +- .../account/auth-forms/password.tsx | 8 +- .../account/password-strength-meter.tsx | 121 ++++++---- .../components/onboarding/profile-setup.tsx | 206 +++++++++--------- web/helpers/password.helper.ts | 75 ++++++- 7 files changed, 259 insertions(+), 176 deletions(-) diff --git a/web/app/accounts/reset-password/page.tsx b/web/app/accounts/reset-password/page.tsx index 14bcf27d6..43f001abe 100644 --- a/web/app/accounts/reset-password/page.tsx +++ b/web/app/accounts/reset-password/page.tsx @@ -20,7 +20,7 @@ import { authErrorHandler, } from "@/helpers/authentication.helper"; import { API_BASE_URL } from "@/helpers/common.helper"; -import { getPasswordStrength } from "@/helpers/password.helper"; +import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // wrappers import { AuthenticationWrapper } from "@/lib/wrappers"; // services @@ -83,7 +83,7 @@ export default function ResetPasswordPage() { const isButtonDisabled = useMemo( () => !!resetFormData.password && - getPasswordStrength(resetFormData.password) >= 3 && + getPasswordStrength(resetFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID && resetFormData.password === resetFormData.confirm_password ? false : true, @@ -187,7 +187,7 @@ export default function ResetPasswordPage() { /> )} - {isPasswordInputFocused && } +
- {isPasswordInputFocused && } +
) : ( passwordFormData.password.length > 0 && - (getPasswordStrength(passwordFormData.password) < 3 || isPasswordInputFocused) && ( - + getPasswordStrength(passwordFormData.password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && ( + ) ); @@ -137,7 +137,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`} onSubmit={(event) => { event.preventDefault(); // Prevent form from submitting by default - if (getPasswordStrength(passwordFormData.password) >= 3) { + if (getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID) { setIsSubmitting(true); captureEvent(mode === EAuthModes.SIGN_IN ? SIGN_IN_WITH_PASSWORD : SIGN_UP_WITH_PASSWORD); event.currentTarget.submit(); // Manually submit the form if the condition is met diff --git a/web/core/components/account/password-strength-meter.tsx b/web/core/components/account/password-strength-meter.tsx index 7383b1e11..342f77efb 100644 --- a/web/core/components/account/password-strength-meter.tsx +++ b/web/core/components/account/password-strength-meter.tsx @@ -1,67 +1,94 @@ -// icons -import { CircleCheck } from "lucide-react"; +"use client"; + +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-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; - text = "Password is weak"; - textColor = `text-[#DC3E42]`; - } 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/web/core/components/onboarding/profile-setup.tsx b/web/core/components/onboarding/profile-setup.tsx index 722fbfa2f..26b6ceeb3 100644 --- a/web/core/components/onboarding/profile-setup.tsx +++ b/web/core/components/onboarding/profile-setup.tsx @@ -17,7 +17,7 @@ import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding // constants import { USER_DETAILS, E_ONBOARDING_STEP_1, E_ONBOARDING_STEP_2 } from "@/constants/event-tracker"; // helpers -import { getPasswordStrength } from "@/helpers/password.helper"; +import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useEventTracker, useUser, useUserProfile } from "@/hooks/store"; // services @@ -248,30 +248,30 @@ export const ProfileSetup: React.FC = observer((props) => { }); }; + // derived values const isPasswordAlreadySetup = !user?.is_password_autoset; - const isSignUpUsingMagicCode = user?.last_login_medium === "magic-code"; - - const password = watch("password"); - const confirmPassword = watch("confirm_password"); - const isValidPassword = (password: string, confirmPassword?: string) => - getPasswordStrength(password) >= 3 && password === confirmPassword; + const isValidPassword = useMemo(() => { + const currentPassword = watch("password") || undefined; + const currentConfirmPassword = watch("confirm_password") || undefined; + if (currentPassword) { + if ( + currentPassword === currentConfirmPassword && + getPasswordStrength(currentPassword) === E_PASSWORD_STRENGTH.STRENGTH_VALID + ) { + return true; + } else { + return false; + } + } else { + return true; + } + }, [watch]); // Check for all available fields validation and if password field is available, then checks for password validation (strength + confirmation). // Also handles the condition for optional password i.e if password field is optional it only checks for above validation if it's not empty. const isButtonDisabled = useMemo( - () => - !isSubmitting && - isValid && - (isPasswordAlreadySetup - ? true - : isSignUpUsingMagicCode - ? !!password && isValidPassword(password, confirmPassword) - : !!password - ? isValidPassword(password, confirmPassword) - : true) - ? false - : true, - [isSubmitting, isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword] + () => (!isSubmitting && isValid && (isPasswordAlreadySetup ? true : isValidPassword) ? false : true), + [isSubmitting, isValid, isPasswordAlreadySetup, isValidPassword] ); const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION; @@ -412,94 +412,98 @@ export const ProfileSetup: React.FC = observer((props) => { {errors.last_name && {errors.last_name.message}}
+ + {/* setting up password for the first time */} {!isPasswordAlreadySetup && ( -
- - ( -
- setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - /> - {showPassword.password ? ( - handleShowPassword("password")} + <> +
+ + ( +
+ setIsPasswordInputFocused(true)} + onBlur={() => setIsPasswordInputFocused(false)} /> - ) : ( - handleShowPassword("password")} + {showPassword.password ? ( + handleShowPassword("password")} + /> + ) : ( + handleShowPassword("password")} + /> + )} +
+ )} + /> + +
+
+ + + watch("password") ? (value === watch("password") ? true : "Passwords don't match") : true, + }} + render={({ field: { value, onChange, ref } }) => ( +
+ - )} -
+ {showPassword.retypePassword ? ( + handleShowPassword("retypePassword")} + /> + ) : ( + handleShowPassword("retypePassword")} + /> + )} +
+ )} + /> + {errors.confirm_password && ( + {errors.confirm_password.message} )} - /> - {isPasswordInputFocused && } - {errors.password && {errors.password.message}} -
- )} - {!isPasswordAlreadySetup && ( -
- - value === password || "Passwords don't match", - }} - render={({ field: { value, onChange, ref } }) => ( -
- - {showPassword.retypePassword ? ( - handleShowPassword("retypePassword")} - /> - ) : ( - handleShowPassword("retypePassword")} - /> - )} -
- )} - /> - {errors.confirm_password && ( - {errors.confirm_password.message} - )} -
+
+ )} )} + + {/* user role once the password is set */} {profileSetupStep !== EProfileSetupSteps.USER_DETAILS && ( <>
diff --git a/web/helpers/password.helper.ts b/web/helpers/password.helper.ts index 8d80b3402..dfe9a5c65 100644 --- a/web/helpers/password.helper.ts +++ b/web/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; };