Skip to content

Commit

Permalink
feat(captcha): use component for Captcha
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed Sep 2, 2023
1 parent 05adda7 commit 13d6d31
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 220 deletions.
2 changes: 1 addition & 1 deletion docs/pages/config/appsettings.zh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ GZCTF 仅支持 PostgreSQL 作为数据库,不支持 MySQL 等其他数据库

#### GoogleRecaptcha

配置 Google Recaptcha 的相关信息,用于注册时的验证码验证,可选项。
配置 Google Recaptcha v3 的相关信息,可选项。

- **VerifyAPIAddress:** Google Recaptcha 验证 API 地址
- **RecaptchaThreshold:** Google Recaptcha 阈值,用于判断验证码是否有效
Expand Down
2 changes: 2 additions & 0 deletions src/GZCTF/ClientApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@mantine/hooks": "^6.0.19",
"@mantine/modals": "^6.0.19",
"@mantine/notifications": "^6.0.19",
"@marsidev/react-turnstile": "^0.3.0",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@microsoft/signalr": "^7.0.10",
Expand All @@ -37,6 +38,7 @@
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-google-recaptcha-v3": "^1.10.1",
"react-pdf": "^7.3.3",
"react-router": "^6.15.0",
"react-router-dom": "^6.15.0",
Expand Down
27 changes: 27 additions & 0 deletions src/GZCTF/ClientApp/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/GZCTF/ClientApp/src/components/AccountView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import LogoHeader from '@Components/LogoHeader'

const useStyles = createStyles(() => ({
input: {
width: '25vw',
minWidth: '250px',
width: '300px',
minWidth: '300px',
maxWidth: '300px',
},
}))
Expand Down
119 changes: 119 additions & 0 deletions src/GZCTF/ClientApp/src/components/Captcha.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { forwardRef, useImperativeHandle, useRef } from 'react'
import { GoogleReCaptchaProvider, useGoogleReCaptcha } from 'react-google-recaptcha-v3'
import { Box, BoxProps, useMantineTheme } from '@mantine/core'
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'
import api, { CaptchaProvider } from '@Api'

interface CaptchaProps extends BoxProps {
action: string
}

interface CaptchaResult {
valid: boolean
token?: string | null
}

export interface CaptchaInstance {
getToken: () => Promise<CaptchaResult>
}

export const useCaptchaRef = () => {
const captchaRef = useRef<CaptchaInstance>(null)

const getToken = async () => {
const res = await captchaRef.current?.getToken()
return res ?? { valid: false }
}

return { captchaRef, getToken } as const
}

const ReCaptchaBox = forwardRef<CaptchaInstance, CaptchaProps>((props, ref) => {
const { action, ...others } = props
const { executeRecaptcha } = useGoogleReCaptcha()

useImperativeHandle(ref, () => ({
getToken: async () => {
if (!executeRecaptcha) {
return { valid: false }
}

const token = await executeRecaptcha(action)
return { valid: !!token, token }
},
}))

return <Box {...others} />
})

const Captcha = forwardRef<CaptchaInstance, CaptchaProps>((props, ref) => {
const { action, ...others } = props
const theme = useMantineTheme()

const { data: info, error } = api.info.useInfoGetClientCaptchaInfo({
refreshInterval: 0,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenHidden: false,
shouldRetryOnError: false,
refreshWhenOffline: false,
})

const type = info?.type ?? CaptchaProvider.None
const turnstileRef = useRef<TurnstileInstance>(null)
const reCaptchaRef = useRef<CaptchaInstance>(null)

useImperativeHandle(ref, () => ({
getToken: async () => {
if (error || !info) {
return { valid: false }
}

if (!info?.siteKey || type === CaptchaProvider.None) {
return { valid: true }
}

if (type === CaptchaProvider.GoogleRecaptcha) {
const res = await reCaptchaRef.current?.getToken()
return res ?? { valid: false }
}

const token = turnstileRef.current?.getResponse()
return { valid: !!token, token }
},
}))

if (error || !info?.siteKey || type === CaptchaProvider.None) {
return <Box {...others} />
}

if (type === CaptchaProvider.GoogleRecaptcha) {
return (
<GoogleReCaptchaProvider
reCaptchaKey={info.siteKey}
container={{
parameters: {
theme: theme.colorScheme,
},
}}
>
<ReCaptchaBox ref={reCaptchaRef} action={action} {...others} />
</GoogleReCaptchaProvider>
)
}

return (
<Box {...others}>
<Turnstile
ref={turnstileRef}
siteKey={info.siteKey}
options={{
theme: theme.colorScheme,
action,
}}
/>
</Box>
)
})

export default Captcha
67 changes: 46 additions & 21 deletions src/GZCTF/ClientApp/src/pages/account/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { FC, useEffect, useState } from 'react'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { PasswordInput, Grid, TextInput, Button, Anchor, Box } from '@mantine/core'
import { PasswordInput, Grid, TextInput, Button, Anchor } from '@mantine/core'
import { useInputState } from '@mantine/hooks'
import { showNotification } from '@mantine/notifications'
import { showNotification, updateNotification } from '@mantine/notifications'
import { mdiCheck, mdiClose } from '@mdi/js'
import { Icon } from '@mdi/react'
import AccountView from '@Components/AccountView'
import { showErrorNotification } from '@Utils/ApiErrorHandler'
import { useCaptcha } from '@Utils/useCaptcha'
import Captcha, { useCaptchaRef } from '@Components/Captcha'
import { usePageTitle } from '@Utils/usePageTitle'
import { useUser } from '@Utils/useUser'
import api from '@Api'

const Login: FC = () => {
const params = useSearchParams()[0]
const captcha = useCaptcha('login')
const navigate = useNavigate()

const [pwd, setPwd] = useInputState('')
const [uname, setUname] = useInputState('')
const [disabled, setDisabled] = useState(false)
const [needRedirect, setNeedRedirect] = useState(false)

const { captchaRef, getToken } = useCaptchaRef()
const { user, mutate } = useUser()

usePageTitle('登录')
Expand Down Expand Up @@ -51,28 +50,54 @@ const Login: FC = () => {
return
}

const token = await captcha?.getChallenge()
const { valid, token } = await getToken()

api.account
.accountLogIn({
if (!valid) {
showNotification({
color: 'orange',
title: '请等待验证码……',
message: '请稍后重试',
loading: true,
})
return
}

showNotification({
color: 'orange',
id: 'login-status',
title: '请求已发送……',
message: '等待服务器验证',
loading: true,
autoClose: false,
})

try {
await api.account.accountLogIn({
userName: uname,
password: pwd,
challenge: token,
})
.then(() => {
showNotification({
color: 'teal',
title: '登录成功',
message: '跳转回登录前页面',
icon: <Icon path={mdiCheck} size={1} />,
})
setNeedRedirect(true)
mutate()

updateNotification({
id: 'login-status',
color: 'teal',
title: '登录成功',
message: '跳转回登录前页面',
icon: <Icon path={mdiCheck} size={1} />,
})
.catch((err) => {
showErrorNotification(err)
setDisabled(false)
setNeedRedirect(true)
mutate()
} catch (err: any) {
updateNotification({
id: 'login-status',
color: 'red',
title: '遇到了问题',
message: `${err.response.data.title}`,
icon: <Icon path={mdiClose} size={1} />,
})
} finally {
setDisabled(false)
}
}

return (
Expand All @@ -97,7 +122,7 @@ const Login: FC = () => {
disabled={disabled}
onChange={(event) => setPwd(event.currentTarget.value)}
/>
<Box id="captcha" />
<Captcha action="login" ref={captchaRef} />
<Anchor
sx={(theme) => ({
fontSize: theme.fontSizes.xs,
Expand Down
Loading

0 comments on commit 13d6d31

Please sign in to comment.