PersonalSetting.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useContext, useEffect, useState } from 'react';
  16. import { useNavigate } from 'react-router-dom';
  17. import {
  18. API,
  19. copy,
  20. showError,
  21. showInfo,
  22. showSuccess,
  23. setStatusData,
  24. setUserData,
  25. } from '../../helpers';
  26. import { UserContext } from '../../context/User';
  27. import { Modal } from '@douyinfe/semi-ui';
  28. import { useTranslation } from 'react-i18next';
  29. // 导入子组件
  30. import UserInfoHeader from './personal/components/UserInfoHeader';
  31. import AccountManagement from './personal/cards/AccountManagement';
  32. import NotificationSettings from './personal/cards/NotificationSettings';
  33. import EmailBindModal from './personal/modals/EmailBindModal';
  34. import WeChatBindModal from './personal/modals/WeChatBindModal';
  35. import AccountDeleteModal from './personal/modals/AccountDeleteModal';
  36. import ChangePasswordModal from './personal/modals/ChangePasswordModal';
  37. const PersonalSetting = () => {
  38. const [userState, userDispatch] = useContext(UserContext);
  39. let navigate = useNavigate();
  40. const { t } = useTranslation();
  41. const [inputs, setInputs] = useState({
  42. wechat_verification_code: '',
  43. email_verification_code: '',
  44. email: '',
  45. self_account_deletion_confirmation: '',
  46. original_password: '',
  47. set_new_password: '',
  48. set_new_password_confirmation: '',
  49. });
  50. const [status, setStatus] = useState({});
  51. const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
  52. const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
  53. const [showEmailBindModal, setShowEmailBindModal] = useState(false);
  54. const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
  55. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  56. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  57. const [turnstileToken, setTurnstileToken] = useState('');
  58. const [loading, setLoading] = useState(false);
  59. const [disableButton, setDisableButton] = useState(false);
  60. const [countdown, setCountdown] = useState(30);
  61. const [systemToken, setSystemToken] = useState('');
  62. const [notificationSettings, setNotificationSettings] = useState({
  63. warningType: 'email',
  64. warningThreshold: 100000,
  65. webhookUrl: '',
  66. webhookSecret: '',
  67. notificationEmail: '',
  68. barkUrl: '',
  69. acceptUnsetModelRatioModel: false,
  70. recordIpLog: false,
  71. });
  72. useEffect(() => {
  73. let saved = localStorage.getItem('status');
  74. if (saved) {
  75. const parsed = JSON.parse(saved);
  76. setStatus(parsed);
  77. if (parsed.turnstile_check) {
  78. setTurnstileEnabled(true);
  79. setTurnstileSiteKey(parsed.turnstile_site_key);
  80. } else {
  81. setTurnstileEnabled(false);
  82. setTurnstileSiteKey('');
  83. }
  84. }
  85. // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
  86. (async () => {
  87. try {
  88. const res = await API.get('/api/status');
  89. const { success, data } = res.data;
  90. if (success && data) {
  91. setStatus(data);
  92. setStatusData(data);
  93. if (data.turnstile_check) {
  94. setTurnstileEnabled(true);
  95. setTurnstileSiteKey(data.turnstile_site_key);
  96. } else {
  97. setTurnstileEnabled(false);
  98. setTurnstileSiteKey('');
  99. }
  100. }
  101. } catch (e) {
  102. // ignore and keep local status
  103. }
  104. })();
  105. getUserData();
  106. }, []);
  107. useEffect(() => {
  108. let countdownInterval = null;
  109. if (disableButton && countdown > 0) {
  110. countdownInterval = setInterval(() => {
  111. setCountdown(countdown - 1);
  112. }, 1000);
  113. } else if (countdown === 0) {
  114. setDisableButton(false);
  115. setCountdown(30);
  116. }
  117. return () => clearInterval(countdownInterval); // Clean up on unmount
  118. }, [disableButton, countdown]);
  119. useEffect(() => {
  120. if (userState?.user?.setting) {
  121. const settings = JSON.parse(userState.user.setting);
  122. setNotificationSettings({
  123. warningType: settings.notify_type || 'email',
  124. warningThreshold: settings.quota_warning_threshold || 500000,
  125. webhookUrl: settings.webhook_url || '',
  126. webhookSecret: settings.webhook_secret || '',
  127. notificationEmail: settings.notification_email || '',
  128. barkUrl: settings.bark_url || '',
  129. acceptUnsetModelRatioModel:
  130. settings.accept_unset_model_ratio_model || false,
  131. recordIpLog: settings.record_ip_log || false,
  132. });
  133. }
  134. }, [userState?.user?.setting]);
  135. const handleInputChange = (name, value) => {
  136. setInputs((inputs) => ({ ...inputs, [name]: value }));
  137. };
  138. const generateAccessToken = async () => {
  139. const res = await API.get('/api/user/token');
  140. const { success, message, data } = res.data;
  141. if (success) {
  142. setSystemToken(data);
  143. await copy(data);
  144. showSuccess(t('令牌已重置并已复制到剪贴板'));
  145. } else {
  146. showError(message);
  147. }
  148. };
  149. const getUserData = async () => {
  150. let res = await API.get(`/api/user/self`);
  151. const { success, message, data } = res.data;
  152. if (success) {
  153. userDispatch({ type: 'login', payload: data });
  154. setUserData(data);
  155. } else {
  156. showError(message);
  157. }
  158. };
  159. const handleSystemTokenClick = async (e) => {
  160. e.target.select();
  161. await copy(e.target.value);
  162. showSuccess(t('系统令牌已复制到剪切板'));
  163. };
  164. const deleteAccount = async () => {
  165. if (inputs.self_account_deletion_confirmation !== userState.user.username) {
  166. showError(t('请输入你的账户名以确认删除!'));
  167. return;
  168. }
  169. const res = await API.delete('/api/user/self');
  170. const { success, message } = res.data;
  171. if (success) {
  172. showSuccess(t('账户已删除!'));
  173. await API.get('/api/user/logout');
  174. userDispatch({ type: 'logout' });
  175. localStorage.removeItem('user');
  176. navigate('/login');
  177. } else {
  178. showError(message);
  179. }
  180. };
  181. const bindWeChat = async () => {
  182. if (inputs.wechat_verification_code === '') return;
  183. const res = await API.get(
  184. `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
  185. );
  186. const { success, message } = res.data;
  187. if (success) {
  188. showSuccess(t('微信账户绑定成功!'));
  189. setShowWeChatBindModal(false);
  190. } else {
  191. showError(message);
  192. }
  193. };
  194. const changePassword = async () => {
  195. if (inputs.original_password === '') {
  196. showError(t('请输入原密码!'));
  197. return;
  198. }
  199. if (inputs.set_new_password === '') {
  200. showError(t('请输入新密码!'));
  201. return;
  202. }
  203. if (inputs.original_password === inputs.set_new_password) {
  204. showError(t('新密码需要和原密码不一致!'));
  205. return;
  206. }
  207. if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
  208. showError(t('两次输入的密码不一致!'));
  209. return;
  210. }
  211. const res = await API.put(`/api/user/self`, {
  212. original_password: inputs.original_password,
  213. password: inputs.set_new_password,
  214. });
  215. const { success, message } = res.data;
  216. if (success) {
  217. showSuccess(t('密码修改成功!'));
  218. setShowWeChatBindModal(false);
  219. } else {
  220. showError(message);
  221. }
  222. setShowChangePasswordModal(false);
  223. };
  224. const sendVerificationCode = async () => {
  225. if (inputs.email === '') {
  226. showError(t('请输入邮箱!'));
  227. return;
  228. }
  229. setDisableButton(true);
  230. if (turnstileEnabled && turnstileToken === '') {
  231. showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
  232. return;
  233. }
  234. setLoading(true);
  235. const res = await API.get(
  236. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  237. );
  238. const { success, message } = res.data;
  239. if (success) {
  240. showSuccess(t('验证码发送成功,请检查邮箱!'));
  241. } else {
  242. showError(message);
  243. }
  244. setLoading(false);
  245. };
  246. const bindEmail = async () => {
  247. if (inputs.email_verification_code === '') {
  248. showError(t('请输入邮箱验证码!'));
  249. return;
  250. }
  251. setLoading(true);
  252. const res = await API.get(
  253. `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
  254. );
  255. const { success, message } = res.data;
  256. if (success) {
  257. showSuccess(t('邮箱账户绑定成功!'));
  258. setShowEmailBindModal(false);
  259. userState.user.email = inputs.email;
  260. } else {
  261. showError(message);
  262. }
  263. setLoading(false);
  264. };
  265. const copyText = async (text) => {
  266. if (await copy(text)) {
  267. showSuccess(t('已复制:') + text);
  268. } else {
  269. // setSearchKeyword(text);
  270. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  271. }
  272. };
  273. const handleNotificationSettingChange = (type, value) => {
  274. setNotificationSettings((prev) => ({
  275. ...prev,
  276. [type]: value.target
  277. ? value.target.value !== undefined
  278. ? value.target.value
  279. : value.target.checked
  280. : value, // handle checkbox properly
  281. }));
  282. };
  283. const saveNotificationSettings = async () => {
  284. try {
  285. const res = await API.put('/api/user/setting', {
  286. notify_type: notificationSettings.warningType,
  287. quota_warning_threshold: parseFloat(
  288. notificationSettings.warningThreshold,
  289. ),
  290. webhook_url: notificationSettings.webhookUrl,
  291. webhook_secret: notificationSettings.webhookSecret,
  292. notification_email: notificationSettings.notificationEmail,
  293. bark_url: notificationSettings.barkUrl,
  294. accept_unset_model_ratio_model:
  295. notificationSettings.acceptUnsetModelRatioModel,
  296. record_ip_log: notificationSettings.recordIpLog,
  297. });
  298. if (res.data.success) {
  299. showSuccess(t('设置保存成功'));
  300. await getUserData();
  301. } else {
  302. showError(res.data.message);
  303. }
  304. } catch (error) {
  305. showError(t('设置保存失败'));
  306. }
  307. };
  308. return (
  309. <div className='mt-[60px]'>
  310. <div className='flex justify-center'>
  311. <div className='w-full max-w-7xl mx-auto px-2'>
  312. {/* 顶部用户信息区域 */}
  313. <UserInfoHeader t={t} userState={userState} />
  314. {/* 账户管理和其他设置 */}
  315. <div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
  316. {/* 左侧:账户管理设置 */}
  317. <AccountManagement
  318. t={t}
  319. userState={userState}
  320. status={status}
  321. systemToken={systemToken}
  322. setShowEmailBindModal={setShowEmailBindModal}
  323. setShowWeChatBindModal={setShowWeChatBindModal}
  324. generateAccessToken={generateAccessToken}
  325. handleSystemTokenClick={handleSystemTokenClick}
  326. setShowChangePasswordModal={setShowChangePasswordModal}
  327. setShowAccountDeleteModal={setShowAccountDeleteModal}
  328. />
  329. {/* 右侧:其他设置 */}
  330. <NotificationSettings
  331. t={t}
  332. notificationSettings={notificationSettings}
  333. handleNotificationSettingChange={handleNotificationSettingChange}
  334. saveNotificationSettings={saveNotificationSettings}
  335. />
  336. </div>
  337. </div>
  338. </div>
  339. {/* 模态框组件 */}
  340. <EmailBindModal
  341. t={t}
  342. showEmailBindModal={showEmailBindModal}
  343. setShowEmailBindModal={setShowEmailBindModal}
  344. inputs={inputs}
  345. handleInputChange={handleInputChange}
  346. sendVerificationCode={sendVerificationCode}
  347. bindEmail={bindEmail}
  348. disableButton={disableButton}
  349. loading={loading}
  350. countdown={countdown}
  351. turnstileEnabled={turnstileEnabled}
  352. turnstileSiteKey={turnstileSiteKey}
  353. setTurnstileToken={setTurnstileToken}
  354. />
  355. <WeChatBindModal
  356. t={t}
  357. showWeChatBindModal={showWeChatBindModal}
  358. setShowWeChatBindModal={setShowWeChatBindModal}
  359. inputs={inputs}
  360. handleInputChange={handleInputChange}
  361. bindWeChat={bindWeChat}
  362. status={status}
  363. />
  364. <AccountDeleteModal
  365. t={t}
  366. showAccountDeleteModal={showAccountDeleteModal}
  367. setShowAccountDeleteModal={setShowAccountDeleteModal}
  368. inputs={inputs}
  369. handleInputChange={handleInputChange}
  370. deleteAccount={deleteAccount}
  371. userState={userState}
  372. turnstileEnabled={turnstileEnabled}
  373. turnstileSiteKey={turnstileSiteKey}
  374. setTurnstileToken={setTurnstileToken}
  375. />
  376. <ChangePasswordModal
  377. t={t}
  378. showChangePasswordModal={showChangePasswordModal}
  379. setShowChangePasswordModal={setShowChangePasswordModal}
  380. inputs={inputs}
  381. handleInputChange={handleInputChange}
  382. changePassword={changePassword}
  383. turnstileEnabled={turnstileEnabled}
  384. turnstileSiteKey={turnstileSiteKey}
  385. setTurnstileToken={setTurnstileToken}
  386. />
  387. </div>
  388. );
  389. };
  390. export default PersonalSetting;