AccountManagement.jsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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 from 'react';
  16. import {
  17. Button,
  18. Card,
  19. Input,
  20. Space,
  21. Typography,
  22. Avatar,
  23. Tabs,
  24. TabPane,
  25. Popover,
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. IconMail,
  29. IconShield,
  30. IconGithubLogo,
  31. IconKey,
  32. IconLock,
  33. IconDelete,
  34. } from '@douyinfe/semi-icons';
  35. import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
  36. import { UserPlus, ShieldCheck } from 'lucide-react';
  37. import TelegramLoginButton from 'react-telegram-login';
  38. import {
  39. onGitHubOAuthClicked,
  40. onOIDCClicked,
  41. onLinuxDOOAuthClicked,
  42. } from '../../../../helpers';
  43. import TwoFASetting from '../components/TwoFASetting';
  44. const AccountManagement = ({
  45. t,
  46. userState,
  47. status,
  48. systemToken,
  49. setShowEmailBindModal,
  50. setShowWeChatBindModal,
  51. generateAccessToken,
  52. handleSystemTokenClick,
  53. setShowChangePasswordModal,
  54. setShowAccountDeleteModal,
  55. }) => {
  56. const renderAccountInfo = (accountId, label) => {
  57. if (!accountId || accountId === '') {
  58. return <span className='text-gray-500'>{t('未绑定')}</span>;
  59. }
  60. const popContent = (
  61. <div className='text-xs p-2'>
  62. <Typography.Paragraph copyable={{ content: accountId }}>
  63. {accountId}
  64. </Typography.Paragraph>
  65. {label ? (
  66. <div className='mt-1 text-[11px] text-gray-500'>{label}</div>
  67. ) : null}
  68. </div>
  69. );
  70. return (
  71. <Popover content={popContent} position='top' trigger='hover'>
  72. <span className='block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer'>
  73. {accountId}
  74. </span>
  75. </Popover>
  76. );
  77. };
  78. return (
  79. <Card className='!rounded-2xl'>
  80. {/* 卡片头部 */}
  81. <div className='flex items-center mb-4'>
  82. <Avatar size='small' color='teal' className='mr-3 shadow-md'>
  83. <UserPlus size={16} />
  84. </Avatar>
  85. <div>
  86. <Typography.Text className='text-lg font-medium'>
  87. {t('账户管理')}
  88. </Typography.Text>
  89. <div className='text-xs text-gray-600'>
  90. {t('账户绑定、安全设置和身份验证')}
  91. </div>
  92. </div>
  93. </div>
  94. <Tabs type='card' defaultActiveKey='binding'>
  95. {/* 账户绑定 Tab */}
  96. <TabPane
  97. tab={
  98. <div className='flex items-center'>
  99. <UserPlus size={16} className='mr-2' />
  100. {t('账户绑定')}
  101. </div>
  102. }
  103. itemKey='binding'
  104. >
  105. <div className='py-4'>
  106. <div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
  107. {/* 邮箱绑定 */}
  108. <Card className='!rounded-xl'>
  109. <div className='flex items-center justify-between gap-3'>
  110. <div className='flex items-center flex-1 min-w-0'>
  111. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  112. <IconMail
  113. size='default'
  114. className='text-slate-600 dark:text-slate-300'
  115. />
  116. </div>
  117. <div className='flex-1 min-w-0'>
  118. <div className='font-medium text-gray-900'>
  119. {t('邮箱')}
  120. </div>
  121. <div className='text-sm text-gray-500 truncate'>
  122. {renderAccountInfo(
  123. userState.user?.email,
  124. t('邮箱地址'),
  125. )}
  126. </div>
  127. </div>
  128. </div>
  129. <div className='flex-shrink-0'>
  130. <Button
  131. type='primary'
  132. theme='outline'
  133. size='small'
  134. onClick={() => setShowEmailBindModal(true)}
  135. >
  136. {userState.user && userState.user.email !== ''
  137. ? t('修改绑定')
  138. : t('绑定')}
  139. </Button>
  140. </div>
  141. </div>
  142. </Card>
  143. {/* 微信绑定 */}
  144. <Card className='!rounded-xl'>
  145. <div className='flex items-center justify-between gap-3'>
  146. <div className='flex items-center flex-1 min-w-0'>
  147. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  148. <SiWechat
  149. size={20}
  150. className='text-slate-600 dark:text-slate-300'
  151. />
  152. </div>
  153. <div className='flex-1 min-w-0'>
  154. <div className='font-medium text-gray-900'>
  155. {t('微信')}
  156. </div>
  157. <div className='text-sm text-gray-500 truncate'>
  158. {userState.user && userState.user.wechat_id !== ''
  159. ? t('已绑定')
  160. : t('未绑定')}
  161. </div>
  162. </div>
  163. </div>
  164. <div className='flex-shrink-0'>
  165. <Button
  166. type='primary'
  167. theme='outline'
  168. size='small'
  169. disabled={!status.wechat_login}
  170. onClick={() => setShowWeChatBindModal(true)}
  171. >
  172. {userState.user && userState.user.wechat_id !== ''
  173. ? t('修改绑定')
  174. : status.wechat_login
  175. ? t('绑定')
  176. : t('未启用')}
  177. </Button>
  178. </div>
  179. </div>
  180. </Card>
  181. {/* GitHub绑定 */}
  182. <Card className='!rounded-xl'>
  183. <div className='flex items-center justify-between gap-3'>
  184. <div className='flex items-center flex-1 min-w-0'>
  185. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  186. <IconGithubLogo
  187. size='default'
  188. className='text-slate-600 dark:text-slate-300'
  189. />
  190. </div>
  191. <div className='flex-1 min-w-0'>
  192. <div className='font-medium text-gray-900'>
  193. {t('GitHub')}
  194. </div>
  195. <div className='text-sm text-gray-500 truncate'>
  196. {renderAccountInfo(
  197. userState.user?.github_id,
  198. t('GitHub ID'),
  199. )}
  200. </div>
  201. </div>
  202. </div>
  203. <div className='flex-shrink-0'>
  204. <Button
  205. type='primary'
  206. theme='outline'
  207. size='small'
  208. onClick={() =>
  209. onGitHubOAuthClicked(status.github_client_id)
  210. }
  211. disabled={
  212. (userState.user && userState.user.github_id !== '') ||
  213. !status.github_oauth
  214. }
  215. >
  216. {status.github_oauth ? t('绑定') : t('未启用')}
  217. </Button>
  218. </div>
  219. </div>
  220. </Card>
  221. {/* OIDC绑定 */}
  222. <Card className='!rounded-xl'>
  223. <div className='flex items-center justify-between gap-3'>
  224. <div className='flex items-center flex-1 min-w-0'>
  225. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  226. <IconShield
  227. size='default'
  228. className='text-slate-600 dark:text-slate-300'
  229. />
  230. </div>
  231. <div className='flex-1 min-w-0'>
  232. <div className='font-medium text-gray-900'>
  233. {t('OIDC')}
  234. </div>
  235. <div className='text-sm text-gray-500 truncate'>
  236. {renderAccountInfo(
  237. userState.user?.oidc_id,
  238. t('OIDC ID'),
  239. )}
  240. </div>
  241. </div>
  242. </div>
  243. <div className='flex-shrink-0'>
  244. <Button
  245. type='primary'
  246. theme='outline'
  247. size='small'
  248. onClick={() =>
  249. onOIDCClicked(
  250. status.oidc_authorization_endpoint,
  251. status.oidc_client_id,
  252. )
  253. }
  254. disabled={
  255. (userState.user && userState.user.oidc_id !== '') ||
  256. !status.oidc_enabled
  257. }
  258. >
  259. {status.oidc_enabled ? t('绑定') : t('未启用')}
  260. </Button>
  261. </div>
  262. </div>
  263. </Card>
  264. {/* Telegram绑定 */}
  265. <Card className='!rounded-xl'>
  266. <div className='flex items-center justify-between gap-3'>
  267. <div className='flex items-center flex-1 min-w-0'>
  268. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  269. <SiTelegram
  270. size={20}
  271. className='text-slate-600 dark:text-slate-300'
  272. />
  273. </div>
  274. <div className='flex-1 min-w-0'>
  275. <div className='font-medium text-gray-900'>
  276. {t('Telegram')}
  277. </div>
  278. <div className='text-sm text-gray-500 truncate'>
  279. {renderAccountInfo(
  280. userState.user?.telegram_id,
  281. t('Telegram ID'),
  282. )}
  283. </div>
  284. </div>
  285. </div>
  286. <div className='flex-shrink-0'>
  287. {status.telegram_oauth ? (
  288. userState.user.telegram_id !== '' ? (
  289. <Button disabled={true} size='small'>
  290. {t('已绑定')}
  291. </Button>
  292. ) : (
  293. <div className='scale-75'>
  294. <TelegramLoginButton
  295. dataAuthUrl='/api/oauth/telegram/bind'
  296. botName={status.telegram_bot_name}
  297. />
  298. </div>
  299. )
  300. ) : (
  301. <Button disabled={true} size='small'>
  302. {t('未启用')}
  303. </Button>
  304. )}
  305. </div>
  306. </div>
  307. </Card>
  308. {/* LinuxDO绑定 */}
  309. <Card className='!rounded-xl'>
  310. <div className='flex items-center justify-between gap-3'>
  311. <div className='flex items-center flex-1 min-w-0'>
  312. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  313. <SiLinux
  314. size={20}
  315. className='text-slate-600 dark:text-slate-300'
  316. />
  317. </div>
  318. <div className='flex-1 min-w-0'>
  319. <div className='font-medium text-gray-900'>
  320. {t('LinuxDO')}
  321. </div>
  322. <div className='text-sm text-gray-500 truncate'>
  323. {renderAccountInfo(
  324. userState.user?.linux_do_id,
  325. t('LinuxDO ID'),
  326. )}
  327. </div>
  328. </div>
  329. </div>
  330. <div className='flex-shrink-0'>
  331. <Button
  332. type='primary'
  333. theme='outline'
  334. size='small'
  335. onClick={() =>
  336. onLinuxDOOAuthClicked(status.linuxdo_client_id)
  337. }
  338. disabled={
  339. (userState.user && userState.user.linux_do_id !== '') ||
  340. !status.linuxdo_oauth
  341. }
  342. >
  343. {status.linuxdo_oauth ? t('绑定') : t('未启用')}
  344. </Button>
  345. </div>
  346. </div>
  347. </Card>
  348. </div>
  349. </div>
  350. </TabPane>
  351. {/* 安全设置 Tab */}
  352. <TabPane
  353. tab={
  354. <div className='flex items-center'>
  355. <ShieldCheck size={16} className='mr-2' />
  356. {t('安全设置')}
  357. </div>
  358. }
  359. itemKey='security'
  360. >
  361. <div className='py-4'>
  362. <div className='space-y-6'>
  363. <Space vertical className='w-full'>
  364. {/* 系统访问令牌 */}
  365. <Card className='!rounded-xl w-full'>
  366. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  367. <div className='flex items-start w-full sm:w-auto'>
  368. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  369. <IconKey size='large' className='text-slate-600' />
  370. </div>
  371. <div className='flex-1'>
  372. <Typography.Title heading={6} className='mb-1'>
  373. {t('系统访问令牌')}
  374. </Typography.Title>
  375. <Typography.Text type='tertiary' className='text-sm'>
  376. {t('用于API调用的身份验证令牌,请妥善保管')}
  377. </Typography.Text>
  378. {systemToken && (
  379. <div className='mt-3'>
  380. <Input
  381. readonly
  382. value={systemToken}
  383. onClick={handleSystemTokenClick}
  384. size='large'
  385. prefix={<IconKey />}
  386. />
  387. </div>
  388. )}
  389. </div>
  390. </div>
  391. <Button
  392. type='primary'
  393. theme='solid'
  394. onClick={generateAccessToken}
  395. className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
  396. icon={<IconKey />}
  397. >
  398. {systemToken ? t('重新生成') : t('生成令牌')}
  399. </Button>
  400. </div>
  401. </Card>
  402. {/* 密码管理 */}
  403. <Card className='!rounded-xl w-full'>
  404. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  405. <div className='flex items-start w-full sm:w-auto'>
  406. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  407. <IconLock size='large' className='text-slate-600' />
  408. </div>
  409. <div>
  410. <Typography.Title heading={6} className='mb-1'>
  411. {t('密码管理')}
  412. </Typography.Title>
  413. <Typography.Text type='tertiary' className='text-sm'>
  414. {t('定期更改密码可以提高账户安全性')}
  415. </Typography.Text>
  416. </div>
  417. </div>
  418. <Button
  419. type='primary'
  420. theme='solid'
  421. onClick={() => setShowChangePasswordModal(true)}
  422. className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
  423. icon={<IconLock />}
  424. >
  425. {t('修改密码')}
  426. </Button>
  427. </div>
  428. </Card>
  429. {/* 两步验证设置 */}
  430. <TwoFASetting t={t} />
  431. {/* 危险区域 */}
  432. <Card className='!rounded-xl w-full'>
  433. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  434. <div className='flex items-start w-full sm:w-auto'>
  435. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  436. <IconDelete size='large' className='text-slate-600' />
  437. </div>
  438. <div>
  439. <Typography.Title
  440. heading={6}
  441. className='mb-1 text-slate-700'
  442. >
  443. {t('删除账户')}
  444. </Typography.Title>
  445. <Typography.Text type='tertiary' className='text-sm'>
  446. {t('此操作不可逆,所有数据将被永久删除')}
  447. </Typography.Text>
  448. </div>
  449. </div>
  450. <Button
  451. type='danger'
  452. theme='solid'
  453. onClick={() => setShowAccountDeleteModal(true)}
  454. className='w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600'
  455. icon={<IconDelete />}
  456. >
  457. {t('删除账户')}
  458. </Button>
  459. </div>
  460. </Card>
  461. </Space>
  462. </div>
  463. </div>
  464. </TabPane>
  465. </Tabs>
  466. </Card>
  467. );
  468. };
  469. export default AccountManagement;