Spaces:
Build error
Build error
| /* | |
| 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 <https://www.gnu.org/licenses/>. | |
| For commercial licensing, please contact support@quantumnous.com | |
| */ | |
| import React, { useContext, useEffect, useState } from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { | |
| API, | |
| copy, | |
| showError, | |
| showInfo, | |
| showSuccess, | |
| setStatusData, | |
| prepareCredentialCreationOptions, | |
| buildRegistrationResult, | |
| isPasskeySupported, | |
| setUserData, | |
| } from '../../helpers'; | |
| import { UserContext } from '../../context/User'; | |
| import { Modal } from '@douyinfe/semi-ui'; | |
| import { useTranslation } from 'react-i18next'; | |
| // 导入子组件 | |
| import UserInfoHeader from './personal/components/UserInfoHeader'; | |
| import AccountManagement from './personal/cards/AccountManagement'; | |
| import NotificationSettings from './personal/cards/NotificationSettings'; | |
| import EmailBindModal from './personal/modals/EmailBindModal'; | |
| import WeChatBindModal from './personal/modals/WeChatBindModal'; | |
| import AccountDeleteModal from './personal/modals/AccountDeleteModal'; | |
| import ChangePasswordModal from './personal/modals/ChangePasswordModal'; | |
| const PersonalSetting = () => { | |
| const [userState, userDispatch] = useContext(UserContext); | |
| let navigate = useNavigate(); | |
| const { t } = useTranslation(); | |
| const [inputs, setInputs] = useState({ | |
| wechat_verification_code: '', | |
| email_verification_code: '', | |
| email: '', | |
| self_account_deletion_confirmation: '', | |
| original_password: '', | |
| set_new_password: '', | |
| set_new_password_confirmation: '', | |
| }); | |
| const [status, setStatus] = useState({}); | |
| const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); | |
| const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); | |
| const [showEmailBindModal, setShowEmailBindModal] = useState(false); | |
| const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); | |
| const [turnstileEnabled, setTurnstileEnabled] = useState(false); | |
| const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | |
| const [turnstileToken, setTurnstileToken] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [disableButton, setDisableButton] = useState(false); | |
| const [countdown, setCountdown] = useState(30); | |
| const [systemToken, setSystemToken] = useState(''); | |
| const [passkeyStatus, setPasskeyStatus] = useState({ enabled: false }); | |
| const [passkeyRegisterLoading, setPasskeyRegisterLoading] = useState(false); | |
| const [passkeyDeleteLoading, setPasskeyDeleteLoading] = useState(false); | |
| const [passkeySupported, setPasskeySupported] = useState(false); | |
| const [notificationSettings, setNotificationSettings] = useState({ | |
| warningType: 'email', | |
| warningThreshold: 100000, | |
| webhookUrl: '', | |
| webhookSecret: '', | |
| notificationEmail: '', | |
| barkUrl: '', | |
| acceptUnsetModelRatioModel: false, | |
| recordIpLog: false, | |
| }); | |
| useEffect(() => { | |
| let saved = localStorage.getItem('status'); | |
| if (saved) { | |
| const parsed = JSON.parse(saved); | |
| setStatus(parsed); | |
| if (parsed.turnstile_check) { | |
| setTurnstileEnabled(true); | |
| setTurnstileSiteKey(parsed.turnstile_site_key); | |
| } else { | |
| setTurnstileEnabled(false); | |
| setTurnstileSiteKey(''); | |
| } | |
| } | |
| // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth) | |
| (async () => { | |
| try { | |
| const res = await API.get('/api/status'); | |
| const { success, data } = res.data; | |
| if (success && data) { | |
| setStatus(data); | |
| setStatusData(data); | |
| if (data.turnstile_check) { | |
| setTurnstileEnabled(true); | |
| setTurnstileSiteKey(data.turnstile_site_key); | |
| } else { | |
| setTurnstileEnabled(false); | |
| setTurnstileSiteKey(''); | |
| } | |
| } | |
| } catch (e) { | |
| // ignore and keep local status | |
| } | |
| })(); | |
| getUserData(); | |
| isPasskeySupported() | |
| .then(setPasskeySupported) | |
| .catch(() => setPasskeySupported(false)); | |
| }, []); | |
| 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(() => { | |
| if (userState?.user?.setting) { | |
| const settings = JSON.parse(userState.user.setting); | |
| setNotificationSettings({ | |
| warningType: settings.notify_type || 'email', | |
| warningThreshold: settings.quota_warning_threshold || 500000, | |
| webhookUrl: settings.webhook_url || '', | |
| webhookSecret: settings.webhook_secret || '', | |
| notificationEmail: settings.notification_email || '', | |
| barkUrl: settings.bark_url || '', | |
| acceptUnsetModelRatioModel: | |
| settings.accept_unset_model_ratio_model || false, | |
| recordIpLog: settings.record_ip_log || false, | |
| }); | |
| } | |
| }, [userState?.user?.setting]); | |
| const handleInputChange = (name, value) => { | |
| setInputs((inputs) => ({ ...inputs, [name]: value })); | |
| }; | |
| const generateAccessToken = async () => { | |
| const res = await API.get('/api/user/token'); | |
| const { success, message, data } = res.data; | |
| if (success) { | |
| setSystemToken(data); | |
| await copy(data); | |
| showSuccess(t('令牌已重置并已复制到剪贴板')); | |
| } else { | |
| showError(message); | |
| } | |
| }; | |
| const loadPasskeyStatus = async () => { | |
| try { | |
| const res = await API.get('/api/user/passkey'); | |
| const { success, data, message } = res.data; | |
| if (success) { | |
| setPasskeyStatus({ | |
| enabled: data?.enabled || false, | |
| last_used_at: data?.last_used_at || null, | |
| backup_eligible: data?.backup_eligible || false, | |
| backup_state: data?.backup_state || false, | |
| }); | |
| } else { | |
| showError(message); | |
| } | |
| } catch (error) { | |
| // 忽略错误,保留默认状态 | |
| } | |
| }; | |
| const handleRegisterPasskey = async () => { | |
| if (!passkeySupported || !window.PublicKeyCredential) { | |
| showInfo(t('当前设备不支持 Passkey')); | |
| return; | |
| } | |
| setPasskeyRegisterLoading(true); | |
| try { | |
| const beginRes = await API.post('/api/user/passkey/register/begin'); | |
| const { success, message, data } = beginRes.data; | |
| if (!success) { | |
| showError(message || t('无法发起 Passkey 注册')); | |
| return; | |
| } | |
| const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data); | |
| const credential = await navigator.credentials.create({ publicKey }); | |
| const payload = buildRegistrationResult(credential); | |
| if (!payload) { | |
| showError(t('Passkey 注册失败,请重试')); | |
| return; | |
| } | |
| const finishRes = await API.post('/api/user/passkey/register/finish', payload); | |
| if (finishRes.data.success) { | |
| showSuccess(t('Passkey 注册成功')); | |
| await loadPasskeyStatus(); | |
| } else { | |
| showError(finishRes.data.message || t('Passkey 注册失败,请重试')); | |
| } | |
| } catch (error) { | |
| if (error?.name === 'AbortError') { | |
| showInfo(t('已取消 Passkey 注册')); | |
| } else { | |
| showError(t('Passkey 注册失败,请重试')); | |
| } | |
| } finally { | |
| setPasskeyRegisterLoading(false); | |
| } | |
| }; | |
| const handleRemovePasskey = async () => { | |
| setPasskeyDeleteLoading(true); | |
| try { | |
| const res = await API.delete('/api/user/passkey'); | |
| const { success, message } = res.data; | |
| if (success) { | |
| showSuccess(t('Passkey 已解绑')); | |
| await loadPasskeyStatus(); | |
| } else { | |
| showError(message || t('操作失败,请重试')); | |
| } | |
| } catch (error) { | |
| showError(t('操作失败,请重试')); | |
| } finally { | |
| setPasskeyDeleteLoading(false); | |
| } | |
| }; | |
| const getUserData = async () => { | |
| let res = await API.get(`/api/user/self`); | |
| const { success, message, data } = res.data; | |
| if (success) { | |
| userDispatch({ type: 'login', payload: data }); | |
| setUserData(data); | |
| await loadPasskeyStatus(); | |
| } else { | |
| showError(message); | |
| } | |
| }; | |
| const handleSystemTokenClick = async (e) => { | |
| e.target.select(); | |
| await copy(e.target.value); | |
| showSuccess(t('系统令牌已复制到剪切板')); | |
| }; | |
| const deleteAccount = async () => { | |
| if (inputs.self_account_deletion_confirmation !== userState.user.username) { | |
| showError(t('请输入你的账户名以确认删除!')); | |
| return; | |
| } | |
| const res = await API.delete('/api/user/self'); | |
| const { success, message } = res.data; | |
| if (success) { | |
| showSuccess(t('账户已删除!')); | |
| await API.get('/api/user/logout'); | |
| userDispatch({ type: 'logout' }); | |
| localStorage.removeItem('user'); | |
| navigate('/login'); | |
| } else { | |
| showError(message); | |
| } | |
| }; | |
| const bindWeChat = async () => { | |
| if (inputs.wechat_verification_code === '') return; | |
| const res = await API.get( | |
| `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, | |
| ); | |
| const { success, message } = res.data; | |
| if (success) { | |
| showSuccess(t('微信账户绑定成功!')); | |
| setShowWeChatBindModal(false); | |
| } else { | |
| showError(message); | |
| } | |
| }; | |
| const changePassword = async () => { | |
| if (inputs.original_password === '') { | |
| showError(t('请输入原密码!')); | |
| return; | |
| } | |
| if (inputs.set_new_password === '') { | |
| showError(t('请输入新密码!')); | |
| return; | |
| } | |
| if (inputs.original_password === inputs.set_new_password) { | |
| showError(t('新密码需要和原密码不一致!')); | |
| return; | |
| } | |
| if (inputs.set_new_password !== inputs.set_new_password_confirmation) { | |
| showError(t('两次输入的密码不一致!')); | |
| return; | |
| } | |
| const res = await API.put(`/api/user/self`, { | |
| original_password: inputs.original_password, | |
| password: inputs.set_new_password, | |
| }); | |
| const { success, message } = res.data; | |
| if (success) { | |
| showSuccess(t('密码修改成功!')); | |
| setShowWeChatBindModal(false); | |
| } else { | |
| showError(message); | |
| } | |
| setShowChangePasswordModal(false); | |
| }; | |
| const sendVerificationCode = async () => { | |
| if (inputs.email === '') { | |
| showError(t('请输入邮箱!')); | |
| return; | |
| } | |
| setDisableButton(true); | |
| if (turnstileEnabled && turnstileToken === '') { | |
| showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!')); | |
| return; | |
| } | |
| setLoading(true); | |
| const res = await API.get( | |
| `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, | |
| ); | |
| const { success, message } = res.data; | |
| if (success) { | |
| showSuccess(t('验证码发送成功,请检查邮箱!')); | |
| } else { | |
| showError(message); | |
| } | |
| setLoading(false); | |
| }; | |
| const bindEmail = async () => { | |
| if (inputs.email_verification_code === '') { | |
| showError(t('请输入邮箱验证码!')); | |
| return; | |
| } | |
| setLoading(true); | |
| const res = await API.get( | |
| `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, | |
| ); | |
| const { success, message } = res.data; | |
| if (success) { | |
| showSuccess(t('邮箱账户绑定成功!')); | |
| setShowEmailBindModal(false); | |
| userState.user.email = inputs.email; | |
| } else { | |
| showError(message); | |
| } | |
| setLoading(false); | |
| }; | |
| const copyText = async (text) => { | |
| if (await copy(text)) { | |
| showSuccess(t('已复制:') + text); | |
| } else { | |
| // setSearchKeyword(text); | |
| Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); | |
| } | |
| }; | |
| const handleNotificationSettingChange = (type, value) => { | |
| setNotificationSettings((prev) => ({ | |
| ...prev, | |
| [type]: value.target | |
| ? value.target.value !== undefined | |
| ? value.target.value | |
| : value.target.checked | |
| : value, // handle checkbox properly | |
| })); | |
| }; | |
| const saveNotificationSettings = async () => { | |
| try { | |
| const res = await API.put('/api/user/setting', { | |
| notify_type: notificationSettings.warningType, | |
| quota_warning_threshold: parseFloat( | |
| notificationSettings.warningThreshold, | |
| ), | |
| webhook_url: notificationSettings.webhookUrl, | |
| webhook_secret: notificationSettings.webhookSecret, | |
| notification_email: notificationSettings.notificationEmail, | |
| bark_url: notificationSettings.barkUrl, | |
| accept_unset_model_ratio_model: | |
| notificationSettings.acceptUnsetModelRatioModel, | |
| record_ip_log: notificationSettings.recordIpLog, | |
| }); | |
| if (res.data.success) { | |
| showSuccess(t('设置保存成功')); | |
| await getUserData(); | |
| } else { | |
| showError(res.data.message); | |
| } | |
| } catch (error) { | |
| showError(t('设置保存失败')); | |
| } | |
| }; | |
| return ( | |
| <div className='mt-[60px]'> | |
| <div className='flex justify-center'> | |
| <div className='w-full max-w-7xl mx-auto px-2'> | |
| {/* 顶部用户信息区域 */} | |
| <UserInfoHeader t={t} userState={userState} /> | |
| {/* 账户管理和其他设置 */} | |
| <div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'> | |
| {/* 左侧:账户管理设置 */} | |
| <AccountManagement | |
| t={t} | |
| userState={userState} | |
| status={status} | |
| systemToken={systemToken} | |
| setShowEmailBindModal={setShowEmailBindModal} | |
| setShowWeChatBindModal={setShowWeChatBindModal} | |
| generateAccessToken={generateAccessToken} | |
| handleSystemTokenClick={handleSystemTokenClick} | |
| setShowChangePasswordModal={setShowChangePasswordModal} | |
| setShowAccountDeleteModal={setShowAccountDeleteModal} | |
| passkeyStatus={passkeyStatus} | |
| passkeySupported={passkeySupported} | |
| passkeyRegisterLoading={passkeyRegisterLoading} | |
| passkeyDeleteLoading={passkeyDeleteLoading} | |
| onPasskeyRegister={handleRegisterPasskey} | |
| onPasskeyDelete={handleRemovePasskey} | |
| /> | |
| {/* 右侧:其他设置 */} | |
| <NotificationSettings | |
| t={t} | |
| notificationSettings={notificationSettings} | |
| handleNotificationSettingChange={handleNotificationSettingChange} | |
| saveNotificationSettings={saveNotificationSettings} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 模态框组件 */} | |
| <EmailBindModal | |
| t={t} | |
| showEmailBindModal={showEmailBindModal} | |
| setShowEmailBindModal={setShowEmailBindModal} | |
| inputs={inputs} | |
| handleInputChange={handleInputChange} | |
| sendVerificationCode={sendVerificationCode} | |
| bindEmail={bindEmail} | |
| disableButton={disableButton} | |
| loading={loading} | |
| countdown={countdown} | |
| turnstileEnabled={turnstileEnabled} | |
| turnstileSiteKey={turnstileSiteKey} | |
| setTurnstileToken={setTurnstileToken} | |
| /> | |
| <WeChatBindModal | |
| t={t} | |
| showWeChatBindModal={showWeChatBindModal} | |
| setShowWeChatBindModal={setShowWeChatBindModal} | |
| inputs={inputs} | |
| handleInputChange={handleInputChange} | |
| bindWeChat={bindWeChat} | |
| status={status} | |
| /> | |
| <AccountDeleteModal | |
| t={t} | |
| showAccountDeleteModal={showAccountDeleteModal} | |
| setShowAccountDeleteModal={setShowAccountDeleteModal} | |
| inputs={inputs} | |
| handleInputChange={handleInputChange} | |
| deleteAccount={deleteAccount} | |
| userState={userState} | |
| turnstileEnabled={turnstileEnabled} | |
| turnstileSiteKey={turnstileSiteKey} | |
| setTurnstileToken={setTurnstileToken} | |
| /> | |
| <ChangePasswordModal | |
| t={t} | |
| showChangePasswordModal={showChangePasswordModal} | |
| setShowChangePasswordModal={setShowChangePasswordModal} | |
| inputs={inputs} | |
| handleInputChange={handleInputChange} | |
| changePassword={changePassword} | |
| turnstileEnabled={turnstileEnabled} | |
| turnstileSiteKey={turnstileSiteKey} | |
| setTurnstileToken={setTurnstileToken} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default PersonalSetting; | |