/*
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 from 'react';
import {
Button,
Card,
Input,
Space,
Typography,
Avatar,
Tabs,
TabPane,
Popover,
Modal,
} from '@douyinfe/semi-ui';
import {
IconMail,
IconShield,
IconGithubLogo,
IconKey,
IconLock,
IconDelete,
} from '@douyinfe/semi-icons';
import { SiTelegram, SiWechat, SiLinux, SiDiscord } from 'react-icons/si';
import { UserPlus, ShieldCheck } from 'lucide-react';
import TelegramLoginButton from 'react-telegram-login';
import {
API,
showError,
showSuccess,
onGitHubOAuthClicked,
onOIDCClicked,
onLinuxDOOAuthClicked,
onDiscordOAuthClicked,
onCustomOAuthClicked,
getOAuthProviderIcon,
} from '../../../../helpers';
import TwoFASetting from '../components/TwoFASetting';
const AccountManagement = ({
t,
userState,
status,
systemToken,
setShowEmailBindModal,
setShowWeChatBindModal,
generateAccessToken,
handleSystemTokenClick,
setShowChangePasswordModal,
setShowAccountDeleteModal,
passkeyStatus,
passkeySupported,
passkeyRegisterLoading,
passkeyDeleteLoading,
onPasskeyRegister,
onPasskeyDelete,
}) => {
const renderAccountInfo = (accountId, label) => {
if (!accountId || accountId === '') {
return {t('未绑定')} ;
}
const popContent = (
{accountId}
{label ? (
{label}
) : null}
);
return (
{accountId}
);
};
const isBound = (accountId) => Boolean(accountId);
const [showTelegramBindModal, setShowTelegramBindModal] =
React.useState(false);
const [customOAuthBindings, setCustomOAuthBindings] = React.useState([]);
const [customOAuthLoading, setCustomOAuthLoading] = React.useState({});
// Fetch custom OAuth bindings
const loadCustomOAuthBindings = async () => {
try {
const res = await API.get('/api/user/oauth/bindings');
if (res.data.success) {
setCustomOAuthBindings(res.data.data || []);
} else {
showError(res.data.message || t('获取绑定信息失败'));
}
} catch (error) {
showError(error.response?.data?.message || error.message || t('获取绑定信息失败'));
}
};
// Unbind custom OAuth provider
const handleUnbindCustomOAuth = async (providerId, providerName) => {
Modal.confirm({
title: t('确认解绑'),
content: t('确定要解绑 {{name}} 吗?', { name: providerName }),
okText: t('确认'),
cancelText: t('取消'),
onOk: async () => {
setCustomOAuthLoading((prev) => ({ ...prev, [providerId]: true }));
try {
const res = await API.delete(`/api/user/oauth/bindings/${providerId}`);
if (res.data.success) {
showSuccess(t('解绑成功'));
await loadCustomOAuthBindings();
} else {
showError(res.data.message);
}
} catch (error) {
showError(error.response?.data?.message || error.message || t('操作失败'));
} finally {
setCustomOAuthLoading((prev) => ({ ...prev, [providerId]: false }));
}
},
});
};
// Handle bind custom OAuth
const handleBindCustomOAuth = (provider) => {
onCustomOAuthClicked(provider);
};
// Check if custom OAuth provider is bound
const isCustomOAuthBound = (providerId) => {
const normalizedId = Number(providerId);
return customOAuthBindings.some((b) => Number(b.provider_id) === normalizedId);
};
// Get binding info for a provider
const getCustomOAuthBinding = (providerId) => {
const normalizedId = Number(providerId);
return customOAuthBindings.find((b) => Number(b.provider_id) === normalizedId);
};
React.useEffect(() => {
loadCustomOAuthBindings();
}, []);
const passkeyEnabled = passkeyStatus?.enabled;
const lastUsedLabel = passkeyStatus?.last_used_at
? new Date(passkeyStatus.last_used_at).toLocaleString()
: t('尚未使用');
return (
{/* 卡片头部 */}
{t('账户管理')}
{t('账户绑定、安全设置和身份验证')}
{/* 账户绑定 Tab */}
{t('账户绑定')}
}
itemKey='binding'
>
{/* 邮箱绑定 */}
{t('邮箱')}
{renderAccountInfo(
userState.user?.email,
t('邮箱地址'),
)}
setShowEmailBindModal(true)}
>
{isBound(userState.user?.email)
? t('修改绑定')
: t('绑定')}
{/* 微信绑定 */}
{t('微信')}
{!status.wechat_login
? t('未启用')
: isBound(userState.user?.wechat_id)
? t('已绑定')
: t('未绑定')}
setShowWeChatBindModal(true)}
>
{isBound(userState.user?.wechat_id)
? t('修改绑定')
: status.wechat_login
? t('绑定')
: t('未启用')}
{/* GitHub绑定 */}
{t('GitHub')}
{renderAccountInfo(
userState.user?.github_id,
t('GitHub ID'),
)}
onGitHubOAuthClicked(status.github_client_id)
}
disabled={
isBound(userState.user?.github_id) ||
!status.github_oauth
}
>
{status.github_oauth ? t('绑定') : t('未启用')}
{/* Discord绑定 */}
{t('Discord')}
{renderAccountInfo(
userState.user?.discord_id,
t('Discord ID'),
)}
onDiscordOAuthClicked(status.discord_client_id)
}
disabled={
isBound(userState.user?.discord_id) ||
!status.discord_oauth
}
>
{status.discord_oauth ? t('绑定') : t('未启用')}
{/* OIDC绑定 */}
{t('OIDC')}
{renderAccountInfo(
userState.user?.oidc_id,
t('OIDC ID'),
)}
onOIDCClicked(
status.oidc_authorization_endpoint,
status.oidc_client_id,
)
}
disabled={
isBound(userState.user?.oidc_id) || !status.oidc_enabled
}
>
{status.oidc_enabled ? t('绑定') : t('未启用')}
{/* Telegram绑定 */}
{t('Telegram')}
{renderAccountInfo(
userState.user?.telegram_id,
t('Telegram ID'),
)}
{status.telegram_oauth ? (
isBound(userState.user?.telegram_id) ? (
{t('已绑定')}
) : (
setShowTelegramBindModal(true)}
>
{t('绑定')}
)
) : (
{t('未启用')}
)}
setShowTelegramBindModal(false)}
footer={null}
>
{t('点击下方按钮通过 Telegram 完成绑定')}
{/* LinuxDO绑定 */}
{t('LinuxDO')}
{renderAccountInfo(
userState.user?.linux_do_id,
t('LinuxDO ID'),
)}
onLinuxDOOAuthClicked(status.linuxdo_client_id)
}
disabled={
isBound(userState.user?.linux_do_id) ||
!status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
{/* 自定义 OAuth 提供商绑定 */}
{status.custom_oauth_providers &&
status.custom_oauth_providers.map((provider) => {
const bound = isCustomOAuthBound(provider.id);
const binding = getCustomOAuthBinding(provider.id);
return (
{getOAuthProviderIcon(
provider.icon || binding?.provider_icon || '',
20,
)}
{provider.name}
{bound
? renderAccountInfo(
binding?.provider_user_id,
t('{{name}} ID', { name: provider.name }),
)
: t('未绑定')}
{bound ? (
handleUnbindCustomOAuth(provider.id, provider.name)
}
>
{t('解绑')}
) : (
handleBindCustomOAuth(provider)}
>
{t('绑定')}
)}
);
})}
{/* 安全设置 Tab */}
{t('安全设置')}
}
itemKey='security'
>
{/* 系统访问令牌 */}
{t('系统访问令牌')}
{t('用于API调用的身份验证令牌,请妥善保管')}
{systemToken && (
}
/>
)}
}
>
{systemToken ? t('重新生成') : t('生成令牌')}
{/* 密码管理 */}
{t('密码管理')}
{t('定期更改密码可以提高账户安全性')}
setShowChangePasswordModal(true)}
className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
icon={ }
>
{t('修改密码')}
{/* Passkey 设置 */}
{t('Passkey 登录')}
{passkeyEnabled
? t('已启用 Passkey,无需密码即可登录')
: t('使用 Passkey 实现免密且更安全的登录体验')}
{t('最后使用时间')}:{lastUsedLabel}
{/*{passkeyEnabled && (*/}
{/*
*/}
{/* {t('备份支持')}:*/}
{/* {passkeyStatus?.backup_eligible*/}
{/* ? t('支持备份')*/}
{/* : t('不支持')}*/}
{/* ,{t('备份状态')}:*/}
{/* {passkeyStatus?.backup_state ? t('已备份') : t('未备份')}*/}
{/*
*/}
{/*)}*/}
{!passkeySupported && (
{t('当前设备不支持 Passkey')}
)}
{
Modal.confirm({
title: t('确认解绑 Passkey'),
content: t(
'解绑后将无法使用 Passkey 登录,确定要继续吗?',
),
okText: t('确认解绑'),
cancelText: t('取消'),
okType: 'danger',
onOk: onPasskeyDelete,
});
}
: onPasskeyRegister
}
className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`}
icon={ }
disabled={!passkeySupported && !passkeyEnabled}
loading={
passkeyEnabled
? passkeyDeleteLoading
: passkeyRegisterLoading
}
>
{passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')}
{/* 两步验证设置 */}
{/* 危险区域 */}
{t('删除账户')}
{t('此操作不可逆,所有数据将被永久删除')}
setShowAccountDeleteModal(true)}
className='w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600'
icon={ }
>
{t('删除账户')}
);
};
export default AccountManagement;