import React, { useEffect, useRef, useState } from 'react'; import * as captchaApi from '../api/captcha.js'; // ── 模块级单例:showCaptcha 挂起请求,由 CaptchaModalHost 消费 ── let pending = null; // { action, type, resolve } let listener = null; function notifyHost() { if (listener) listener(); } /** * 验证码流程入口(迁移自 public/js/captcha.js): * - null 无需验证 / 用户取消 * - object { type: 'proof' | 'recaptcha' | 'turnstile', value } * proof → 内置验证码通过,value 为后端签发的 captcha_proof * recaptcha → reCAPTCHA 验证通过,value 为 siteverify token * turnstile → Turnstile 验证通过,value 为 siteverify token * 调用方提交时按 type 放入 captcha_proof / recaptcha_token / turnstile_token 字段。 */ export function showCaptcha(action) { return new Promise((resolve) => { captchaApi.required(action) .then((r) => { if (!r || !r.required || r.type === 'none') { resolve(null); return; } pending = { action, type: r.type || 'builtin', resolve }; notifyHost(); }) .catch(() => resolve(null)); }); } // 加载第三方验证码脚本(recaptcha / turnstile) function ensureThirdPartyScript(type) { if (type === 'recaptcha') { window.recaptchaCallbacks = window.recaptchaCallbacks || []; if (typeof window.grecaptcha === 'undefined' && !document.querySelector('script[src*="recaptcha/api"]')) { const s = document.createElement('script'); s.src = 'https://www.recaptcha.net/recaptcha/api.js?onload=onRecaptchaLoad&render=explicit'; s.async = true; s.defer = true; document.head.appendChild(s); } } else { window.turnstileCallbacks = window.turnstileCallbacks || []; if (typeof window.turnstile === 'undefined' && !document.querySelector('script[src*="turnstile"]')) { const s = document.createElement('script'); s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad&render=explicit'; s.async = true; s.defer = true; document.head.appendChild(s); } } } // SHA-256(Proof of Work 使用) async function sha256(str) { const buf = new TextEncoder().encode(str); const hash = await crypto.subtle.digest('SHA-256', buf); return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, '0')).join(''); } // 内置 SVG 验证码弹窗 function BuiltinCaptcha({ onFinish }) { const [svg, setSvg] = useState(''); const [error, setError] = useState(''); const [answer, setAnswer] = useState(''); const [powDone, setPowDone] = useState(false); const [verifying, setVerifying] = useState(false); const tokenRef = useRef(null); const imgRef = useRef(null); const loadImage = async () => { setError(''); setAnswer(''); const data = await captchaApi.getImage(); if (data && data.token) { tokenRef.current = data.token; setSvg(data.svg); } }; useEffect(() => { loadImage(); // 智能验证(Proof of Work)后台运行,20s 内找到满足难度的 nonce (async () => { try { const chal = await captchaApi.powChallenge(); if (!chal || !chal.token) return; const target = '0'.repeat(chal.difficulty); const start = Date.now(); let nonce = 0; while (Date.now() - start < 20000) { const hash = await sha256(chal.prefix + nonce); if (hash.startsWith(target)) { await captchaApi.powVerify(chal.token, String(nonce)); setPowDone(true); return; } nonce++; } } catch { /* 失败不影响主流程 */ } })(); }, []); // 让后端返回的 SVG 自适应弹窗宽度(照搬 captcha.js 的样式处理) useEffect(() => { if (svg && imgRef.current) { const s = imgRef.current.querySelector('svg'); if (s) s.setAttribute('style', 'width:100%;max-width:240px;height:auto;border-radius:8px;display:block'); } }, [svg]); const submit = async () => { if (!powDone) { setError('智能验证尚未完成,请稍候...'); return; } if (!answer.trim() || !tokenRef.current) { setError('请输入验证码'); return; } setVerifying(true); const v = await captchaApi.verify(tokenRef.current, answer.trim()); setVerifying(false); if (v.success) { onFinish({ type: 'proof', value: v.proof || '' }); } else { setError(v.error || '验证码错误'); tokenRef.current = null; setAnswer(''); loadImage(); } }; return (
{status}