mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-13 11:09:29 -05:00
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 <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
f1cc434e49
commit
38021d2026
72
apps/web/components/auth/IsPasswordValid.tsx
Normal file
72
apps/web/components/auth/IsPasswordValid.tsx
Normal file
@@ -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<Validation[]>(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 (
|
||||
<span className="flex h-5 w-5 items-center justify-center">
|
||||
<i className="inline-block h-2 w-2 rounded-full bg-slate-700"></i>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return <CheckIcon className="h-5 w-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-2 text-left text-slate-700 sm:text-sm">
|
||||
<ul>
|
||||
{validations.map((validation, index) => (
|
||||
<li key={index}>
|
||||
<div className="flex items-center">
|
||||
{renderIcon(validation.state)}
|
||||
{validation.label}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 = () => {
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="*******"
|
||||
aria-placeholder="password"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { PasswordInput } from "@formbricks/ui";
|
||||
import { createUser } from "@/lib/users/users";
|
||||
import { XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
@@ -8,6 +9,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
import { GithubButton } from "./GithubButton";
|
||||
import { GoogleButton } from "@/components/auth/GoogleButton";
|
||||
import IsPasswordValid from "@/components/auth/IsPasswordValid";
|
||||
|
||||
export const SignupForm = () => {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -17,8 +19,12 @@ export const SignupForm = () => {
|
||||
const nameRef = useRef<HTMLInputElement>(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<HTMLFormElement>(null);
|
||||
const [password, setPassword] = useState<string|null>(null)
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
|
||||
const checkFormValidity = () => {
|
||||
// If all fields are filled, enable the button
|
||||
@@ -110,16 +118,17 @@ export const SignupForm = () => {
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={password ? password : ""}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
{process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && (
|
||||
@@ -131,10 +140,12 @@ export const SignupForm = () => {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<IsPasswordValid password={password} setIsValid={setIsValid} />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault()
|
||||
if (!showLogin) {
|
||||
setShowLogin(true);
|
||||
setButtonEnabled(false);
|
||||
@@ -147,7 +158,7 @@ export const SignupForm = () => {
|
||||
variant="darkCTA"
|
||||
className="w-full justify-center"
|
||||
loading={signingUp}
|
||||
disabled={!isButtonEnabled}>
|
||||
disabled={formRef.current ? (!isButtonEnabled || !isValid): !isButtonEnabled}>
|
||||
Continue with Email
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
42
packages/ui/components/PasswordInput.tsx
Normal file
42
packages/ui/components/PasswordInput.tsx
Normal file
@@ -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<React.InputHTMLAttributes<HTMLInputElement>, "type"> {}
|
||||
|
||||
const PasswordInput = ({ className, ...rest }: PasswordInputProps) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword((prevShowPassword) => !prevShowPassword);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("absolute top-1/2 right-3 transform -translate-y-1/2")}
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-slate-400 " />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-slate-400 " />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { PasswordInput };
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user