mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-26 04:02:47 +00:00
feat: Add Cap captcha support (#260)
* feat: Add Cap captcha support * Update the translation * Update the frontend * fix: CapCaptcha not rendering after route switch
This commit is contained in:
parent
1f75c1c59e
commit
62e70ea6a5
|
|
@ -428,6 +428,13 @@
|
|||
"turnstile": "Cloudflare Turnstile",
|
||||
"turnstileSiteKey": "Site Key",
|
||||
"turnstileSiteKSecret": "Secret",
|
||||
"cap": "Cap",
|
||||
"capInstanceURL": "Instance URL",
|
||||
"capInstanceURLDes": "The URL of your self-hosted Cap server. For more details, see the <0>standalone mode documentation</0>.",
|
||||
"capKeyID": "Key ID",
|
||||
"capKeyIDDes": "The key ID from your Cap server dashboard.",
|
||||
"capKeySecret": "Key Secret",
|
||||
"capKeySecretDes": "The key secret from your Cap server dashboard.",
|
||||
"captchaProvider": "Captcha provider",
|
||||
"captchaWidth": "Width",
|
||||
"captchaHeight": "Height",
|
||||
|
|
|
|||
|
|
@ -426,6 +426,13 @@
|
|||
"turnstile": "Cloudflare Turnstile",
|
||||
"turnstileSiteKey": "サイトキー",
|
||||
"turnstileSiteKSecret": "キー",
|
||||
"cap": "Cap",
|
||||
"capInstanceURL": "インスタンス URL",
|
||||
"capInstanceURLDes": "自身でホストしている Cap サーバーの URL。詳細については、<0>スタンドアロンモードドキュメント</0> を参照してください。",
|
||||
"capKeyID": "キー ID",
|
||||
"capKeyIDDes": "Cap サーバーダッシュボードから取得したキー ID。",
|
||||
"capKeySecret": "キーシークレット",
|
||||
"capKeySecretDes": "Cap サーバーダッシュボードから取得したキーシークレット。",
|
||||
"captchaProvider": "認証コードタイプ",
|
||||
"captchaWidth": "幅",
|
||||
"captchaHeight": "高さ",
|
||||
|
|
|
|||
|
|
@ -426,6 +426,13 @@
|
|||
"turnstile": "Cloudflare Turnstile",
|
||||
"turnstileSiteKey": "站点密钥",
|
||||
"turnstileSiteKSecret": "密钥",
|
||||
"cap": "Cap",
|
||||
"capInstanceURL": "实例 URL",
|
||||
"capInstanceURLDes": "自部署 Cap 服务器的 URL 地址。详细信息请参考 <0>独立模式文档</0>。",
|
||||
"capKeyID": "密钥 ID",
|
||||
"capKeyIDDes": "从 Cap 服务器控制面板获取的密钥 ID。",
|
||||
"capKeySecret": "密钥密码",
|
||||
"capKeySecretDes": "从 Cap 服务器控制面板获取的密钥密码。",
|
||||
"captchaProvider": "验证码类型",
|
||||
"captchaWidth": "宽度",
|
||||
"captchaHeight": "高度",
|
||||
|
|
|
|||
|
|
@ -423,6 +423,13 @@
|
|||
"turnstile": "Cloudflare Turnstile",
|
||||
"turnstileSiteKey": "站點金鑰",
|
||||
"turnstileSiteKSecret": "金鑰",
|
||||
"cap": "Cap",
|
||||
"capInstanceURL": "實例 URL",
|
||||
"capInstanceURLDes": "自部署 Cap 伺服器的 URL 地址。詳細資訊請參考 <0>獨立模式文檔</0>。",
|
||||
"capKeyID": "金鑰 ID",
|
||||
"capKeyIDDes": "從 Cap 伺服器控制面板獲取的金鑰 ID。",
|
||||
"capKeySecret": "金鑰密碼",
|
||||
"capKeySecretDes": "從 Cap 伺服器控制面板獲取的金鑰密碼。",
|
||||
"captchaProvider": "驗證碼型別",
|
||||
"captchaWidth": "寬度",
|
||||
"captchaHeight": "高度",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export enum CaptchaType {
|
|||
// Deprecated
|
||||
TCAPTCHA = "tcaptcha",
|
||||
TURNSTILE = "turnstile",
|
||||
CAP = "cap",
|
||||
}
|
||||
|
||||
export interface SiteConfig {
|
||||
|
|
@ -22,6 +23,9 @@ export interface SiteConfig {
|
|||
captcha_ReCaptchaKey?: string;
|
||||
captcha_type?: CaptchaType;
|
||||
turnstile_site_id?: string;
|
||||
captcha_cap_instance_url?: string;
|
||||
captcha_cap_key_id?: string;
|
||||
captcha_cap_key_secret?: string;
|
||||
register_enabled?: boolean;
|
||||
logo?: string;
|
||||
logo_light?: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FormControl, Link, Stack } from "@mui/material";
|
||||
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
|
||||
import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx";
|
||||
import * as React from "react";
|
||||
import { NoMarginHelperText } from "../Settings.tsx";
|
||||
|
||||
export interface CapCaptchaProps {
|
||||
values: {
|
||||
[key: string]: string;
|
||||
};
|
||||
setSettings: (settings: { [key: string]: string }) => void;
|
||||
}
|
||||
|
||||
const CapCaptcha = ({ values, setSettings }: CapCaptchaProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<SettingForm title={t("settings.capInstanceURL")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
value={values.captcha_cap_instance_url}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
captcha_cap_instance_url: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="https://cap.example.com"
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
<Trans
|
||||
i18nKey="settings.capInstanceURLDes"
|
||||
ns={"dashboard"}
|
||||
components={[
|
||||
<Link
|
||||
key={0}
|
||||
href={"https://capjs.js.org/guide/standalone.html"}
|
||||
target={"_blank"}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.capKeyID")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
value={values.captcha_cap_key_id}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
captcha_cap_key_id: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
<Trans
|
||||
i18nKey="settings.capKeyIDDes"
|
||||
ns={"dashboard"}
|
||||
components={[
|
||||
<Link
|
||||
key={0}
|
||||
href={"https://capjs.js.org/guide/standalone.html"}
|
||||
target={"_blank"}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.capKeySecret")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
value={values.captcha_cap_key_secret}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
captcha_cap_key_secret: e.target.value,
|
||||
})
|
||||
}
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
<Trans
|
||||
i18nKey="settings.capKeySecretDes"
|
||||
ns={"dashboard"}
|
||||
components={[
|
||||
<Link
|
||||
key={0}
|
||||
href={"https://capjs.js.org/guide/standalone.html"}
|
||||
target={"_blank"}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CapCaptcha;
|
||||
|
|
@ -25,6 +25,7 @@ import { CaptchaType } from "../../../../api/site.ts";
|
|||
import GraphicCaptcha from "./GraphicCaptcha.tsx";
|
||||
import ReCaptcha from "./ReCaptcha.tsx";
|
||||
import TurnstileCaptcha from "./TurnstileCaptcha.tsx";
|
||||
import CapCaptcha from "./CapCaptcha.tsx";
|
||||
|
||||
const Captcha = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
|
@ -136,6 +137,13 @@ const Captcha = () => {
|
|||
{t("settings.turnstile")}
|
||||
</ListItemText>
|
||||
</SquareMenuItem>
|
||||
<SquareMenuItem value={CaptchaType.CAP}>
|
||||
<ListItemText slotProps={{
|
||||
primary: { variant: "body2" }
|
||||
}}>
|
||||
{t("settings.cap")}
|
||||
</ListItemText>
|
||||
</SquareMenuItem>
|
||||
</DenseSelect>
|
||||
<NoMarginHelperText>
|
||||
{t("settings.captchaTypeDes")}
|
||||
|
|
@ -160,6 +168,12 @@ const Captcha = () => {
|
|||
>
|
||||
<TurnstileCaptcha setSettings={setSettings} values={values} />
|
||||
</Collapse>
|
||||
<Collapse
|
||||
in={values.captcha_type === CaptchaType.CAP}
|
||||
unmountOnExit
|
||||
>
|
||||
<CapCaptcha setSettings={setSettings} values={values} />
|
||||
</Collapse>
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -221,6 +221,9 @@ const Settings = () => {
|
|||
"captcha_ReCaptchaSecret",
|
||||
"captcha_turnstile_site_key",
|
||||
"captcha_turnstile_site_secret",
|
||||
"captcha_cap_instance_url",
|
||||
"captcha_cap_key_id",
|
||||
"captcha_cap_key_secret",
|
||||
]}
|
||||
>
|
||||
<Captcha />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { CaptchaParams } from "./Captcha.tsx";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export interface CapProps {
|
||||
onStateChange: (state: CaptchaParams) => void;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
const CapCaptcha = ({ onStateChange, generation, ...rest }: CapProps) => {
|
||||
const captchaRef = useRef<HTMLDivElement>(null);
|
||||
const widgetRef = useRef<any>(null);
|
||||
const onStateChangeRef = useRef(onStateChange);
|
||||
const scriptLoadedRef = useRef(false);
|
||||
|
||||
const capInstanceURL = useAppSelector(
|
||||
(state) => state.siteConfig.basic.config.captcha_cap_instance_url,
|
||||
);
|
||||
const capKeyID = useAppSelector(
|
||||
(state) => state.siteConfig.basic.config.captcha_cap_key_id,
|
||||
);
|
||||
|
||||
// Keep callback reference up to date
|
||||
useEffect(() => {
|
||||
onStateChangeRef.current = onStateChange;
|
||||
}, [onStateChange]);
|
||||
|
||||
const createWidget = () => {
|
||||
if (!captchaRef.current || !capInstanceURL || !capKeyID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up existing widget
|
||||
if (widgetRef.current) {
|
||||
widgetRef.current.remove?.();
|
||||
widgetRef.current = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
captchaRef.current.innerHTML = "";
|
||||
|
||||
if (typeof window !== "undefined" && (window as any).Cap) {
|
||||
const widget = document.createElement("cap-widget");
|
||||
widget.setAttribute("data-cap-api-endpoint", `${capInstanceURL.replace(/\/$/, "")}/${capKeyID}/api/`);
|
||||
widget.id = "cap-widget";
|
||||
|
||||
captchaRef.current.appendChild(widget);
|
||||
|
||||
widget.addEventListener("solve", (e: any) => {
|
||||
const token = e.detail.token;
|
||||
if (token) {
|
||||
onStateChangeRef.current({ ticket: token });
|
||||
}
|
||||
});
|
||||
|
||||
widgetRef.current = widget;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCaptcha = () => {
|
||||
createWidget();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (generation > 0) {
|
||||
refreshCaptcha();
|
||||
}
|
||||
}, [generation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!capInstanceURL || !capKeyID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptId = "cap-widget-script";
|
||||
let script = document.getElementById(scriptId) as HTMLScriptElement;
|
||||
|
||||
const initWidget = () => {
|
||||
scriptLoadedRef.current = true;
|
||||
// Add a small delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
createWidget();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
if (!script) {
|
||||
script = document.createElement("script");
|
||||
script.id = scriptId;
|
||||
script.src = `${capInstanceURL.replace(/\/$/, "")}/assets/widget.js`;
|
||||
script.async = true;
|
||||
script.onload = initWidget;
|
||||
script.onerror = () => {
|
||||
console.error("Failed to load Cap widget script");
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else if (scriptLoadedRef.current || (window as any).Cap) {
|
||||
// Script already loaded
|
||||
initWidget();
|
||||
} else {
|
||||
// Script exists but not loaded yet
|
||||
script.onload = initWidget;
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Clean up widget but keep script for reuse
|
||||
if (widgetRef.current) {
|
||||
widgetRef.current.remove?.();
|
||||
widgetRef.current = null;
|
||||
}
|
||||
if (captchaRef.current) {
|
||||
captchaRef.current.innerHTML = "";
|
||||
}
|
||||
};
|
||||
}, [capInstanceURL, capKeyID]);
|
||||
|
||||
if (!capInstanceURL || !capKeyID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<div ref={captchaRef} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CapCaptcha;
|
||||
|
|
@ -3,6 +3,7 @@ import { CaptchaType } from "../../../api/site.ts";
|
|||
import DefaultCaptcha from "./DefaultCaptcha.tsx";
|
||||
import ReCaptchaV2 from "./ReCaptchaV2.tsx";
|
||||
import TurnstileCaptcha from "./TurnstileCaptcha.tsx";
|
||||
import CapCaptcha from "./CapCaptcha.tsx";
|
||||
|
||||
export interface CaptchaProps {
|
||||
onStateChange: (state: CaptchaParams) => void;
|
||||
|
|
@ -27,6 +28,8 @@ export const Captcha = (props: CaptchaProps) => {
|
|||
return <ReCaptchaV2 {...props} />;
|
||||
case CaptchaType.TURNSTILE:
|
||||
return <TurnstileCaptcha {...props} />;
|
||||
case CaptchaType.CAP:
|
||||
return <CapCaptcha {...props} />;
|
||||
// case "tcaptcha":
|
||||
// return { ...tcaptcha, captchaRefreshRef, captchaLoading };
|
||||
default:
|
||||
|
|
|
|||
Loading…
Reference in New Issue