feat: add captcha (#2613)

* feat: captcha

* add borderRadius

* code perf
This commit is contained in:
papapatrick
2024-09-05 11:46:51 +08:00
committed by GitHub
parent d6233cd7b1
commit 3bcc3430fb
9 changed files with 215 additions and 93 deletions

View File

@@ -2,12 +2,14 @@ export enum UserAuthTypeEnum {
register = 'register',
findPassword = 'findPassword',
wxLogin = 'wxLogin',
bindNotification = 'bindNotification'
bindNotification = 'bindNotification',
captcha = 'captcha'
}
export const userAuthTypeMap = {
[UserAuthTypeEnum.register]: 'register',
[UserAuthTypeEnum.findPassword]: 'findPassword',
[UserAuthTypeEnum.wxLogin]: 'wxLogin',
[UserAuthTypeEnum.bindNotification]: 'bindNotification'
[UserAuthTypeEnum.bindNotification]: 'bindNotification',
[UserAuthTypeEnum.captcha]: 'captcha'
};

View File

@@ -647,8 +647,7 @@
"success": "Start syncing"
}
},
"training": {
}
"training": {}
},
"data": {
"Auxiliary Data": "Auxiliary data",
@@ -1257,6 +1256,7 @@
"auth": {
"Sending Code": "Sending"
},
"captcha_placeholder": "Please enter the verification code",
"inform": {
"System message": "System message"
},

View File

@@ -647,8 +647,7 @@
"success": "开始同步"
}
},
"training": {
}
"training": {}
},
"data": {
"Auxiliary Data": "辅助数据",
@@ -1248,6 +1247,7 @@
},
"user": {
"Avatar": "头像",
"captcha_placeholder": "请输入验证码",
"Go laf env": "点击前往 {{env}} 获取 PAT 凭证。",
"Laf account course": "查看绑定 laf 账号教程。",
"Laf account intro": "绑定你的 laf 账号后,你将可以在工作流中使用 laf 模块,实现在线编写代码。",

View File

@@ -0,0 +1,64 @@
import { getCaptchaPic } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { Box, Button, Input, Image, ModalBody, ModalFooter } from '@chakra-ui/react';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
const SendCodeAuthModal = ({
username,
type,
onClose
}: {
username: string;
type: UserAuthTypeEnum;
onClose: () => void;
}) => {
const { t } = useTranslation();
const [captchaInput, setCaptchaInput] = useState('');
const { codeSending, sendCode } = useSendCode();
const {
data,
loading,
runAsync: getCaptcha
} = useRequest2(() => getCaptchaPic(username), { manual: false });
return (
<MyModal isOpen={true} isLoading={loading}>
<ModalBody pt={8}>
<Image
borderRadius={'md'}
w={'100%'}
h={'200px'}
_hover={{ cursor: 'pointer' }}
mb={8}
onClick={getCaptcha}
src={data?.captchaImage}
alt="captcha"
/>
<Input
placeholder={t('common:support.user.captcha_placeholder')}
value={captchaInput}
onChange={(e) => setCaptchaInput(e.target.value)}
/>
</ModalBody>
<ModalFooter gap={2}>
<Button isLoading={codeSending} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button
isLoading={codeSending}
onClick={async () => {
await sendCode({ username, type, captcha: captchaInput });
onClose();
}}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default SendCodeAuthModal;

View File

@@ -1,5 +1,14 @@
import React, { useCallback } from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button, HStack } from '@chakra-ui/react';
import {
ModalBody,
Box,
Flex,
Input,
ModalFooter,
Button,
HStack,
useDisclosure
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
@@ -9,6 +18,8 @@ import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SendCodeAuthModal from '@/components/support/user/safe/SendCodeAuthModal';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
type FormType = {
account: string;
@@ -27,7 +38,11 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
});
const account = watch('account');
const verifyCode = watch('verifyCode');
const {
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => {
return updateNotificationAccount(data);
@@ -42,16 +57,13 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
}
);
const { sendCodeText, sendCode, codeCountDown } = useSendCode();
const { sendCodeText, codeCountDown } = useSendCode();
const onclickSendCode = useCallback(async () => {
const check = await trigger('account');
if (!check) return;
sendCode({
username: getValues('account'),
type: 'bindNotification'
});
}, [getValues, sendCode, trigger]);
onOpenCodeAuthModal();
}, [onOpenCodeAuthModal, trigger]);
const placeholder = feConfigs?.bind_notification_method
?.map((item) => {
@@ -65,68 +77,77 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
.join('/');
return (
<MyModal
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('common:user.Notification Receive')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('user:notification.Bind Notification Pipe Hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
placeholder={placeholder}
></Input>
<>
<MyModal
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('common:user.Notification Receive')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('user:notification.Bind Notification Pipe Hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
placeholder={placeholder}
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('user:password.verification_code')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('user:password.code_required')}
></Input>
<Box
position={'absolute'}
right={2}
zIndex={1}
fontSize={'sm'}
{...(codeCountDown > 0
? {
color: 'myGray.500'
}
: {
color: 'primary.700',
cursor: 'pointer',
onClick: onclickSendCode
})}
>
{sendCodeText}
</Box>
</Flex>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('user:password.verification_code')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('user:password.code_required')}
></Input>
<Box
position={'absolute'}
right={2}
zIndex={1}
fontSize={'sm'}
{...(codeCountDown > 0
? {
color: 'myGray.500'
}
: {
color: 'primary.700',
cursor: 'pointer',
onClick: onclickSendCode
})}
>
{sendCodeText}
</Box>
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
{openCodeAuthModal && (
<SendCodeAuthModal
onClose={onCloseCodeAuthModal}
username={getValues('account')}
type={UserAuthTypeEnum.bindNotification}
/>
)}
</>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useState, Dispatch, useCallback } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { FormControl, Box, Input, Button, useDisclosure } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postFindPassword } from '@/web/support/user/api';
@@ -8,6 +8,8 @@ import type { ResLogin } from '@/global/support/api/userRes.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import SendCodeAuthModal from '@/components/support/user/safe/SendCodeAuthModal';
interface Props {
setPageType: Dispatch<`${LoginPageTypeEnum}`>;
loginSuccess: (e: ResLogin) => void;
@@ -34,16 +36,17 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
mode: 'onBlur'
});
const { codeSending, sendCodeText, sendCode, codeCountDown } = useSendCode();
const { sendCodeText, codeCountDown } = useSendCode();
const {
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const onclickSendCode = useCallback(async () => {
const check = await trigger('username');
if (!check) return;
sendCode({
username: getValues('username'),
type: 'findPassword'
});
}, [getValues, sendCode, trigger]);
onOpenCodeAuthModal();
}, [onOpenCodeAuthModal, trigger]);
const [requesting, setRequesting] = useState(false);
const placeholder = feConfigs?.find_password_method
@@ -200,6 +203,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{t('user:password.to_login')}
</Box>
</Box>
{openCodeAuthModal && (
<SendCodeAuthModal
onClose={onCloseCodeAuthModal}
username={getValues('username')}
type={UserAuthTypeEnum.findPassword}
/>
)}
</>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useState, Dispatch, useCallback } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { FormControl, Box, Input, Button, useDisclosure } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postRegister } from '@/web/support/user/api';
@@ -11,6 +11,9 @@ import { emptyTemplates } from '@/web/core/app/templates';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import SendCodeAuthModal from '@/components/support/user/safe/SendCodeAuthModal';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
interface Props {
loginSuccess: (e: ResLogin) => void;
setPageType: Dispatch<`${LoginPageTypeEnum}`>;
@@ -37,17 +40,18 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
} = useForm<RegisterType>({
mode: 'onBlur'
});
const { sendCodeText, sendCode, codeCountDown } = useSendCode();
const {
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const { sendCodeText, codeCountDown } = useSendCode();
const onclickSendCode = useCallback(async () => {
const check = await trigger('username');
if (!check) return;
sendCode({
username: getValues('username'),
type: 'register'
});
}, [getValues, sendCode, trigger]);
onOpenCodeAuthModal();
}, [onOpenCodeAuthModal, trigger]);
const [requesting, setRequesting] = useState(false);
@@ -215,6 +219,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{t('user:register.to_login')}
</Box>
</Box>
{openCodeAuthModal && (
<SendCodeAuthModal
onClose={onCloseCodeAuthModal}
username={getValues('username')}
type={UserAuthTypeEnum.register}
/>
)}
</>
);
};

View File

@@ -15,6 +15,7 @@ export const sendAuthCode = (data: {
username: string;
type: `${UserAuthTypeEnum}`;
googleToken: string;
captcha: string;
}) => POST(`/proApi/support/user/inform/sendAuthCode`, data);
export const getTokenLogin = () =>
@@ -82,3 +83,8 @@ export const getWXLoginQR = () =>
export const getWXLoginResult = (code: string) =>
GET<ResLogin>(`/proApi/support/user/account/login/wx/getResult`, { code });
export const getCaptchaPic = (username: string) =>
GET<{
captchaImage: string;
}>('/proApi/support/user/account/captcha', { username });

View File

@@ -12,10 +12,18 @@ export const useSendCode = () => {
const [codeCountDown, setCodeCountDown] = useState(0);
const { runAsync: sendCode, loading: codeSending } = useRequest2(
async ({ username, type }: { username: string; type: `${UserAuthTypeEnum}` }) => {
async ({
username,
type,
captcha
}: {
username: string;
type: `${UserAuthTypeEnum}`;
captcha: string;
}) => {
if (codeCountDown > 0) return;
const googleToken = await getClientToken(feConfigs.googleClientVerKey);
await sendAuthCode({ username, type, googleToken });
await sendAuthCode({ username, type, googleToken, captcha });
setCodeCountDown(60);
timer = setInterval(() => {