2025-08-17 00:49:54 +08:00
|
|
|
|
/*
|
|
|
|
|
|
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 from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Tabs,
|
|
|
|
|
|
TabPane
|
|
|
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
|
|
|
import {
|
|
|
|
|
|
IconMail,
|
|
|
|
|
|
IconShield,
|
|
|
|
|
|
IconGithubLogo,
|
|
|
|
|
|
IconKey,
|
|
|
|
|
|
IconLock,
|
|
|
|
|
|
IconDelete
|
|
|
|
|
|
} from '@douyinfe/semi-icons';
|
|
|
|
|
|
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
|
|
|
|
|
import { UserPlus, ShieldCheck } from 'lucide-react';
|
|
|
|
|
|
import TelegramLoginButton from 'react-telegram-login';
|
|
|
|
|
|
import {
|
|
|
|
|
|
onGitHubOAuthClicked,
|
|
|
|
|
|
onOIDCClicked,
|
|
|
|
|
|
onLinuxDOOAuthClicked
|
|
|
|
|
|
} from '../../../../helpers';
|
|
|
|
|
|
import TwoFASetting from '../components/TwoFASetting';
|
|
|
|
|
|
|
|
|
|
|
|
const AccountManagement = ({
|
|
|
|
|
|
t,
|
|
|
|
|
|
userState,
|
|
|
|
|
|
status,
|
|
|
|
|
|
systemToken,
|
|
|
|
|
|
setShowEmailBindModal,
|
|
|
|
|
|
setShowWeChatBindModal,
|
|
|
|
|
|
generateAccessToken,
|
|
|
|
|
|
handleSystemTokenClick,
|
|
|
|
|
|
setShowChangePasswordModal,
|
|
|
|
|
|
setShowAccountDeleteModal
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card className="!rounded-2xl">
|
|
|
|
|
|
{/* 卡片头部 */}
|
|
|
|
|
|
<div className="flex items-center mb-4">
|
|
|
|
|
|
<Avatar size="small" color="teal" className="mr-3 shadow-md">
|
|
|
|
|
|
<UserPlus size={16} />
|
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Typography.Text className="text-lg font-medium">{t('账户管理')}</Typography.Text>
|
|
|
|
|
|
<div className="text-xs text-gray-600">{t('账户绑定、安全设置和身份验证')}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-17 01:22:44 +08:00
|
|
|
|
<Tabs type="card" defaultActiveKey="binding">
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{/* 账户绑定 Tab */}
|
|
|
|
|
|
<TabPane
|
|
|
|
|
|
tab={
|
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<UserPlus size={16} className="mr-2" />
|
|
|
|
|
|
{t('账户绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
itemKey="binding"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="py-4">
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
|
|
|
{/* 邮箱绑定 */}
|
|
|
|
|
|
<Card className="!rounded-xl">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center flex-1">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
|
|
|
|
|
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="font-medium text-gray-900">{t('邮箱')}</div>
|
|
|
|
|
|
<div className="text-sm text-gray-500 truncate">
|
|
|
|
|
|
{userState.user && userState.user.email !== ''
|
|
|
|
|
|
? userState.user.email
|
|
|
|
|
|
: t('未绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => setShowEmailBindModal(true)}
|
|
|
|
|
|
className="!rounded-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
{userState.user && userState.user.email !== ''
|
|
|
|
|
|
? t('修改绑定')
|
|
|
|
|
|
: t('绑定')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 微信绑定 */}
|
|
|
|
|
|
<Card className="!rounded-xl">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center flex-1">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
|
|
|
|
|
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="font-medium text-gray-900">{t('微信')}</div>
|
|
|
|
|
|
<div className="text-sm text-gray-500 truncate">
|
|
|
|
|
|
{userState.user && userState.user.wechat_id !== ''
|
|
|
|
|
|
? t('已绑定')
|
|
|
|
|
|
: t('未绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
disabled={!status.wechat_login}
|
|
|
|
|
|
onClick={() => setShowWeChatBindModal(true)}
|
|
|
|
|
|
className="!rounded-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
{userState.user && userState.user.wechat_id !== ''
|
|
|
|
|
|
? t('修改绑定')
|
|
|
|
|
|
: status.wechat_login
|
|
|
|
|
|
? t('绑定')
|
|
|
|
|
|
: t('未启用')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* GitHub绑定 */}
|
|
|
|
|
|
<Card className="!rounded-xl">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center flex-1">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
|
|
|
|
|
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="font-medium text-gray-900">{t('GitHub')}</div>
|
|
|
|
|
|
<div className="text-sm text-gray-500 truncate">
|
|
|
|
|
|
{userState.user && userState.user.github_id !== ''
|
|
|
|
|
|
? userState.user.github_id
|
|
|
|
|
|
: t('未绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
(userState.user && userState.user.github_id !== '') ||
|
|
|
|
|
|
!status.github_oauth
|
|
|
|
|
|
}
|
|
|
|
|
|
className="!rounded-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
{status.github_oauth ? t('绑定') : t('未启用')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* OIDC绑定 */}
|
|
|
|
|
|
<Card className="!rounded-xl">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center flex-1">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
|
|
|
|
|
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="font-medium text-gray-900">{t('OIDC')}</div>
|
|
|
|
|
|
<div className="text-sm text-gray-500 truncate">
|
|
|
|
|
|
{userState.user && userState.user.oidc_id !== ''
|
|
|
|
|
|
? userState.user.oidc_id
|
|
|
|
|
|
: t('未绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => onOIDCClicked(
|
|
|
|
|
|
status.oidc_authorization_endpoint,
|
|
|
|
|
|
status.oidc_client_id,
|
|
|
|
|
|
)}
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
(userState.user && userState.user.oidc_id !== '') ||
|
|
|
|
|
|
!status.oidc_enabled
|
|
|
|
|
|
}
|
|
|
|
|
|
className="!rounded-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
{status.oidc_enabled ? t('绑定') : t('未启用')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Telegram绑定 */}
|
|
|
|
|
|
<Card className="!rounded-xl">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center flex-1">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
|
|
|
|
|
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="font-medium text-gray-900">{t('Telegram')}</div>
|
|
|
|
|
|
<div className="text-sm text-gray-500 truncate">
|
|
|
|
|
|
{userState.user && userState.user.telegram_id !== ''
|
|
|
|
|
|
? userState.user.telegram_id
|
|
|
|
|
|
: t('未绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
|
{status.telegram_oauth ? (
|
|
|
|
|
|
userState.user.telegram_id !== '' ? (
|
|
|
|
|
|
<Button disabled={true} size="small" className="!rounded-lg">
|
|
|
|
|
|
{t('已绑定')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="scale-75">
|
|
|
|
|
|
<TelegramLoginButton
|
|
|
|
|
|
dataAuthUrl='/api/oauth/telegram/bind'
|
|
|
|
|
|
botName={status.telegram_bot_name}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button disabled={true} size="small" className="!rounded-lg">
|
|
|
|
|
|
{t('未启用')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* LinuxDO绑定 */}
|
|
|
|
|
|
<Card className="!rounded-xl">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center flex-1">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
|
|
|
|
|
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
|
|
|
|
|
|
<div className="text-sm text-gray-500 truncate">
|
|
|
|
|
|
{userState.user && userState.user.linux_do_id !== ''
|
|
|
|
|
|
? userState.user.linux_do_id
|
|
|
|
|
|
: t('未绑定')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
(userState.user && userState.user.linux_do_id !== '') ||
|
|
|
|
|
|
!status.linuxdo_oauth
|
|
|
|
|
|
}
|
|
|
|
|
|
className="!rounded-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TabPane>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 安全设置 Tab */}
|
|
|
|
|
|
<TabPane
|
|
|
|
|
|
tab={
|
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<ShieldCheck size={16} className="mr-2" />
|
|
|
|
|
|
{t('安全设置')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
itemKey="security"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="py-4">
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<Space vertical className='w-full'>
|
|
|
|
|
|
{/* 系统访问令牌 */}
|
|
|
|
|
|
<Card className="!rounded-xl w-full">
|
|
|
|
|
|
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
|
|
|
|
|
<div className="flex items-start w-full sm:w-auto">
|
|
|
|
|
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
|
|
|
|
|
<IconKey size="large" className="text-slate-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<Typography.Title heading={6} className="mb-1">
|
|
|
|
|
|
{t('系统访问令牌')}
|
|
|
|
|
|
</Typography.Title>
|
|
|
|
|
|
<Typography.Text type="tertiary" className="text-sm">
|
|
|
|
|
|
{t('用于API调用的身份验证令牌,请妥善保管')}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
{systemToken && (
|
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
readonly
|
|
|
|
|
|
value={systemToken}
|
|
|
|
|
|
onClick={handleSystemTokenClick}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
className="!rounded-lg"
|
|
|
|
|
|
prefix={<IconKey />}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="solid"
|
|
|
|
|
|
onClick={generateAccessToken}
|
|
|
|
|
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
|
|
|
|
|
icon={<IconKey />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{systemToken ? t('重新生成') : t('生成令牌')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 密码管理 */}
|
|
|
|
|
|
<Card className="!rounded-xl w-full">
|
|
|
|
|
|
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
|
|
|
|
|
<div className="flex items-start w-full sm:w-auto">
|
|
|
|
|
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
|
|
|
|
|
<IconLock size="large" className="text-slate-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Typography.Title heading={6} className="mb-1">
|
|
|
|
|
|
{t('密码管理')}
|
|
|
|
|
|
</Typography.Title>
|
|
|
|
|
|
<Typography.Text type="tertiary" className="text-sm">
|
|
|
|
|
|
{t('定期更改密码可以提高账户安全性')}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="solid"
|
|
|
|
|
|
onClick={() => setShowChangePasswordModal(true)}
|
|
|
|
|
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
|
|
|
|
|
icon={<IconLock />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t('修改密码')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 两步验证设置 */}
|
|
|
|
|
|
<TwoFASetting t={t} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 危险区域 */}
|
|
|
|
|
|
<Card className="!rounded-xl w-full">
|
|
|
|
|
|
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
|
|
|
|
|
<div className="flex items-start w-full sm:w-auto">
|
|
|
|
|
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
|
|
|
|
|
<IconDelete size="large" className="text-slate-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Typography.Title heading={6} className="mb-1 text-slate-700">
|
|
|
|
|
|
{t('删除账户')}
|
|
|
|
|
|
</Typography.Title>
|
|
|
|
|
|
<Typography.Text type="tertiary" className="text-sm">
|
|
|
|
|
|
{t('此操作不可逆,所有数据将被永久删除')}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
theme="solid"
|
|
|
|
|
|
onClick={() => setShowAccountDeleteModal(true)}
|
|
|
|
|
|
className="!rounded-lg w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
|
|
|
|
|
|
icon={<IconDelete />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t('删除账户')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TabPane>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default AccountManagement;
|