/* 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, 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: '', gotifyUrl: '', gotifyToken: '', gotifyPriority: 5, 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 || '', gotifyUrl: settings.gotify_url || '', gotifyToken: settings.gotify_token || '', gotifyPriority: settings.gotify_priority !== undefined ? settings.gotify_priority : 5, 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, gotify_url: notificationSettings.gotifyUrl, gotify_token: notificationSettings.gotifyToken, gotify_priority: (() => { const parsed = parseInt(notificationSettings.gotifyPriority); return isNaN(parsed) ? 5 : parsed; })(), 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 (
{/* 顶部用户信息区域 */} {/* 账户管理和其他设置 */}
{/* 左侧:账户管理设置 */} {/* 右侧:其他设置 */}
{/* 模态框组件 */}
); }; export default PersonalSetting;