AccountManagement.js 17 KB

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