chore: handled password validation on onboarding screen (#4894)

This commit is contained in:
guru_sainath 2024-06-20 17:27:28 +05:30 committed by GitHub
parent 522cdc6873
commit 1b1302dfbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 277 additions and 125 deletions

View file

@ -1,69 +1,94 @@
"use client"; "use client";
import { FC, useMemo } from "react";
// import { CircleCheck } from "lucide-react";
// helpers // helpers
import { CircleCheck } from "lucide-react";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper"; import {
// icons E_PASSWORD_STRENGTH,
// PASSWORD_CRITERIA,
getPasswordStrength,
} from "@/helpers/password.helper";
type Props = { type TPasswordStrengthMeter = {
password: string; password: string;
isFocused?: boolean;
}; };
export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => { export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
const { password } = props; const { password, isFocused = false } = props;
// derived values
const strength = getPasswordStrength(password); const strength = useMemo(() => getPasswordStrength(password), [password]);
let bars = []; const strengthBars = useMemo(() => {
let text = ""; switch (strength) {
let textColor = ""; case E_PASSWORD_STRENGTH.EMPTY: {
return {
if (password.length === 0) { bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; text: "Please enter your password.",
text = "Password requirements"; textColor: "text-custom-text-100",
} 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]`;
} }
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 criteria = [ const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
{ 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) },
];
if (!isPasswordMeterVisible) return <></>;
return ( return (
<div className="w-full"> <div className="w-full space-y-2 pt-2">
<div className="flex w-full gap-1.5"> <div className="space-y-1.5">
{bars.map((color, index) => ( <div className="relative flex items-center gap-2">
<div key={index} className={cn("w-full h-1 rounded-full", color)} /> {strengthBars?.bars.map((color, index) => (
<div key={`${color}-${index}`} className={cn("w-full h-1 rounded-full", color)} />
))} ))}
</div> </div>
<p className={cn("text-xs font-medium py-1", textColor)}>{text}</p> <div className={cn(`text-xs font-medium text-custom-text-100`, strengthBars?.textColor)}>
<div className="flex flex-wrap gap-x-4 gap-y-2"> {strengthBars?.text}
{criteria.map((criterion, index) => ( </div>
</div>
{/* <div className="relative flex flex-wrap gap-x-4 gap-y-2">
{PASSWORD_CRITERIA.map((criteria) => (
<div <div
key={index} key={criteria.key}
className={cn( className={cn(
"flex items-center gap-1 text-xs font-medium", "relative flex items-center gap-1 text-xs",
criterion.isValid ? `text-[#3E9B4F]` : "text-custom-text-400" criteria.isCriteriaValid(password) ? `text-green-500/70` : "text-custom-text-300"
)} )}
> >
<CircleCheck width={14} height={14} /> <CircleCheck width={14} height={14} />
{criterion.label} {criteria.label}
</div> </div>
))} ))}
</div> </div> */}
</div> </div>
); );
}; };

View file

@ -10,7 +10,7 @@ import { Button, Checkbox, Input, Spinner } from "@plane/ui";
import { Banner, PasswordStrengthMeter } from "@/components/common"; import { Banner, PasswordStrengthMeter } from "@/components/common";
// helpers // helpers
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper"; import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper";
// services // services
import { AuthService } from "@/services/auth.service"; import { AuthService } from "@/services/auth.service";
@ -121,7 +121,7 @@ export const InstanceSetupForm: FC = (props) => {
formData.first_name && formData.first_name &&
formData.email && formData.email &&
formData.password && formData.password &&
getPasswordStrength(formData.password) >= 3 && getPasswordStrength(formData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
formData.password === formData.confirm_password formData.password === formData.confirm_password
? false ? false
: true, : true,
@ -271,7 +271,7 @@ export const InstanceSetupForm: FC = (props) => {
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && ( {errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
<p className="px-1 text-xs text-red-500">{errorData.message}</p> <p className="px-1 text-xs text-red-500">{errorData.message}</p>
)} )}
{isPasswordInputFocused && <PasswordStrengthMeter password={formData.password} />} <PasswordStrengthMeter password={formData.password} isFocused={isPasswordInputFocused} />
</div> </div>
<div className="w-full space-y-1"> <div className="w-full space-y-1">

View file

@ -1,16 +1,67 @@
import zxcvbn from "zxcvbn"; import zxcvbn from "zxcvbn";
export const isPasswordCriteriaMet = (password: string) => { export enum E_PASSWORD_STRENGTH {
const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)]; EMPTY = "empty",
LENGTH_NOT_VALID = "length_not_valid",
STRENGTH_NOT_VALID = "strength_not_valid",
STRENGTH_VALID = "strength_valid",
}
return criteria.every((criterion) => criterion); const PASSWORD_MIN_LENGTH = 8;
}; // const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
export const getPasswordStrength = (password: string) => { // const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
if (password.length === 0) return 0;
if (password.length < 8) return 1; export const PASSWORD_CRITERIA = [
if (!isPasswordCriteriaMet(password)) return 2; {
key: "min_8_char",
const result = zxcvbn(password); label: "Min 8 characters",
return result.score; 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;
}; };

View file

@ -8,7 +8,7 @@ import { Button, Input, Spinner } from "@plane/ui";
import { PasswordStrengthMeter } from "@/components/account"; import { PasswordStrengthMeter } from "@/components/account";
// helpers // helpers
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper"; import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper";
// services // services
import { AuthService } from "@/services/auth.service"; import { AuthService } from "@/services/auth.service";
// types // types
@ -67,8 +67,8 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const passwordSupport = passwordFormData.password.length > 0 && const passwordSupport = passwordFormData.password.length > 0 &&
mode === EAuthModes.SIGN_UP && mode === EAuthModes.SIGN_UP &&
(getPasswordStrength(passwordFormData.password) < 3 || isPasswordInputFocused) && ( getPasswordStrength(passwordFormData.password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && (
<PasswordStrengthMeter password={passwordFormData.password} /> <PasswordStrengthMeter password={passwordFormData.password} isFocused={isPasswordInputFocused} />
); );
const isButtonDisabled = useMemo( const isButtonDisabled = useMemo(
@ -76,7 +76,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
!isSubmitting && !isSubmitting &&
!!passwordFormData.password && !!passwordFormData.password &&
(mode === EAuthModes.SIGN_UP (mode === EAuthModes.SIGN_UP
? getPasswordStrength(passwordFormData.password) >= 3 && ? getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
passwordFormData.password === passwordFormData.confirm_password passwordFormData.password === passwordFormData.confirm_password
: true) : true)
? false ? false

View file

@ -1,69 +1,94 @@
"use client"; "use client";
// icons import { FC, useMemo } from "react";
import { CircleCheck } from "lucide-react"; // import { CircleCheck } from "lucide-react";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; 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; password: string;
isFocused?: boolean;
}; };
export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => { export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
const { password } = props; const { password, isFocused = false } = props;
// derived values
const strength = getPasswordStrength(password); const strength = useMemo(() => getPasswordStrength(password), [password]);
let bars = []; const strengthBars = useMemo(() => {
let text = ""; switch (strength) {
let textColor = ""; case E_PASSWORD_STRENGTH.EMPTY: {
return {
if (password.length === 0) { bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`]; text: "Please enter your password.",
text = "Password requirements"; textColor: "text-custom-text-100",
} 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]`;
} }
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 criteria = [ const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
{ 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) },
];
if (!isPasswordMeterVisible) return <></>;
return ( return (
<div className="w-full p-1"> <div className="w-full space-y-2 pt-2">
<div className="flex w-full gap-1.5"> <div className="space-y-1.5">
{bars.map((color, index) => ( <div className="relative flex items-center gap-2">
<div key={index} className={cn("w-full h-1 rounded-full", color)} /> {strengthBars?.bars.map((color, index) => (
<div key={`${color}-${index}`} className={cn("w-full h-1 rounded-full", color)} />
))} ))}
</div> </div>
<p className={cn("text-xs font-medium py-1", textColor)}>{text}</p> <div className={cn(`text-xs font-medium text-custom-text-100`, strengthBars?.textColor)}>
<div className="flex flex-wrap gap-x-4 gap-y-2"> {strengthBars?.text}
{criteria.map((criterion, index) => ( </div>
</div>
{/* <div className="relative flex flex-wrap gap-x-4 gap-y-2">
{PASSWORD_CRITERIA.map((criteria) => (
<div <div
key={index} key={criteria.key}
className={cn( className={cn(
"flex items-center gap-1 text-xs font-medium", "relative flex items-center gap-1 text-xs",
criterion.isValid ? `text-[#3E9B4F]` : "text-custom-text-400" criteria.isCriteriaValid(password) ? `text-green-500/70` : "text-custom-text-300"
)} )}
> >
<CircleCheck width={14} height={14} /> <CircleCheck width={14} height={14} />
{criterion.label} {criteria.label}
</div> </div>
))} ))}
</div> </div> */}
</div> </div>
); );
}; };

View file

@ -1,16 +1,67 @@
import zxcvbn from "zxcvbn"; import zxcvbn from "zxcvbn";
export const isPasswordCriteriaMet = (password: string) => { export enum E_PASSWORD_STRENGTH {
const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)]; EMPTY = "empty",
LENGTH_NOT_VALID = "length_not_valid",
STRENGTH_NOT_VALID = "strength_not_valid",
STRENGTH_VALID = "strength_valid",
}
return criteria.every((criterion) => criterion); const PASSWORD_MIN_LENGTH = 8;
}; // const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
export const getPasswordStrength = (password: string) => { // const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
if (password.length === 0) return 0;
if (password.length < 8) return 1; export const PASSWORD_CRITERIA = [
if (!isPasswordCriteriaMet(password)) return 2; {
key: "min_8_char",
const result = zxcvbn(password); label: "Min 8 characters",
return result.score; 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;
}; };