From 38021d2026fdc4d15b7dc98105973c8be9e39eda Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:13:50 +0530 Subject: [PATCH] Add password requirements and password validation (#412) * resolved * made changes * made submit button disabled on failing password checks * removed submitted state and fixed typos * redactored IsPasswordValid Component * removed cursor-pointer from XCircleIcon * added PasswordInput component * made some refactors in PasswordInput component * made eye icon lighter * removed unsed code * fix infinite state updates --------- Co-authored-by: Matthias Nannt --- apps/web/components/auth/IsPasswordValid.tsx | 72 ++++++++++++++++++++ apps/web/components/auth/SigninForm.tsx | 5 +- apps/web/components/auth/SignupForm.tsx | 23 +++++-- packages/ui/components/PasswordInput.tsx | 42 ++++++++++++ packages/ui/index.tsx | 1 + 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 apps/web/components/auth/IsPasswordValid.tsx create mode 100644 packages/ui/components/PasswordInput.tsx diff --git a/apps/web/components/auth/IsPasswordValid.tsx b/apps/web/components/auth/IsPasswordValid.tsx new file mode 100644 index 0000000000..7aedc38460 --- /dev/null +++ b/apps/web/components/auth/IsPasswordValid.tsx @@ -0,0 +1,72 @@ +import { CheckIcon } from "@heroicons/react/24/solid"; +import React, { useState, useEffect } from "react"; + +interface Validation { + label: string; + state: boolean; +} + +const PASSWORD_REGEX = { + UPPER_AND_LOWER: /^(?=.*[A-Z])(?=.*[a-z])/, + NUMBER: /\d/, +}; + +const DEFAULT_VALIDATIONS = [ + { label: "Mix of uppercase and lowercase", state: false }, + { label: "Minimum 8 characters long", state: false }, + { label: "Contain at least 1 number", state: false }, +]; + +export default function IsPasswordValid({ + password, + setIsValid, +}: { + password: string | null; + setIsValid: (isValid: boolean) => void; +}) { + const [validations, setValidations] = useState(DEFAULT_VALIDATIONS); + + useEffect(() => { + let newValidations = [...DEFAULT_VALIDATIONS]; + if (password) { + newValidations = checkValidation(newValidations, 0, PASSWORD_REGEX.UPPER_AND_LOWER.test(password)); + newValidations = checkValidation(newValidations, 1, password.length >= 8); + newValidations = checkValidation(newValidations, 2, PASSWORD_REGEX.NUMBER.test(password)); + } + setIsValid(newValidations.every((validation) => validation.state === true)); + setValidations(newValidations); + + function checkValidation(prevValidations, index: number, state: boolean) { + const updatedValidations = [...prevValidations]; + updatedValidations[index].state = state; + return updatedValidations; + } + }, [password, setIsValid]); + + const renderIcon = (state: boolean) => { + if (state === false) { + return ( + + + + ); + } else { + return ; + } + }; + + return ( +
+
    + {validations.map((validation, index) => ( +
  • +
    + {renderIcon(validation.state)} + {validation.label} +
    +
  • + ))} +
+
+ ); +} diff --git a/apps/web/components/auth/SigninForm.tsx b/apps/web/components/auth/SigninForm.tsx index 4a75a96c68..2abbaa03a4 100644 --- a/apps/web/components/auth/SigninForm.tsx +++ b/apps/web/components/auth/SigninForm.tsx @@ -1,7 +1,7 @@ "use client"; import { GoogleButton } from "@/components/auth/GoogleButton"; -import { Button } from "@formbricks/ui"; +import { Button, PasswordInput } from "@formbricks/ui"; import { XCircleIcon } from "@heroicons/react/24/solid"; import { signIn } from "next-auth/react"; import Link from "next/dist/client/link"; @@ -64,10 +64,9 @@ export const SigninForm = () => { - { const searchParams = useSearchParams(); @@ -17,8 +19,12 @@ export const SignupForm = () => { const nameRef = useRef(null); const handleSubmit = async (e: any) => { - setSigningUp(true); e.preventDefault(); + + if(!isValid){ + return + } + setSigningUp(true); try { await createUser( e.target.elements.name.value, @@ -42,6 +48,8 @@ export const SignupForm = () => { const [isButtonEnabled, setButtonEnabled] = useState(true); const [isPasswordFocused, setIsPasswordFocused] = useState(false); const formRef = useRef(null); + const [password, setPassword] = useState(null) + const [isValid, setIsValid] = useState(false) const checkFormValidity = () => { // If all fields are filled, enable the button @@ -110,16 +118,17 @@ export const SignupForm = () => { - setPassword(e.target.value)} autoComplete="current-password" placeholder="*******" aria-placeholder="password" onFocus={() => setIsPasswordFocused(true)} required - className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" + className="focus:border-brand focus:ring-brand block w-full rounded-md shadow-sm sm:text-sm" /> {process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && ( @@ -131,10 +140,12 @@ export const SignupForm = () => { )} + )} diff --git a/packages/ui/components/PasswordInput.tsx b/packages/ui/components/PasswordInput.tsx new file mode 100644 index 0000000000..d5143acf90 --- /dev/null +++ b/packages/ui/components/PasswordInput.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useState } from "react"; +import { cn } from "@formbricks/lib/cn"; +import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; + +export interface PasswordInputProps extends Omit, "type"> {} + +const PasswordInput = ({ className, ...rest }: PasswordInputProps) => { + const [showPassword, setShowPassword] = useState(false); + + const togglePasswordVisibility = () => { + setShowPassword((prevShowPassword) => !prevShowPassword); + }; + + return ( +
+ + +
+ ); +}; + +export { PasswordInput }; + diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index d62a7e3cbf..0096c1f233 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -27,6 +27,7 @@ export { export { Editor, AddVariablesDropdown } from "./components/editor"; export { ErrorComponent } from "./components/ErrorComponent"; export { Input } from "./components/Input"; +export { PasswordInput } from "./components/PasswordInput"; export { Label } from "./components/Label"; export { PageTitle } from "./components/PageTitle"; export { Popover, PopoverTrigger, PopoverContent } from "./components/Popover";