/* 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, useRef, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { UserContext } from '../../context/User'; import { API, getLogo, showError, showInfo, showSuccess, updateAPI, getSystemName, setUserData, onGitHubOAuthClicked, onDiscordOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked, prepareCredentialRequestOptions, buildAssertionResult, isPasskeySupported, } 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 TelegramLoginButton from 'react-telegram-login'; import { IconGithubLogo, IconMail, IconLock, IconKey, } from '@douyinfe/semi-icons'; import OIDCIcon from '../common/logo/OIDCIcon'; import WeChatIcon from '../common/logo/WeChatIcon'; import LinuxDoIcon from '../common/logo/LinuxDoIcon'; import TwoFAVerification from './TwoFAVerification'; import { useTranslation } from 'react-i18next'; import { SiDiscord }from 'react-icons/si'; const LoginForm = () => { let navigate = useNavigate(); const { t } = useTranslation(); const [inputs, setInputs] = useState({ username: '', password: '', wechat_verification_code: '', }); const { username, password } = inputs; const [searchParams, setSearchParams] = useSearchParams(); const [submitted, setSubmitted] = useState(false); const [userState, userDispatch] = useContext(UserContext); const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileToken, setTurnstileToken] = useState(''); const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); const [showEmailLogin, setShowEmailLogin] = 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 [emailLoginLoading, setEmailLoginLoading] = useState(false); const [loginLoading, setLoginLoading] = useState(false); const [resetPasswordLoading, setResetPasswordLoading] = useState(false); const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = useState(false); const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [showTwoFA, setShowTwoFA] = useState(false); const [passkeySupported, setPasskeySupported] = useState(false); const [passkeyLoading, setPasskeyLoading] = useState(false); const [agreedToTerms, setAgreedToTerms] = useState(false); const [hasUserAgreement, setHasUserAgreement] = useState(false); const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); const [githubButtonText, setGithubButtonText] = useState('使用 GitHub 继续'); const [githubButtonDisabled, setGithubButtonDisabled] = useState(false); const githubTimeoutRef = useRef(null); const logo = getLogo(); const systemName = getSystemName(); let affCode = new URLSearchParams(window.location.search).get('aff'); if (affCode) { localStorage.setItem('aff', affCode); } const [status] = useState(() => { const savedStatus = localStorage.getItem('status'); return savedStatus ? JSON.parse(savedStatus) : {}; }); useEffect(() => { 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(() => { isPasskeySupported() .then(setPasskeySupported) .catch(() => setPasskeySupported(false)); return () => { if (githubTimeoutRef.current) { clearTimeout(githubTimeoutRef.current); } }; }, []); useEffect(() => { if (searchParams.get('expired')) { showError(t('未登录或登录已过期,请重新登录')); } }, []); const onWeChatLoginClicked = () => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } setWechatLoading(true); setShowWeChatLoginModal(true); setWechatLoading(false); }; const onSubmitWeChatVerificationCode = async () => { if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,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('登录成功!'); setShowWeChatLoginModal(false); } else { showError(message); } } catch (error) { showError('登录失败,请重试'); } finally { setWechatCodeSubmitLoading(false); } }; function handleChange(name, value) { setInputs((inputs) => ({ ...inputs, [name]: value })); } async function handleSubmit(e) { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); return; } setSubmitted(true); setLoginLoading(true); try { if (username && password) { const res = await API.post( `/api/user/login?turnstile=${turnstileToken}`, { username, password, }, ); const { success, message, data } = res.data; if (success) { // 检查是否需要2FA验证 if (data && data.require_2fa) { setShowTwoFA(true); setLoginLoading(false); return; } userDispatch({ type: 'login', payload: data }); setUserData(data); updateAPI(); showSuccess('登录成功!'); if (username === 'root' && password === '123456') { Modal.error({ title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true, }); } navigate('/console'); } else { showError(message); } } else { showError('请输入用户名和密码!'); } } catch (error) { showError('登录失败,请重试'); } finally { setLoginLoading(false); } } // 添加Telegram登录处理函数 const onTelegramLoginClicked = async (response) => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } 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('登录成功!'); setUserData(data); updateAPI(); navigate('/'); } else { showError(message); } } catch (error) { showError('登录失败,请重试'); } }; // 包装的GitHub登录点击处理 const handleGitHubClick = () => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } if (githubButtonDisabled) { return; } setGithubLoading(true); setGithubButtonDisabled(true); setGithubButtonText(t('正在跳转 GitHub...')); if (githubTimeoutRef.current) { clearTimeout(githubTimeoutRef.current); } githubTimeoutRef.current = setTimeout(() => { setGithubLoading(false); setGithubButtonText(t('请求超时,请刷新页面后重新发起 GitHub 登录')); setGithubButtonDisabled(true); }, 20000); try { onGitHubOAuthClicked(status.github_client_id); } finally { // 由于重定向,这里不会执行到,但为了完整性添加 setTimeout(() => setGithubLoading(false), 3000); } }; // 包装的Discord登录点击处理 const handleDiscordClick = () => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } setDiscordLoading(true); try { onDiscordOAuthClicked(status.discord_client_id); } finally { // 由于重定向,这里不会执行到,但为了完整性添加 setTimeout(() => setDiscordLoading(false), 3000); } }; // 包装的OIDC登录点击处理 const handleOIDCClick = () => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } setOidcLoading(true); try { onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id); } finally { // 由于重定向,这里不会执行到,但为了完整性添加 setTimeout(() => setOidcLoading(false), 3000); } }; // 包装的LinuxDO登录点击处理 const handleLinuxDOClick = () => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } setLinuxdoLoading(true); try { onLinuxDOOAuthClicked(status.linuxdo_client_id); } finally { // 由于重定向,这里不会执行到,但为了完整性添加 setTimeout(() => setLinuxdoLoading(false), 3000); } }; // 包装的邮箱登录选项点击处理 const handleEmailLoginClick = () => { setEmailLoginLoading(true); setShowEmailLogin(true); setEmailLoginLoading(false); }; const handlePasskeyLogin = async () => { if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { showInfo(t('请先阅读并同意用户协议和隐私政策')); return; } if (!passkeySupported) { showInfo('当前环境无法使用 Passkey 登录'); return; } if (!window.PublicKeyCredential) { showInfo('当前浏览器不支持 Passkey'); return; } setPasskeyLoading(true); try { const beginRes = await API.post('/api/user/passkey/login/begin'); const { success, message, data } = beginRes.data; if (!success) { showError(message || '无法发起 Passkey 登录'); return; } const publicKeyOptions = prepareCredentialRequestOptions( data?.options || data?.publicKey || data, ); const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions, }); const payload = buildAssertionResult(assertion); if (!payload) { showError('Passkey 验证失败,请重试'); return; } const finishRes = await API.post( '/api/user/passkey/login/finish', payload, ); const finish = finishRes.data; if (finish.success) { userDispatch({ type: 'login', payload: finish.data }); setUserData(finish.data); updateAPI(); showSuccess('登录成功!'); navigate('/console'); } else { showError(finish.message || 'Passkey 登录失败,请重试'); } } catch (error) { if (error?.name === 'AbortError') { showInfo('已取消 Passkey 登录'); } else { showError('Passkey 登录失败,请重试'); } } finally { setPasskeyLoading(false); } }; // 包装的重置密码点击处理 const handleResetPasswordClick = () => { setResetPasswordLoading(true); navigate('/reset'); setResetPasswordLoading(false); }; // 包装的其他登录选项点击处理 const handleOtherLoginOptionsClick = () => { setOtherLoginOptionsLoading(true); setShowEmailLogin(false); setOtherLoginOptionsLoading(false); }; // 2FA验证成功处理 const handle2FASuccess = (data) => { userDispatch({ type: 'login', payload: data }); setUserData(data); updateAPI(); showSuccess('登录成功!'); navigate('/console'); }; // 返回登录页面 const handleBackToLogin = () => { setShowTwoFA(false); setInputs({ username: '', password: '', wechat_verification_code: '' }); }; const renderOAuthOptions = () => { return (
Logo {systemName}
{t('登 录')}
{status.wechat_login && ( )} {status.github_oauth && ( )} {status.discord_oauth && ( )} {status.oidc_enabled && ( )} {status.linuxdo_oauth && ( )} {status.telegram_oauth && (
)} {status.passkey_login && passkeySupported && ( )} {t('或')}
{(hasUserAgreement || hasPrivacyPolicy) && (
setAgreedToTerms(e.target.checked)} > {t('我已阅读并同意')} {hasUserAgreement && ( <> {t('用户协议')} )} {hasUserAgreement && hasPrivacyPolicy && t('和')} {hasPrivacyPolicy && ( <> {t('隐私政策')} )}
)} {!status.self_use_mode_enabled && (
{t('没有账户?')}{' '} {t('注册')}
)}
); }; const renderEmailLoginForm = () => { return (
Logo {systemName}
{t('登 录')}
{status.passkey_login && passkeySupported && ( )}
handleChange('username', value)} prefix={} /> handleChange('password', value)} prefix={} /> {(hasUserAgreement || hasPrivacyPolicy) && (
setAgreedToTerms(e.target.checked)} > {t('我已阅读并同意')} {hasUserAgreement && ( <> {t('用户协议')} )} {hasUserAgreement && hasPrivacyPolicy && t('和')} {hasPrivacyPolicy && ( <> {t('隐私政策')} )}
)}
{(status.github_oauth || status.discord_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && ( <> {t('或')}
)} {!status.self_use_mode_enabled && (
{t('没有账户?')}{' '} {t('注册')}
)}
); }; // 微信登录模态框 const renderWeChatLoginModal = () => { return ( setShowWeChatLoginModal(false)} okText={t('登录')} centered={true} okButtonProps={{ loading: wechatCodeSubmitLoading, }} >
微信二维码

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

handleChange('wechat_verification_code', value) } />
); }; // 2FA验证弹窗 const render2FAModal = () => { return (
两步验证 } visible={showTwoFA} onCancel={handleBackToLogin} footer={null} width={450} centered >
); }; return (
{/* 背景模糊晕染球 */}
{showEmailLogin || !( status.github_oauth || status.discord_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth ) ? renderEmailLoginForm() : renderOAuthOptions()} {renderWeChatLoginModal()} {render2FAModal()} {turnstileEnabled && (
{ setTurnstileToken(token); }} />
)}
); }; export default LoginForm;