diff --git a/public/i18n/en.json b/public/i18n/en.json index f44f6a0..1faa64d 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -238,6 +238,8 @@ "LoginNow": "Login Now", "RegisterNow": "Register Now", "VerificationFailed": "Verification failed: ", + "WaitingForCloudflareVerification": "Waiting for Cloudflare verification", + "CloudflareVerificationNotReady": "Cloudflare verification is not ready. Check your network and try again shortly.", "EnterUsername": "Enter username", "EnterPassword": "Enter password", "EnterEmail": "Enter email", @@ -364,4 +366,4 @@ "AllCharts": "All Charts", "RandomRecommend": "Random Recommend", "500UploadErrorAndHint": "Upload failed, please go to the main site to upload" -} \ No newline at end of file +} diff --git a/public/i18n/ja.json b/public/i18n/ja.json index c7a4f31..e22b6a1 100644 --- a/public/i18n/ja.json +++ b/public/i18n/ja.json @@ -238,6 +238,8 @@ "LoginNow": "今すぐログイン", "RegisterNow": "今すぐ登録", "VerificationFailed": "認証に失敗しました:", + "WaitingForCloudflareVerification": "Cloudflare 認証を待っています", + "CloudflareVerificationNotReady": "Cloudflare 認証が完了していません。ネットワークを確認して、しばらくしてから再試行してください。", "EnterUsername": "ユーザー名を入力", "EnterPassword": "パスワードを入力", "EnterEmail": "メールアドレスを入力", @@ -359,4 +361,4 @@ "CreateFailed": "作成に失敗しました", "Submit": "送信", "500UploadErrorAndHint": "アップロードに失敗しました。主サイトに行ってアップロードしてください" -} \ No newline at end of file +} diff --git a/public/i18n/ko.json b/public/i18n/ko.json index 436f7f6..a6daf3c 100644 --- a/public/i18n/ko.json +++ b/public/i18n/ko.json @@ -238,6 +238,8 @@ "LoginNow": "지금 로그인", "RegisterNow": "지금 가입", "VerificationFailed": "인증에 실패했습니다: ", + "WaitingForCloudflareVerification": "Cloudflare 인증 대기 중", + "CloudflareVerificationNotReady": "Cloudflare 인증이 완료되지 않았습니다. 네트워크를 확인한 후 잠시 뒤 다시 시도해 주세요.", "EnterUsername": "사용자 이름을 입력하세요", "EnterPassword": "비밀번호를 입력하세요", "EnterEmail": "이메일을 입력하세요", @@ -359,4 +361,4 @@ "CreateFailed": "생성 실패", "Submit": "제출", "500UploadErrorAndHint": "업로드 실패, 주 사이트로 가서 업로드하세요" -} \ No newline at end of file +} diff --git a/public/i18n/zh.json b/public/i18n/zh.json index f90c908..c4078e1 100644 --- a/public/i18n/zh.json +++ b/public/i18n/zh.json @@ -248,6 +248,8 @@ "LoginNow": "立即登录", "RegisterNow": "立即注册", "VerificationFailed": "验证失败:", + "WaitingForCloudflareVerification": "等待 Cloudflare 验证", + "CloudflareVerificationNotReady": "Cloudflare 验证尚未完成,请检查网络后稍候重试", "EnterUsername": "请输入用户名", "EnterPassword": "请输入密码", "EnterEmail": "请输入邮箱", @@ -369,4 +371,4 @@ "CreateFailed": "创建失败", "Submit": "提交", "500UploadErrorAndHint": "上传失败, 请前往主站上传" -} \ No newline at end of file +} diff --git a/src/pages/ForginsterPage.tsx b/src/pages/ForginsterPage.tsx index e74bfb7..4d91af3 100644 --- a/src/pages/ForginsterPage.tsx +++ b/src/pages/ForginsterPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import 'react-photo-view/dist/react-photo-view.css'; import { md5 } from 'js-md5'; import { endpoints } from '@/config/api'; @@ -17,6 +17,47 @@ type TabType = 'login' | 'register' | 'forget'; const TAB_ORDER: TabType[] = ['login', 'register', 'forget']; const AUTH_CARD_CLASSNAME = 'bg-[rgb(30_30_30/90%)] shadow-[0_20px_40px_rgb(0_0_0/40%)] backdrop-blur-[20px] p-8 md:p-12 border border-white/10 rounded-[20px]'; +const TURNSTILE_SITE_KEY = '0x4AAAAAACAEyA1EhHmEDS0o'; +const TURNSTILE_SCRIPT_ID = 'cloudflare-turnstile-script'; + +type TurnstileApi = { + render: (container: HTMLElement, options: { sitekey: string }) => string; + remove?: (widgetId: string) => void; +}; + +declare global { + interface Window { + turnstile?: TurnstileApi; + } +} + +let turnstileScriptPromise: Promise | null = null; + +function loadTurnstileScript() { + if (window.turnstile) return Promise.resolve(); + if (turnstileScriptPromise) return turnstileScriptPromise; + + turnstileScriptPromise = new Promise((resolve, reject) => { + const existingScript = document.getElementById(TURNSTILE_SCRIPT_ID) as HTMLScriptElement | null; + const script = existingScript ?? document.createElement('script'); + + const handleLoad = () => resolve(); + const handleError = () => reject(new Error('Cloudflare Turnstile failed to load')); + + script.addEventListener('load', handleLoad, { once: true }); + script.addEventListener('error', handleError, { once: true }); + + if (!existingScript) { + script.id = TURNSTILE_SCRIPT_ID; + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + script.defer = true; + document.head.appendChild(script); + } + }); + + return turnstileScriptPromise; +} const tabTransitionVariants = { enter: (direction: number) => ({ @@ -270,16 +311,73 @@ function LoginTab() { ); } +function TurnstileWidget() { + const loc = useLoc(); + const containerRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let cancelled = false; + let widgetId: string | undefined; + const hasRenderedWidget = () => container.childElementCount > 0; + const updateVisibility = () => setIsVisible(hasRenderedWidget()); + const observer = new MutationObserver(updateVisibility); + + observer.observe(container, { childList: true, subtree: true }); + updateVisibility(); + + loadTurnstileScript() + .then(() => { + if (cancelled || !window.turnstile || hasRenderedWidget()) return; + widgetId = window.turnstile.render(container, { sitekey: TURNSTILE_SITE_KEY }); + updateVisibility(); + }) + .catch(() => { + // The placeholder remains visible when Cloudflare cannot be reached. + }); + + return () => { + cancelled = true; + observer.disconnect(); + if (widgetId) window.turnstile?.remove?.(widgetId); + }; + }, []); + + return ( +
+
+ {!isVisible && ( +
+
+ )} +
+ ); +} + function RegisterTab() { const loc = useLoc(); const [isPosting, setIsPosting] = useState(false); async function onSubmit(event: React.FormEvent) { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const turnstileResponse = formData.get('cf-turnstile-response'); + if (typeof turnstileResponse !== 'string' || turnstileResponse.trim() === '') { + toast.error(loc( + 'CloudflareVerificationNotReady', + 'Cloudflare 验证尚未完成,请检查网络后稍候重试', + )); + return; + } + setIsPosting(true); try { - event.preventDefault(); - - const formData = new FormData(event.currentTarget); if (formData.get('password') !== formData.get('password2')) { toast.error(loc('PasswdNoMatch', '两次密码不匹配')); return; @@ -310,9 +408,13 @@ function RegisterTab() { case retCode.CODE_EMAIL_ALREADY_EXISTS: toast.error(loc('EmailExists', '邮箱已被注册')); break; - default: - toast.error(loc('VerificationFailed', '验证失败') + rsp.message); + default: { + const message = typeof rsp.message === 'string' ? rsp.message.trim() : ''; + toast.error(message + ? `${loc('VerificationFailed', '验证失败:')}${message}` + : loc('VerificationFailed', '验证失败')); break; + } } return; } @@ -376,12 +478,7 @@ function RegisterTab() { />
- -
+