Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:add captcha #5389

Merged
merged 1 commit into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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