Skip to content

Commit

Permalink
fix(desktop): twice captcha
Browse files Browse the repository at this point in the history
  • Loading branch information
xudaotutou committed Feb 21, 2025
1 parent af8a321 commit d9f7bb4
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 83 deletions.
127 changes: 84 additions & 43 deletions frontend/desktop/src/components/signin/Captcha/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import request from '@/services/request';
import { useConfigStore } from '@/stores/config';
import useScriptStore from '@/stores/script';
import { ApiResp } from '@/types';
import { Box, Button, ButtonProps, Link, LinkProps } from '@chakra-ui/react';
import { delay } from 'lodash';
import React, {
ReactElement,
forwardRef,
useEffect,
useId,
useImperativeHandle,
useRef,
useState
} from 'react';
import React, { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
import { v4 } from 'uuid';
import useSms from '../auth/useSms';
import useSmsStateStore from '@/stores/captcha';
import useCustomError from '../auth/useCustomError';
import { useTranslation } from 'next-i18next';
import { I18nCommonKey } from '@/types/i18next';
import { jsonRes } from '@/services/backend/response';
export type TCaptchaInstance = {
getToken: () => Promise<string>;
reset: () => void;
invoke: () => void;
};

const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(props, ref) {
type Props = {
showError: (errorMessage: I18nCommonKey, duration?: number) => void;
};
const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(
{ showError }: Props,
ref
) {
const captchaElementRef = useRef<HTMLDivElement>(null);
const captchaInstanceRef = useRef(null);
const tokenRef = useRef('');
Expand All @@ -28,16 +31,8 @@ const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(props,
ref,
() => {
return {
getToken: async () => {
invoke() {
buttonRef.current?.click();
await new Promise((res) => {
setTimeout(res, 1000);
});
const token = tokenRef.current;
return token;
},
reset() {
tokenRef.current = '';
}
};
},
Expand All @@ -46,13 +41,12 @@ const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(props,
const { captchaIsLoaded } = useScriptStore();
const [buttonId] = useState('captcha_button_pop');
const [captchaId] = useState('captcha_' + v4().slice(0, 8));

const { i18n, t } = useTranslation();
// @ts-ignore
const getInstance = (instance) => {
captchaInstanceRef.current = instance;
};

const onBizResultCallback = () => {};
useEffect(() => {
if (!captchaIsLoaded) return;
const initAliyunCaptchaOptions = {
Expand All @@ -61,27 +55,80 @@ const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(props,
mode: 'popup',
element: '#' + captchaId,
button: '#' + buttonId,
async captchaVerifyCallback(captchaToken: string) {
async captchaVerifyCallback(captchaVerifyParam: string) {
try {
tokenRef.current = captchaToken;
return {
captchaResult: true
};
const state = useSmsStateStore.getState();
const id = state.phoneNumber;
const res = await request.post<
any,
ApiResp<
| {
result: boolean;
code: string;
}
| undefined
>
>('/api/auth/phone/sms', {
id,
captchaVerifyParam
});
console.log(res);
if (res.code === 200) {
if (res.message !== 'successfully') {
return {
captchaResult: true,
bizResult: false
};
} else {
return {
captchaResult: true,
bizResult: true
};
}
} else {
// boolean | undefined
if (res.data?.result !== true)
return {
captchaResult: false
};
else
return {
captchaResult: true,
bizResult: false
};
}
} catch (err) {
tokenRef.current = '';
return {
captchaResult: false
};
// @ts-ignore
if (err?.code === 409 && err?.data?.result === false) {
return {
captchaResult: false,
bizResult: false
};
} else {
return {
captchaResult: true,
bizResult: false
};
}
return;
}
},
onBizResultCallback(bizResult: boolean) {
if (bizResult) {
const state = useSmsStateStore.getState();
state.setRemainTime(60);
} else {
const message = i18n.t('common:get_code_failed') || 'Get code failed';
showError(message);
}
},
onBizResultCallback: onBizResultCallback,
getInstance,
slideStyle: {
width: 360,
height: 40
},
immediate: false,
language: 'cn',
language: i18n.language === 'en' ? 'en' : 'cn',
region: 'cn'
};

Expand All @@ -91,16 +138,10 @@ const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(props,
return () => {
captchaInstanceRef.current = null;
};
}, [captchaIsLoaded]);
}, [captchaIsLoaded, t, i18n.language]);
return (
<>
<Button
ref={buttonRef}
variant={'unstyled'}
hidden
id={buttonId}
{...(props as ButtonProps)}
/>
<Button ref={buttonRef} variant={'unstyled'} hidden id={buttonId} />
<div ref={captchaElementRef} id={captchaId} />
</>
);
Expand Down
71 changes: 39 additions & 32 deletions frontend/desktop/src/components/signin/auth/useSms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ import { getRegionToken } from '@/api/auth';
import { getBaiduId, getInviterId, getUserSemData, sessionConfig } from '@/utils/sessionConfig';
import { I18nCommonKey } from '@/types/i18next';
import { useConfigStore } from '@/stores/config';
import useSmsStateStore from '@/stores/captcha';

export default function useSms({
showError
showError,
invokeCaptcha
}: {
showError: (errorMessage: I18nCommonKey, duration?: number) => void;
invokeCaptcha: () => void;
}) {
const { t } = useTranslation();
const _remainTime = useRef(0);
const router = useRouter();
const { authConfig } = useConfigStore();
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -35,7 +37,7 @@ export default function useSms({
phoneNumber: string;
verifyCode: string;
}>();

const phoneNumber = watch('phoneNumber');
const login = async () => {
const deepSearch = (obj: any): I18nCommonKey => {
if (!obj || typeof obj !== 'object') return 'submit_error';
Expand Down Expand Up @@ -78,56 +80,61 @@ export default function useSms({
}
)();
};
const { remainTime, setRemainTime, setPhoneNumber } = useSmsStateStore();
useEffect(() => {
if (remainTime <= 0) return;
const interval = setInterval(() => {
setRemainTime(remainTime - 1);
}, 1000);
return () => clearInterval(interval);
}, [remainTime]);

const SmsModal = ({
onAfterGetCode,
getCfToken
getCfToken,
invokeCaptcha
}: {
// for cloudflare
getCfToken?: () => Promise<string | undefined>;
onAfterGetCode?: () => void;
// for ali captcha
invokeCaptcha?: () => void;
}) => {
const [remainTime, setRemainTime] = useState(_remainTime.current);

useEffect(() => {
if (remainTime <= 0) return;
const interval = setInterval(() => {
setRemainTime(remainTime - 1);
}, 1000);
return () => clearInterval(interval);
}, [remainTime]);
const [invokeTime, setInvokeTime] = useState(new Date().getTime());
const [invokeTime, setInvokeTime] = useState(new Date().getTime() - 1000);
const getCode: MouseEventHandler = async (e: MouseEvent) => {
e.preventDefault();
if (!(await trigger('phoneNumber'))) {
showError(t('common:invalid_phone_number') || 'Invalid phone number');
return;
}
// throttle
if (new Date().getTime() - invokeTime <= 1000) {
return;
} else {
setInvokeTime(new Date().getTime());
}
const cfToken = await getCfToken?.();
if (authConfig?.captcha.enabled && authConfig.captcha.ali.enabled) {
if (!cfToken) return;
}
setRemainTime(60);
_remainTime.current = 60;
try {
setPhoneNumber(phoneNumber);
invokeCaptcha?.();
} else {
// for cf ornot
const cfToken = await getCfToken?.();
const res = await request.post<any, ApiResp<any>>('/api/auth/phone/sms', {
id: getValues('phoneNumber'),
cfToken
});
if (res.code !== 200 || res.message !== 'successfully') {
throw new Error('Get code failed');
if (authConfig?.captcha.enabled && authConfig.captcha.ali.enabled) {
if (!cfToken) return;
}
setRemainTime(60);
try {
const res = await request.post<any, ApiResp<any>>('/api/auth/phone/sms', {
id: getValues('phoneNumber'),
cfToken
});
if (res.code !== 200 || res.message !== 'successfully') {
throw new Error('Get code failed');
}
} catch (err) {
showError(t('common:get_code_failed') || 'Get code failed');
setRemainTime(0);
}
} catch (err) {
showError(t('common:get_code_failed') || 'Get code failed');
setRemainTime(0);
_remainTime.current = 0;
} finally {
onAfterGetCode?.();
}
};
return (
Expand Down
22 changes: 17 additions & 5 deletions frontend/desktop/src/components/signin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ export default function SigninComponent() {
};
const { Protocol, isAgree, setIsInvalid } = useProtocol(protocol_data!);
const { WechatComponent, login: wechatSubmit } = useWechat();
const { SmsModal, login: smsSubmit, isLoading: smsLoading } = useSms({ showError });
const {
SmsModal,
login: smsSubmit,
isLoading: smsLoading
} = useSms({
showError,
invokeCaptcha() {
captchaRef.current?.invoke();
}
});
const {
PasswordComponent,
pageState,
Expand Down Expand Up @@ -85,13 +94,14 @@ export default function SigninComponent() {
<SmsModal
onAfterGetCode={() => {
turnstileRef.current?.reset();
captchaRef.current?.reset();
}}
getCfToken={async () => {
const token = await captchaRef.current?.getToken();
const turnstiletoken = turnstileRef.current?.getResponse();
const token = turnstileRef.current?.getResponse();
return token;
}}
invokeCaptcha={() => {
captchaRef.current?.invoke();
}}
/>
)
},
Expand Down Expand Up @@ -231,7 +241,9 @@ export default function SigninComponent() {
siteKey={conf.commonConfig?.cfSiteKey}
/>
)}
{conf.authConfig?.captcha.enabled && <HiddenCaptchaComponent ref={captchaRef} />}
{conf.authConfig?.captcha.enabled && (
<HiddenCaptchaComponent ref={captchaRef} showError={showError} />
)}
<Button
variant={'unstyled'}
background="linear-gradient(90deg, #000000 0%, rgba(36, 40, 44, 0.9) 98.29%)"
Expand Down
1 change: 1 addition & 0 deletions frontend/desktop/src/pages/api/auth/phone/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
filterPhoneParams,
sendPhoneCodeGuard
} from '@/services/backend/middleware/sms';
import { jsonRes } from '@/services/backend/response';
import { sendPhoneCodeSvc } from '@/services/backend/svc/sms';
import { enablePhoneSms } from '@/services/enable';
import { NextApiRequest, NextApiResponse } from 'next';
Expand Down
6 changes: 3 additions & 3 deletions frontend/desktop/src/services/backend/middleware/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,18 @@ export const filterCaptcha = async (
await Promise.resolve(next());
return;
}
const { cfToken: captchaVerifyParam } = req.body as { cfToken?: string };
const { captchaVerifyParam } = req.body as { captchaVerifyParam?: string };
if (!captchaVerifyParam)
return jsonRes(res, {
message: 'captchaToken is invalid',
message: 'captchaVerifyParam is invalid',
code: 400
});
const result = await captchaReq({
captchaVerifyParam
});
if (!result?.verifyResult)
return jsonRes(res, {
message: 'captchaToken is invalid',
message: 'captchaVerifyParam is invalid',
data: {
result: !!result?.verifyResult,
code: result?.verifyCode || ''
Expand Down
Loading

0 comments on commit d9f7bb4

Please sign in to comment.