mirror of https://github.com/buster-so/buster.git
password validation check
This commit is contained in:
parent
e5e56ab01d
commit
62d13729fb
|
@ -287,13 +287,9 @@ const LoginOptions: React.FC<{
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
tabIndex={5}
|
tabIndex={5}
|
||||||
/>
|
/>
|
||||||
{signUpFlow && (
|
|
||||||
<div className="absolute top-0 right-1.5 flex h-full items-center">
|
|
||||||
<PolicyCheck password={password} show={signUpFlow} onCheckChange={setPasswordCheck} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{signUpFlow && (
|
{signUpFlow && (
|
||||||
|
<>
|
||||||
<Input
|
<Input
|
||||||
value={password2}
|
value={password2}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
|
@ -307,6 +303,16 @@ const LoginOptions: React.FC<{
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
tabIndex={6}
|
tabIndex={6}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{password && (
|
||||||
|
<PolicyCheck
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
password2={password2}
|
||||||
|
onChangePolicyCheck={setPasswordCheck}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col space-y-0.5">
|
<div className="flex flex-col space-y-0.5">
|
||||||
|
@ -315,10 +321,6 @@ const LoginOptions: React.FC<{
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PolicyCheck
|
|
||||||
password={password}
|
|
||||||
show={signUpFlow && disableSubmitButton && !!password}
|
|
||||||
placement="top">
|
|
||||||
<Button
|
<Button
|
||||||
size={'tall'}
|
size={'tall'}
|
||||||
block={true}
|
block={true}
|
||||||
|
@ -329,7 +331,6 @@ const LoginOptions: React.FC<{
|
||||||
tabIndex={7}>
|
tabIndex={7}>
|
||||||
{!signUpFlow ? `Sign in` : `Sign up`}
|
{!signUpFlow ? `Sign in` : `Sign up`}
|
||||||
</Button>
|
</Button>
|
||||||
</PolicyCheck>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-2 pt-0">
|
<div className="flex flex-col gap-y-2 pt-0">
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { PolicyCheck } from './PolicyCheck';
|
|
||||||
import { fn } from '@storybook/test';
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'Features/Auth/PolicyCheck',
|
|
||||||
component: PolicyCheck,
|
|
||||||
parameters: {
|
|
||||||
layout: 'centered'
|
|
||||||
},
|
|
||||||
tags: ['autodocs'],
|
|
||||||
argTypes: {
|
|
||||||
password: { control: 'text' },
|
|
||||||
show: { control: 'boolean' },
|
|
||||||
placement: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['top', 'right', 'bottom', 'left']
|
|
||||||
},
|
|
||||||
onCheckChange: { action: 'onCheckChange' }
|
|
||||||
}
|
|
||||||
} satisfies Meta<typeof PolicyCheck>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof PolicyCheck>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
password: '',
|
|
||||||
show: true,
|
|
||||||
placement: 'left',
|
|
||||||
onCheckChange: fn()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ValidPassword: Story = {
|
|
||||||
args: {
|
|
||||||
password: 'Test123!@#',
|
|
||||||
show: true,
|
|
||||||
placement: 'left',
|
|
||||||
onCheckChange: fn()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InvalidPassword: Story = {
|
|
||||||
args: {
|
|
||||||
password: 'weak',
|
|
||||||
show: true,
|
|
||||||
placement: 'left',
|
|
||||||
onCheckChange: fn()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DifferentPlacement: Story = {
|
|
||||||
args: {
|
|
||||||
password: 'Test123!@#',
|
|
||||||
show: true,
|
|
||||||
placement: 'right',
|
|
||||||
onCheckChange: fn()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithCustomChildren: Story = {
|
|
||||||
args: {
|
|
||||||
password: 'Test123!@#',
|
|
||||||
show: true,
|
|
||||||
placement: 'left',
|
|
||||||
onCheckChange: fn(),
|
|
||||||
children: <span className="text-blue-500">Custom trigger element</span>
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -3,14 +3,34 @@ import React, { useEffect, useMemo } from 'react';
|
||||||
import { Text } from '@/components/ui/typography';
|
import { Text } from '@/components/ui/typography';
|
||||||
import { Popover, PopoverProps } from '@/components/ui/popover/Popover';
|
import { Popover, PopoverProps } from '@/components/ui/popover/Popover';
|
||||||
import { Button } from '@/components/ui/buttons/Button';
|
import { Button } from '@/components/ui/buttons/Button';
|
||||||
|
import { validate } from 'email-validator';
|
||||||
|
|
||||||
|
const PasswordCheckItem: React.FC<{
|
||||||
|
passwordGood: boolean;
|
||||||
|
text: string;
|
||||||
|
}> = ({ passwordGood, text }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{passwordGood ? (
|
||||||
|
<div className="text-success-foreground">
|
||||||
|
<CircleCheck />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-danger-foreground">
|
||||||
|
<CircleXmark />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Text size="sm">{text}</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const PolicyCheck: React.FC<{
|
export const PolicyCheck: React.FC<{
|
||||||
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
show: boolean;
|
password2: string | undefined;
|
||||||
onCheckChange?: (value: boolean) => void;
|
onChangePolicyCheck: (passed: boolean) => void;
|
||||||
children?: React.ReactNode;
|
}> = ({ email, password, password2, onChangePolicyCheck }) => {
|
||||||
placement?: 'top' | 'right' | 'bottom' | 'left';
|
|
||||||
}> = ({ password, show, onCheckChange, children, placement = 'left' }) => {
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const containsNumber = /\d/;
|
const containsNumber = /\d/;
|
||||||
const containsSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
|
const containsSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
|
||||||
|
@ -32,6 +52,10 @@ export const PolicyCheck: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
|
{
|
||||||
|
text: 'Email is valid',
|
||||||
|
check: validate(email)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Contains a number',
|
text: 'Contains a number',
|
||||||
check: passwordGood.numberCheck
|
check: passwordGood.numberCheck
|
||||||
|
@ -51,90 +75,39 @@ export const PolicyCheck: React.FC<{
|
||||||
{
|
{
|
||||||
text: 'Is at least 8 characters long',
|
text: 'Is at least 8 characters long',
|
||||||
check: passwordGood.passwordLengthCheck
|
check: passwordGood.passwordLengthCheck
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Passwords match',
|
||||||
|
check: password === password2 || password2 === undefined
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [password]);
|
}, [password, email, password2]);
|
||||||
|
|
||||||
const allCompleted = useMemo(() => {
|
const percentageCompleted = useMemo(() => {
|
||||||
return items.every((item) => item.check);
|
const numberOfChecks = items.length;
|
||||||
|
const numberOfChecksCompleted = items.filter((item) => item.check).length;
|
||||||
|
return (numberOfChecksCompleted / numberOfChecks) * 100;
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show && onCheckChange) {
|
onChangePolicyCheck(percentageCompleted === 100);
|
||||||
onCheckChange(allCompleted);
|
}, [percentageCompleted, onChangePolicyCheck]);
|
||||||
}
|
|
||||||
}, [show, allCompleted, onCheckChange]);
|
|
||||||
|
|
||||||
const PasswordCheck: React.FC<{
|
|
||||||
passwordGood: boolean;
|
|
||||||
text: string;
|
|
||||||
}> = ({ passwordGood, text }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{passwordGood ? (
|
|
||||||
<div className="text-success-foreground">
|
|
||||||
<CircleCheck />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-danger-foreground">
|
|
||||||
<CircleXmark />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Text size="sm">{text}</Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sideMemo: PopoverProps['side'] = useMemo(() => {
|
|
||||||
switch (placement) {
|
|
||||||
case 'top':
|
|
||||||
return 'top';
|
|
||||||
case 'right':
|
|
||||||
return 'right';
|
|
||||||
case 'bottom':
|
|
||||||
return 'bottom';
|
|
||||||
case 'left':
|
|
||||||
return 'left';
|
|
||||||
}
|
|
||||||
}, [placement]);
|
|
||||||
|
|
||||||
const alignMemo: PopoverProps['align'] = useMemo(() => {
|
|
||||||
switch (placement) {
|
|
||||||
case 'top':
|
|
||||||
return 'start';
|
|
||||||
case 'right':
|
|
||||||
return 'end';
|
|
||||||
case 'bottom':
|
|
||||||
return 'start';
|
|
||||||
case 'left':
|
|
||||||
return 'end';
|
|
||||||
}
|
|
||||||
}, [placement]);
|
|
||||||
|
|
||||||
if (!show) return children;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<div className="animate-in fade-in-0 flex flex-col gap-y-1 duration-300">
|
||||||
side={sideMemo}
|
<div className="mx-1.5 h-1 rounded-full bg-gray-200">
|
||||||
align={alignMemo}
|
<div
|
||||||
content={
|
className="bg-primary h-1 rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${percentageCompleted}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-y-1 p-1.5">
|
<div className="flex flex-col gap-y-1 p-1.5">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<PasswordCheck key={index} passwordGood={item.check} text={item.text} />
|
<PasswordCheckItem key={index} passwordGood={item.check} text={item.text} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}>
|
</div>
|
||||||
{!children ? (
|
|
||||||
<Button
|
|
||||||
variant={'ghost'}
|
|
||||||
type="button"
|
|
||||||
size={'small'}
|
|
||||||
prefix={allCompleted ? <CircleCheck /> : <CircleInfo />}></Button>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -106,12 +106,14 @@ export const ResetPasswordForm: React.FC<{
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PolicyCheck
|
<PolicyCheck
|
||||||
placement="top"
|
|
||||||
password={password}
|
password={password}
|
||||||
show={!!password}
|
password2={password2}
|
||||||
onCheckChange={(v) => {
|
email={email || ''}
|
||||||
|
onChangePolicyCheck={(v) => {
|
||||||
setGoodPassword(v);
|
setGoodPassword(v);
|
||||||
}}>
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
variant="black"
|
variant="black"
|
||||||
|
@ -120,7 +122,6 @@ export const ResetPasswordForm: React.FC<{
|
||||||
onClick={handleResetPassword}>
|
onClick={handleResetPassword}>
|
||||||
Reset Password
|
Reset Password
|
||||||
</Button>
|
</Button>
|
||||||
</PolicyCheck>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue