Skip to content

Commit

Permalink
feat:add captcha (#5389)
Browse files Browse the repository at this point in the history
  • Loading branch information
xudaotutou authored Feb 21, 2025
1 parent 3034e8d commit 09262b5
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 36 deletions.
2 changes: 2 additions & 0 deletions frontend/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
"postinstall": "pnpm gen:global && pnpm gen:region"
},
"dependencies": {
"@alicloud/captcha20230305": "1.1.3",
"@alicloud/dysmsapi20170525": "^2.0.24",
"@alicloud/openapi-client": "^0.4.6",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/tea-util": "^1.4.7",
"@chakra-ui/anatomy": "^2.2.1",
"@chakra-ui/icons": "^2.1.1",
Expand Down
109 changes: 109 additions & 0 deletions frontend/desktop/src/components/signin/Captcha/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import request from '@/services/request';
import { useConfigStore } from '@/stores/config';
import useScriptStore from '@/stores/script';
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 { v4 } from 'uuid';
export type TCaptchaInstance = {
getToken: () => Promise<string>;
reset: () => void;
};

const HiddenCaptchaComponent = forwardRef(function HiddenCaptchaComponent(props, ref) {
const captchaElementRef = useRef<HTMLDivElement>(null);
const captchaInstanceRef = useRef(null);
const tokenRef = useRef('');
const buttonRef = useRef<HTMLButtonElement>(null);
const { authConfig } = useConfigStore();
useImperativeHandle<any, TCaptchaInstance>(
ref,
() => {
return {
getToken: async () => {
buttonRef.current?.click();
await new Promise((res) => {
setTimeout(res, 1000);
});
const token = tokenRef.current;
return token;
},
reset() {
tokenRef.current = '';
}
};
},
[]
);
const { captchaIsLoaded } = useScriptStore();
const [buttonId] = useState('captcha_button_pop');
const [captchaId] = useState('captcha_' + v4().slice(0, 8));

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

const onBizResultCallback = () => {};
useEffect(() => {
if (!captchaIsLoaded) return;
const initAliyunCaptchaOptions = {
SceneId: authConfig?.captcha.ali.sceneId,
prefix: authConfig?.captcha.ali.prefix,
mode: 'popup',
element: '#' + captchaId,
button: '#' + buttonId,
async captchaVerifyCallback(captchaToken: string) {
try {
tokenRef.current = captchaToken;
return {
captchaResult: true
};
} catch (err) {
tokenRef.current = '';
return {
captchaResult: false
};
}
},
onBizResultCallback: onBizResultCallback,
getInstance,
slideStyle: {
width: 360,
height: 40
},
immediate: false,
language: 'cn',
region: 'cn'
};

// @ts-ignore
window.initAliyunCaptcha(initAliyunCaptchaOptions);

return () => {
captchaInstanceRef.current = null;
};
}, [captchaIsLoaded]);
return (
<>
<Button
ref={buttonRef}
variant={'unstyled'}
hidden
id={buttonId}
{...(props as ButtonProps)}
/>
<div ref={captchaElementRef} id={captchaId} />
</>
);
});

export { HiddenCaptchaComponent };
28 changes: 18 additions & 10 deletions frontend/desktop/src/components/signin/auth/useSms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import {
import { useTranslation } from 'next-i18next';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { MouseEvent, MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import { get, useForm } from 'react-hook-form';
import { getRegionToken } from '@/api/auth';
import { getBaiduId, getInviterId, getUserSemData, sessionConfig } from '@/utils/sessionConfig';
import { I18nCommonKey } from '@/types/i18next';
import { useConfigStore } from '@/stores/config';

export default function useSms({
showError
Expand All @@ -27,9 +28,10 @@ export default function useSms({
const { t } = useTranslation();
const _remainTime = useRef(0);
const router = useRouter();
const { authConfig } = useConfigStore();
const [isLoading, setIsLoading] = useState(false);
const setToken = useSessionStore((s) => s.setToken);
const { register, handleSubmit, trigger, getValues } = useForm<{
const { register, handleSubmit, trigger, getValues, watch } = useForm<{
phoneNumber: string;
verifyCode: string;
}>();
Expand Down Expand Up @@ -81,7 +83,7 @@ export default function useSms({
onAfterGetCode,
getCfToken
}: {
getCfToken?: () => string | undefined;
getCfToken?: () => Promise<string | undefined>;
onAfterGetCode?: () => void;
}) => {
const [remainTime, setRemainTime] = useState(_remainTime.current);
Expand All @@ -93,19 +95,26 @@ export default function useSms({
}, 1000);
return () => clearInterval(interval);
}, [remainTime]);

const getCode: MouseEventHandler = async (e) => {
const [invokeTime, setInvokeTime] = useState(new Date().getTime());
const getCode: MouseEventHandler = async (e: MouseEvent) => {
e.preventDefault();

if (!(await trigger('phoneNumber'))) {
showError(t('common:invalid_phone_number') || 'Invalid phone number');
return;
}
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 {
const cfToken = getCfToken?.();
const cfToken = await getCfToken?.();
const res = await request.post<any, ApiResp<any>>('/api/auth/phone/sms', {
id: getValues('phoneNumber'),
cfToken
Expand All @@ -121,7 +130,6 @@ export default function useSms({
onAfterGetCode?.();
}
};

return (
<>
<InputGroup
Expand Down
11 changes: 9 additions & 2 deletions frontend/desktop/src/components/signin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import Head from 'next/head';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useRef, useState } from 'react';
import useWechat from './auth/useWechat';
import { HiddenCaptchaComponent, TCaptchaInstance } from './Captcha';
import { captcha } from 'tencentcloud-sdk-nodejs';

export default function SigninComponent() {
const conf = useConfigStore();
Expand Down Expand Up @@ -74,6 +76,7 @@ export default function SigninComponent() {
}
}, []);
const turnstileRef = useRef<TurnstileInstance>(null);
const captchaRef = useRef<TCaptchaInstance>(null);
const loginConfig = useMemo(() => {
return {
[LoginType.SMS]: {
Expand All @@ -82,9 +85,12 @@ export default function SigninComponent() {
<SmsModal
onAfterGetCode={() => {
turnstileRef.current?.reset();
captchaRef.current?.reset();
}}
getCfToken={() => {
return turnstileRef.current?.getResponse();
getCfToken={async () => {
const token = await captchaRef.current?.getToken();
const turnstiletoken = turnstileRef.current?.getResponse();
return token;
}}
/>
)
Expand Down Expand Up @@ -225,6 +231,7 @@ export default function SigninComponent() {
siteKey={conf.commonConfig?.cfSiteKey}
/>
)}
{conf.authConfig?.captcha.enabled && <HiddenCaptchaComponent ref={captchaRef} />}
<Button
variant={'unstyled'}
background="linear-gradient(90deg, #000000 0%, rgba(36, 40, 44, 0.9) 98.29%)"
Expand Down
13 changes: 10 additions & 3 deletions frontend/desktop/src/pages/api/auth/phone/sms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ErrorHandler } from '@/services/backend/middleware/error';
import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms';
import {
filterCaptcha,
filterCf,
filterPhoneParams,
sendPhoneCodeGuard
} from '@/services/backend/middleware/sms';
import { sendPhoneCodeSvc } from '@/services/backend/svc/sms';
import { enablePhoneSms } from '@/services/enable';
import { NextApiRequest, NextApiResponse } from 'next';
Expand All @@ -9,8 +14,10 @@ export default ErrorHandler(async function handler(req: NextApiRequest, res: Nex
throw new Error('SMS is not enabled');
}
await filterCf(req, res, async () => {
await filterPhoneParams(req, res, ({ phoneNumbers: phone }) =>
sendPhoneCodeGuard(phone)(res, () => sendPhoneCodeSvc(phone)(res))
await filterCaptcha(req, res, () =>
filterPhoneParams(req, res, ({ phoneNumbers: phone }) =>
sendPhoneCodeGuard(phone)(res, () => sendPhoneCodeSvc(phone)(res))
)
);
});
});
6 changes: 3 additions & 3 deletions frontend/desktop/src/pages/api/platform/getAppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export async function getAppConfig(): Promise<AppClientConfigType> {
const authConf = await getAuthClientConfig();
const commonConf = await getCommonClientConfig();
const layoutConf = await getLayoutConfig();
const _tracking = global.AppConfig.tracking;
const appConfig = global.AppConfig;
const tracking: Required<TrackingConfigType> = {
websiteId: _tracking.websiteId || '',
hostUrl: _tracking.hostUrl || ''
websiteId: appConfig?.tracking?.websiteId || '',
hostUrl: appConfig?.tracking?.hostUrl || ''
};
const conf = genResConfig(cloudConf, authConf, commonConf, layoutConf, tracking);
if (!global.commitCroner) {
Expand Down
9 changes: 9 additions & 0 deletions frontend/desktop/src/pages/api/platform/getAuthConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

// genResAuthConfig Return AuthConfigType with only necessary fields for response to client, to avoid exposing sensitive data
function genResAuthClientConfig(conf: AuthConfigType) {
const captcha = conf.captcha;
const authClientConfig: AuthClientConfigType = {
callbackURL: conf.callbackURL || '',
invite: {
Expand Down Expand Up @@ -62,6 +63,14 @@ function genResAuthClientConfig(conf: AuthConfigType) {
proxyAddress: conf.idp.oauth2?.proxyAddress || ''
}
},
captcha: {
enabled: !!captcha?.enabled,
ali: {
enabled: !!captcha?.ali?.enabled,
sceneId: captcha?.ali?.sceneId || '',
prefix: captcha?.ali?.prefix || ''
}
},
hasBaiduToken: !!conf.baiduToken,
billingToken: ''
};
Expand Down
14 changes: 12 additions & 2 deletions frontend/desktop/src/pages/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import Head from 'next/head';
import { useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import Script from 'next/script';
import useScriptStore from '@/stores/script';

export default function SigninPage() {
const { layoutConfig } = useConfigStore();
const { layoutConfig, authConfig } = useConfigStore();
const { t } = useTranslation();

const { setCaptchaIsLoad } = useScriptStore();
useEffect(() => {
const url = sessionStorage.getItem('accessTemplatesNoLogin');
if (!!url) {
Expand All @@ -29,9 +30,18 @@ export default function SigninPage() {
<link rel="shortcut icon" href={layoutConfig?.logo ? layoutConfig?.logo : '/favicon.ico'} />
<link rel="icon" href={layoutConfig?.logo ? layoutConfig?.logo : '/favicon.ico'} />
</Head>
{authConfig?.captcha.enabled && (
<Script
src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
onLoad={() => {
setCaptchaIsLoad();
}}
/>
)}
{layoutConfig?.meta.scripts?.map((item, i) => {
return <Script key={i} {...item} />;
})}

<SigninComponent />
</Box>
);
Expand Down
Loading

0 comments on commit 09262b5

Please sign in to comment.