perf: captcha code (#2620)

* perf:  captcha code

* perf: dockerfile
This commit is contained in:
Archer
2024-09-05 13:41:11 +08:00
committed by GitHub
parent 5ed89130ef
commit 7fed4d697f
9 changed files with 179 additions and 226 deletions

View File

@@ -647,7 +647,8 @@
"success": "Start syncing" "success": "Start syncing"
} }
}, },
"training": {} "training": {
}
}, },
"data": { "data": {
"Auxiliary Data": "Auxiliary data", "Auxiliary Data": "Auxiliary data",
@@ -1084,13 +1085,15 @@
"default_reply": "Default reply", "default_reply": "Default reply",
"error": { "error": {
"Create failed": "Create failed", "Create failed": "Create failed",
"code_error": "Code error",
"fileNotFound": "File not found~", "fileNotFound": "File not found~",
"inheritPermissionError": "Inherit permission Error", "inheritPermissionError": "Inherit permission Error",
"missingParams": "Insufficient parameters", "missingParams": "Insufficient parameters",
"team": { "team": {
"overSize": "Team members exceed the limit" "overSize": "Team members exceed the limit"
}, },
"upload_file_error_filename": "{{name}} upload failed" "upload_file_error_filename": "{{name}} upload failed",
"username_empty": "Account cannot be empty"
}, },
"extraction_results": "Extract results", "extraction_results": "Extract results",
"field_name": "Name", "field_name": "Name",

View File

@@ -647,7 +647,8 @@
"success": "开始同步" "success": "开始同步"
} }
}, },
"training": {} "training": {
}
}, },
"data": { "data": {
"Auxiliary Data": "辅助数据", "Auxiliary Data": "辅助数据",
@@ -1084,13 +1085,15 @@
"default_reply": "默认回复", "default_reply": "默认回复",
"error": { "error": {
"Create failed": "创建失败", "Create failed": "创建失败",
"code_error": "验证码错误",
"fileNotFound": "文件找不到了~", "fileNotFound": "文件找不到了~",
"inheritPermissionError": "权限继承错误", "inheritPermissionError": "权限继承错误",
"missingParams": "参数缺失", "missingParams": "参数缺失",
"team": { "team": {
"overSize": "团队成员超出上限" "overSize": "团队成员超出上限"
}, },
"upload_file_error_filename": "{{name}} 上传失败" "upload_file_error_filename": "{{name}} 上传失败",
"username_empty": "账号不能为空"
}, },
"extraction_results": "提取结果", "extraction_results": "提取结果",
"field_name": "字段名", "field_name": "字段名",
@@ -1247,7 +1250,6 @@
}, },
"user": { "user": {
"Avatar": "头像", "Avatar": "头像",
"captcha_placeholder": "请输入验证码",
"Go laf env": "点击前往 {{env}} 获取 PAT 凭证。", "Go laf env": "点击前往 {{env}} 获取 PAT 凭证。",
"Laf account course": "查看绑定 laf 账号教程。", "Laf account course": "查看绑定 laf 账号教程。",
"Laf account intro": "绑定你的 laf 账号后,你将可以在工作流中使用 laf 模块,实现在线编写代码。", "Laf account intro": "绑定你的 laf 账号后,你将可以在工作流中使用 laf 模块,实现在线编写代码。",
@@ -1257,6 +1259,7 @@
"auth": { "auth": {
"Sending Code": "正在发送" "Sending Code": "正在发送"
}, },
"captcha_placeholder": "请输入验证码",
"inform": { "inform": {
"System message": "系统消息" "System message": "系统消息"
}, },

View File

@@ -75,8 +75,8 @@ COPY ./projects/app/data /app/data
RUN chown -R nextjs:nodejs /app/data RUN chown -R nextjs:nodejs /app/data
ENV NODE_ENV production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000 ENV PORT=3000
EXPOSE 3000 EXPOSE 3000

View File

@@ -1,58 +1,72 @@
import { getCaptchaPic } from '@/web/support/user/api'; import { getCaptchaPic } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import { Button, Input, Image, ModalBody, ModalFooter, Skeleton } from '@chakra-ui/react';
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 MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useForm } from 'react-hook-form';
const SendCodeAuthModal = ({ const SendCodeAuthModal = ({
username, username,
type, onClose,
onClose onSending,
onSendCode
}: { }: {
username: string; username: string;
type: UserAuthTypeEnum;
onClose: () => void; onClose: () => void;
onSending: boolean;
onSendCode: (params_0: { username: string; captcha: string }) => Promise<void>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [captchaInput, setCaptchaInput] = useState('');
const { codeSending, sendCode } = useSendCode(); const { register, handleSubmit } = useForm({
defaultValues: {
code: ''
}
});
const { const {
data, data,
loading, loading,
runAsync: getCaptcha runAsync: getCaptcha
} = useRequest2(() => getCaptchaPic(username), { manual: false }); } = useRequest2(() => getCaptchaPic(username), { manual: false });
return ( return (
<MyModal isOpen={true} isLoading={loading}> <MyModal isOpen={true}>
<ModalBody pt={8}> <ModalBody pt={8}>
<Image <Skeleton
borderRadius={'md'} minH="200px"
w={'100%'} isLoaded={!loading}
h={'200px'} fadeDuration={2}
_hover={{ cursor: 'pointer' }} display={'flex'}
mb={8} justifyContent={'center'}
onClick={getCaptcha} my={1}
src={data?.captchaImage} >
alt="captcha" <Image
/> borderRadius={'md'}
<Input w={'100%'}
placeholder={t('common:support.user.captcha_placeholder')} h={'200px'}
value={captchaInput} _hover={{ cursor: 'pointer' }}
onChange={(e) => setCaptchaInput(e.target.value)} mb={8}
/> onClick={getCaptcha}
src={data?.captchaImage}
alt=""
/>
</Skeleton>
<Input placeholder={t('common:support.user.captcha_placeholder')} {...register('code')} />
</ModalBody> </ModalBody>
<ModalFooter gap={2}> <ModalFooter gap={2}>
<Button isLoading={codeSending} variant={'whiteBase'} onClick={onClose}> <Button isLoading={onSending} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')} {t('common:common.Cancel')}
</Button> </Button>
<Button <Button
isLoading={codeSending} isLoading={onSending}
onClick={async () => { onClick={handleSubmit(({ code }) => {
await sendCode({ username, type, captcha: captchaInput }); return onSendCode({ username, captcha: code }).then(() => {
onClose(); onClose();
}} });
})}
> >
{t('common:common.Confirm')} {t('common:common.Confirm')}
</Button> </Button>

View File

@@ -18,8 +18,6 @@ import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore'; 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 = { type FormType = {
account: string; account: string;
@@ -30,7 +28,8 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { initUserInfo } = useUserStore(); const { initUserInfo } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { register, handleSubmit, trigger, getValues, watch } = useForm<FormType>({
const { register, handleSubmit, watch } = useForm<FormType>({
defaultValues: { defaultValues: {
account: '', account: '',
verifyCode: '' verifyCode: ''
@@ -38,11 +37,7 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
}); });
const account = watch('account'); const account = watch('account');
const verifyCode = watch('verifyCode'); const verifyCode = watch('verifyCode');
const {
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const { runAsync: onSubmit, loading: isLoading } = useRequest2( const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => { (data: FormType) => {
return updateNotificationAccount(data); return updateNotificationAccount(data);
@@ -57,13 +52,7 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
} }
); );
const { sendCodeText, codeCountDown } = useSendCode(); const { SendCodeBox } = useSendCode({ type: 'bindNotification' });
const onclickSendCode = useCallback(async () => {
const check = await trigger('account');
if (!check) return;
onOpenCodeAuthModal();
}, [onOpenCodeAuthModal, trigger]);
const placeholder = feConfigs?.bind_notification_method const placeholder = feConfigs?.bind_notification_method
?.map((item) => { ?.map((item) => {
@@ -107,23 +96,7 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
{...register('verifyCode', { required: true })} {...register('verifyCode', { required: true })}
placeholder={t('user:password.code_required')} placeholder={t('user:password.code_required')}
></Input> ></Input>
<Box <SendCodeBox username={account} />
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> </Flex>
</ModalBody> </ModalBody>
@@ -140,13 +113,6 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
</Button> </Button>
</ModalFooter> </ModalFooter>
</MyModal> </MyModal>
{openCodeAuthModal && (
<SendCodeAuthModal
onClose={onCloseCodeAuthModal}
username={getValues('account')}
type={UserAuthTypeEnum.bindNotification}
/>
)}
</> </>
); );
}; };

View File

@@ -8,8 +8,8 @@ import type { ResLogin } from '@/global/support/api/userRes.d';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import SendCodeAuthModal from '@/components/support/user/safe/SendCodeAuthModal';
interface Props { interface Props {
setPageType: Dispatch<`${LoginPageTypeEnum}`>; setPageType: Dispatch<`${LoginPageTypeEnum}`>;
loginSuccess: (e: ResLogin) => void; loginSuccess: (e: ResLogin) => void;
@@ -30,25 +30,15 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
register, register,
handleSubmit, handleSubmit,
getValues, getValues,
trigger, watch,
formState: { errors } formState: { errors }
} = useForm<RegisterType>({ } = useForm<RegisterType>({
mode: 'onBlur' mode: 'onBlur'
}); });
const username = watch('username');
const { sendCodeText, codeCountDown } = useSendCode(); const { SendCodeBox } = useSendCode({ type: 'findPassword' });
const {
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const onclickSendCode = useCallback(async () => {
const check = await trigger('username');
if (!check) return;
onOpenCodeAuthModal();
}, [onOpenCodeAuthModal, trigger]);
const [requesting, setRequesting] = useState(false);
const placeholder = feConfigs?.find_password_method const placeholder = feConfigs?.find_password_method
?.map((item) => { ?.map((item) => {
switch (item) { switch (item) {
@@ -62,30 +52,23 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
}) })
.join('/'); .join('/');
const onclickFindPassword = useCallback( const { runAsync: onclickFindPassword, loading: requesting } = useRequest2(
async ({ username, code, password }: RegisterType) => { async ({ username, code, password }: RegisterType) => {
setRequesting(true); loginSuccess(
try { await postFindPassword({
loginSuccess( username,
await postFindPassword({ code,
username, password
code, })
password );
}) toast({
); status: 'success',
toast({ title: t('user:password.retrieved')
title: t('user:password.retrieved'), });
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || t('user:password.change_error'),
status: 'error'
});
}
setRequesting(false);
}, },
[loginSuccess, toast] {
refreshDeps: [loginSuccess, t, toast]
}
); );
return ( return (
@@ -131,23 +114,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
required: t('user:password.code_required') required: t('user:password.code_required')
})} })}
></Input> ></Input>
<Box <SendCodeBox username={username} />
position={'absolute'}
right={3}
zIndex={1}
fontSize={'sm'}
{...(codeCountDown > 0
? {
color: 'myGray.500'
}
: {
color: 'primary.700',
cursor: 'pointer',
onClick: onclickSendCode
})}
>
{sendCodeText}
</Box>
</FormControl> </FormControl>
<FormControl mt={6} isInvalid={!!errors.password}> <FormControl mt={6} isInvalid={!!errors.password}>
<Input <Input
@@ -203,13 +170,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{t('user:password.to_login')} {t('user:password.to_login')}
</Box> </Box>
</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 React, { Dispatch } from 'react';
import { FormControl, Box, Input, Button, useDisclosure } from '@chakra-ui/react'; import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postRegister } from '@/web/support/user/api'; import { postRegister } from '@/web/support/user/api';
@@ -12,8 +12,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; 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 { interface Props {
loginSuccess: (e: ResLogin) => void; loginSuccess: (e: ResLogin) => void;
setPageType: Dispatch<`${LoginPageTypeEnum}`>; setPageType: Dispatch<`${LoginPageTypeEnum}`>;
@@ -35,63 +34,47 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
register, register,
handleSubmit, handleSubmit,
getValues, getValues,
trigger, watch,
formState: { errors } formState: { errors }
} = useForm<RegisterType>({ } = useForm<RegisterType>({
mode: 'onBlur' mode: 'onBlur'
}); });
const { const username = watch('username');
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const { sendCodeText, codeCountDown } = useSendCode();
const onclickSendCode = useCallback(async () => { const { SendCodeBox } = useSendCode({ type: 'register' });
const check = await trigger('username');
if (!check) return;
onOpenCodeAuthModal();
}, [onOpenCodeAuthModal, trigger]);
const [requesting, setRequesting] = useState(false); const { runAsync: onclickRegister, loading: requesting } = useRequest2(
const onclickRegister = useCallback(
async ({ username, password, code }: RegisterType) => { async ({ username, password, code }: RegisterType) => {
setRequesting(true); loginSuccess(
try { await postRegister({
loginSuccess( username,
await postRegister({ code,
username, password,
code, inviterId: localStorage.getItem('inviterId') || undefined
password, })
inviterId: localStorage.getItem('inviterId') || undefined );
})
); toast({
toast({ status: 'success',
title: t('user:register.success'), title: t('user:register.success')
status: 'success' });
});
// auto register template app // auto register template app
setTimeout(() => { setTimeout(() => {
Object.entries(emptyTemplates).map(([type, emptyTemplate]) => { Object.entries(emptyTemplates).map(([type, emptyTemplate]) => {
postCreateApp({ postCreateApp({
avatar: emptyTemplate.avatar, avatar: emptyTemplate.avatar,
name: t(emptyTemplate.name as any), name: t(emptyTemplate.name as any),
modules: emptyTemplate.nodes, modules: emptyTemplate.nodes,
edges: emptyTemplate.edges, edges: emptyTemplate.edges,
type: type as AppTypeEnum type: type as AppTypeEnum
});
}); });
}, 100);
} catch (error: any) {
toast({
title: error.message || t('user:register.error'),
status: 'error'
}); });
} }, 100);
setRequesting(false);
}, },
[loginSuccess, t, toast] {
refreshDeps: [loginSuccess, t, toast]
}
); );
const placeholder = feConfigs?.register_method const placeholder = feConfigs?.register_method
@@ -148,23 +131,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
required: t('user:password.code_required') required: t('user:password.code_required')
})} })}
></Input> ></Input>
<Box <SendCodeBox username={username} />
position={'absolute'}
right={3}
zIndex={1}
fontSize={'sm'}
{...(codeCountDown > 0
? {
color: 'myGray.500'
}
: {
color: 'primary.700',
cursor: 'pointer',
onClick: onclickSendCode
})}
>
{sendCodeText}
</Box>
</FormControl> </FormControl>
<FormControl mt={6} isInvalid={!!errors.password}> <FormControl mt={6} isInvalid={!!errors.password}>
<Input <Input
@@ -219,13 +186,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{t('user:register.to_login')} {t('user:register.to_login')}
</Box> </Box>
</Box> </Box>
{openCodeAuthModal && (
<SendCodeAuthModal
onClose={onCloseCodeAuthModal}
username={getValues('username')}
type={UserAuthTypeEnum.register}
/>
)}
</> </>
); );
}; };

View File

@@ -87,4 +87,4 @@ export const getWXLoginResult = (code: string) =>
export const getCaptchaPic = (username: string) => export const getCaptchaPic = (username: string) =>
GET<{ GET<{
captchaImage: string; captchaImage: string;
}>('/proApi/support/user/account/captcha', { username }); }>('/proApi/support/user/account/captcha/getImgCaptcha', { username });

View File

@@ -4,26 +4,24 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { Box, BoxProps, useDisclosure } from '@chakra-ui/react';
import SendCodeAuthModal from '@/components/support/user/safe/SendCodeAuthModal';
import { useMemoizedFn } from 'ahooks';
import { useToast } from '@fastgpt/web/hooks/useToast';
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
export const useSendCode = () => { export const useSendCode = ({ type }: { type: `${UserAuthTypeEnum}` }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { toast } = useToast();
const [codeCountDown, setCodeCountDown] = useState(0); const [codeCountDown, setCodeCountDown] = useState(0);
const { runAsync: sendCode, loading: codeSending } = useRequest2( const { runAsync: sendCode, loading: codeSending } = useRequest2(
async ({ async ({ username, captcha }: { username: string; captcha: string }) => {
username,
type,
captcha
}: {
username: string;
type: `${UserAuthTypeEnum}`;
captcha: string;
}) => {
if (codeCountDown > 0) return; if (codeCountDown > 0) return;
const googleToken = await getClientToken(feConfigs.googleClientVerKey); const googleToken = await getClientToken(feConfigs.googleClientVerKey);
await sendAuthCode({ username, type, googleToken, captcha }); await sendAuthCode({ username, type, googleToken, captcha });
setCodeCountDown(60); setCodeCountDown(60);
timer = setInterval(() => { timer = setInterval(() => {
@@ -38,7 +36,7 @@ export const useSendCode = () => {
{ {
successToast: t('user:password.code_sended'), successToast: t('user:password.code_sended'),
errorToast: t('user:password.code_send_error'), errorToast: t('user:password.code_send_error'),
refreshDeps: [codeCountDown, feConfigs?.googleClientVerKey] refreshDeps: [codeCountDown, type, feConfigs?.googleClientVerKey]
} }
); );
@@ -53,11 +51,60 @@ export const useSendCode = () => {
return t('user:password.get_code'); return t('user:password.get_code');
}, [codeCountDown, codeSending, t]); }, [codeCountDown, codeSending, t]);
const {
isOpen: openCodeAuthModal,
onOpen: onOpenCodeAuthModal,
onClose: onCloseCodeAuthModal
} = useDisclosure();
const SendCodeBox = useMemoizedFn(({ username, ...styles }: BoxProps & { username: string }) => {
return (
<>
<Box
position={'absolute'}
right={3}
zIndex={1}
fontSize={'sm'}
{...styles}
{...(codeCountDown > 0
? {
color: 'myGray.500'
}
: {
color: 'primary.700',
cursor: 'pointer',
onClick: () => {
if (!username) {
toast({
status: 'warning',
title: t('common:error.username_empty')
});
} else {
onOpenCodeAuthModal();
}
}
})}
>
{sendCodeText}
</Box>
{openCodeAuthModal && (
<SendCodeAuthModal
onClose={onCloseCodeAuthModal}
username={username}
onSending={codeSending}
onSendCode={sendCode}
/>
)}
</>
);
});
return { return {
codeSending, codeSending,
sendCode, sendCode,
sendCodeText, sendCodeText,
codeCountDown codeCountDown,
SendCodeBox
}; };
}; };