/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { API, getLogo, showError, showInfo, showSuccess, updateAPI, getSystemName, getOAuthProviderIcon, setUserData, onDiscordOAuthClicked, onCustomOAuthClicked, } from '../../helpers'; import Turnstile from 'react-turnstile'; import { Button, Card, Checkbox, Divider, Form, Icon, Modal, } from '@douyinfe/semi-ui'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import { IconGithubLogo, IconMail, IconUser, IconLock, IconKey, } from '@douyinfe/semi-icons'; import { onGitHubOAuthClicked, onLinuxDOOAuthClicked, onOIDCClicked, } from '../../helpers'; import OIDCIcon from '../common/logo/OIDCIcon'; import LinuxDoIcon from '../common/logo/LinuxDoIcon'; import WeChatIcon from '../common/logo/WeChatIcon'; import TelegramLoginButton from 'react-telegram-login/src'; import { UserContext } from '../../context/User'; import { StatusContext } from '../../context/Status'; import { useTranslation } from 'react-i18next'; import { SiDiscord } from 'react-icons/si'; const RegisterForm = () => { let navigate = useNavigate(); const { t } = useTranslation(); const githubButtonTextKeyByState = { idle: '使用 GitHub 继续', redirecting: '正在跳转 GitHub...', timeout: '请求超时,请刷新页面后重新发起 GitHub 登录', }; const [inputs, setInputs] = useState({ username: '', password: '', password2: '', email: '', verification_code: '', wechat_verification_code: '', }); const { username, password, password2 } = inputs; const [userState, userDispatch] = useContext(UserContext); const [statusState] = useContext(StatusContext); const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileToken, setTurnstileToken] = useState(''); const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); const [showEmailRegister, setShowEmailRegister] = useState(false); const [wechatLoading, setWechatLoading] = useState(false); const [githubLoading, setGithubLoading] = useState(false); const [discordLoading, setDiscordLoading] = useState(false); const [oidcLoading, setOidcLoading] = useState(false); const [linuxdoLoading, setLinuxdoLoading] = useState(false); const [emailRegisterLoading, setEmailRegisterLoading] = useState(false); const [registerLoading, setRegisterLoading] = useState(false); const [verificationCodeLoading, setVerificationCodeLoading] = useState(false); const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false); const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [customOAuthLoading, setCustomOAuthLoading] = useState({}); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); const [agreedToTerms, setAgreedToTerms] = useState(false); const [hasUserAgreement, setHasUserAgreement] = useState(false); const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); const [githubButtonState, setGithubButtonState] = useState('idle'); const [githubButtonDisabled, setGithubButtonDisabled] = useState(false); const [captchaId, setCaptchaId] = useState(''); const [captchaImage, setCaptchaImage] = useState(''); const [captchaCode, setCaptchaCode] = useState(''); const githubTimeoutRef = useRef(null); const githubButtonText = t(githubButtonTextKeyByState[githubButtonState]); const logo = getLogo(); const systemName = getSystemName(); let affCode = new URLSearchParams(window.location.search).get('aff'); if (affCode) { localStorage.setItem('aff', affCode); } const status = useMemo(() => { if (statusState?.status) return statusState.status; const savedStatus = localStorage.getItem('status'); if (!savedStatus) return {}; try { return JSON.parse(savedStatus) || {}; } catch (err) { return {}; } }, [statusState?.status]); const hasCustomOAuthProviders = (status.custom_oauth_providers || []).length > 0; const hasOAuthRegisterOptions = Boolean( status.github_oauth || status.discord_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth || hasCustomOAuthProviders, ); const captchaEnabled = !!status?.captcha_enabled; const [showEmailVerification, setShowEmailVerification] = useState(false); useEffect(() => { setShowEmailVerification(!!status?.email_verification); if (status?.turnstile_check) { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } // 从 status 获取用户协议和隐私政策的启用状态 setHasUserAgreement(status?.user_agreement_enabled || false); setHasPrivacyPolicy(status?.privacy_policy_enabled || false); }, [status]); useEffect(() => { let countdownInterval = null; if (disableButton && countdown > 0) { countdownInterval = setInterval(() => { setCountdown(countdown - 1); }, 1000); } else if (countdown === 0) { setDisableButton(false); setCountdown(30); } return () => clearInterval(countdownInterval); // Clean up on unmount }, [disableButton, countdown]); useEffect(() => { return () => { if (githubTimeoutRef.current) { clearTimeout(githubTimeoutRef.current); } }; }, []); const loadCaptcha = async () => { try { const res = await API.get('/api/captcha'); const { success, data } = res.data; if (success) { setCaptchaId(data.id); setCaptchaImage(data.captcha_image); setCaptchaCode(''); } } catch (error) { // silent fail } }; useEffect(() => { if (showEmailVerification && captchaEnabled) { loadCaptcha(); } }, [showEmailVerification, captchaEnabled]); const onWeChatLoginClicked = () => { setWechatLoading(true); setShowWeChatLoginModal(true); setWechatLoading(false); }; const onSubmitWeChatVerificationCode = async () => { if (turnstileEnabled && turnstileToken === '') { showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!')); return; } setWechatCodeSubmitLoading(true); try { const res = await API.get( `/api/oauth/wechat?code=${inputs.wechat_verification_code}`, ); const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); setUserData(data); updateAPI(); navigate('/'); showSuccess(t('登录成功!')); setShowWeChatLoginModal(false); } else { showError(message); } } catch (error) { showError(t('登录失败,请重试')); } finally { setWechatCodeSubmitLoading(false); } }; function handleChange(name, value) { setInputs((inputs) => ({ ...inputs, [name]: value })); } async function handleSubmit(e) { if (password.length < 8) { showInfo(t('密码长度不得小于 8 位!')); return; } if (password !== password2) { showInfo(t('两次输入的密码不一致')); return; } if (username && password) { if (turnstileEnabled && turnstileToken === '') { showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!')); return; } setRegisterLoading(true); try { if (!affCode) { affCode = localStorage.getItem('aff'); } inputs.aff_code = affCode; const res = await API.post( `/api/user/register?turnstile=${turnstileToken}`, inputs, ); const { success, message } = res.data; if (success) { navigate('/login'); showSuccess(t('注册成功!')); } else { showError(message); } } catch (error) { showError(t('注册失败,请重试')); } finally { setRegisterLoading(false); } } } const sendVerificationCode = async () => { if (inputs.email === '') return; if (turnstileEnabled && turnstileToken === '') { showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!')); return; } if (captchaEnabled && captchaCode === '') { showInfo(t('请先输入图片验证码')); return; } setVerificationCodeLoading(true); try { let url = `/api/verification?email=${encodeURIComponent(inputs.email)}&turnstile=${turnstileToken}`; if (captchaEnabled) { url += `&captcha_id=${encodeURIComponent(captchaId)}&captcha_code=${encodeURIComponent(captchaCode)}`; } const res = await API.get(url); const { success, message } = res.data; if (success) { showSuccess(t('验证码发送成功,请检查你的邮箱!')); setDisableButton(true); } else { showError(message); } if (captchaEnabled) loadCaptcha(); } catch (error) { showError(t('发送验证码失败,请重试')); } finally { setVerificationCodeLoading(false); } }; const handleGitHubClick = () => { if (githubButtonDisabled) { return; } setGithubLoading(true); setGithubButtonDisabled(true); setGithubButtonState('redirecting'); if (githubTimeoutRef.current) { clearTimeout(githubTimeoutRef.current); } githubTimeoutRef.current = setTimeout(() => { setGithubLoading(false); setGithubButtonState('timeout'); setGithubButtonDisabled(true); }, 20000); try { onGitHubOAuthClicked(status.github_client_id, { shouldLogout: true }); } finally { setTimeout(() => setGithubLoading(false), 3000); } }; const handleDiscordClick = () => { setDiscordLoading(true); try { onDiscordOAuthClicked(status.discord_client_id, { shouldLogout: true }); } finally { setTimeout(() => setDiscordLoading(false), 3000); } }; const handleOIDCClick = () => { setOidcLoading(true); try { onOIDCClicked( status.oidc_authorization_endpoint, status.oidc_client_id, false, { shouldLogout: true }, ); } finally { setTimeout(() => setOidcLoading(false), 3000); } }; const handleLinuxDOClick = () => { setLinuxdoLoading(true); try { onLinuxDOOAuthClicked(status.linuxdo_client_id, { shouldLogout: true }); } finally { setTimeout(() => setLinuxdoLoading(false), 3000); } }; const handleCustomOAuthClick = (provider) => { setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: true })); try { onCustomOAuthClicked(provider, { shouldLogout: true }); } finally { setTimeout(() => { setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: false })); }, 3000); } }; const handleEmailRegisterClick = () => { setEmailRegisterLoading(true); setShowEmailRegister(true); setEmailRegisterLoading(false); }; const handleOtherRegisterOptionsClick = () => { setOtherRegisterOptionsLoading(true); setShowEmailRegister(false); setOtherRegisterOptionsLoading(false); }; const onTelegramLoginClicked = async (response) => { const fields = [ 'id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash', 'lang', ]; const params = {}; fields.forEach((field) => { if (response[field]) { params[field] = response[field]; } }); try { const res = await API.get(`/api/oauth/telegram/login`, { params }); const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); showSuccess(t('登录成功!')); setUserData(data); updateAPI(); navigate('/'); } else { showError(message); } } catch (error) { showError(t('登录失败,请重试')); } }; const renderOAuthOptions = () => { return (
Logo {systemName}
{t('注 册')}
{status.wechat_login && ( )} {status.github_oauth && ( )} {status.discord_oauth && ( )} {status.oidc_enabled && ( )} {status.linuxdo_oauth && ( )} {status.custom_oauth_providers && status.custom_oauth_providers.map((provider) => ( ))} {status.telegram_oauth && (
)} {t('或')}
{t('已有账户?')}{' '} {t('登录')}
); }; const renderEmailRegisterForm = () => { return (
Logo {systemName}
{t('注 册')}
handleChange('username', value)} prefix={} /> handleChange('password', value)} prefix={} /> handleChange('password2', value)} prefix={} /> {showEmailVerification && ( <> handleChange('email', value)} prefix={} suffix={ } /> {captchaEnabled && (
setCaptchaCode(value)} value={captchaCode} prefix={} /> captcha
)} handleChange('verification_code', value) } prefix={} /> )} {(hasUserAgreement || hasPrivacyPolicy) && (
setAgreedToTerms(e.target.checked)} > {t('我已阅读并同意')} {hasUserAgreement && ( <> {t('用户协议')} )} {hasUserAgreement && hasPrivacyPolicy && t('和')} {hasPrivacyPolicy && ( <> {t('隐私政策')} )}
)}
{hasOAuthRegisterOptions && ( <> {t('或')}
)}
{t('已有账户?')}{' '} {t('登录')}
); }; const renderWeChatLoginModal = () => { return ( setShowWeChatLoginModal(false)} okText={t('登录')} centered={true} okButtonProps={{ loading: wechatCodeSubmitLoading, }} >
{t('微信二维码')}

{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}

handleChange('wechat_verification_code', value) } />
); }; return (
{/* 背景模糊晕染球 */}
{showEmailRegister || !hasOAuthRegisterOptions ? renderEmailRegisterForm() : renderOAuthOptions()} {renderWeChatLoginModal()} {turnstileEnabled && (
{ setTurnstileToken(token); }} />
)}
); }; export default RegisterForm;