/* 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 { useTranslation } from 'react-i18next'; import { API, showError, showSuccess, getOAuthProviderIcon, } from '../../../../helpers'; import { Modal, Spin, Typography, Card, Checkbox, Tag, Button, } from '@douyinfe/semi-ui'; import { IconLink, IconMail, IconDelete, IconGithubLogo, } from '@douyinfe/semi-icons'; import { SiDiscord, SiTelegram, SiWechat, SiLinux } from 'react-icons/si'; const { Text } = Typography; const UserBindingManagementModal = ({ visible, onCancel, userId, isMobile, formApiRef, }) => { const { t } = useTranslation(); const [bindingLoading, setBindingLoading] = React.useState(false); const [showUnboundOnly, setShowUnboundOnly] = React.useState(false); const [statusInfo, setStatusInfo] = React.useState({}); const [customOAuthBindings, setCustomOAuthBindings] = React.useState([]); const [bindingActionLoading, setBindingActionLoading] = React.useState({}); const loadBindingData = React.useCallback(async () => { if (!userId) return; setBindingLoading(true); try { const [statusRes, customBindingRes] = await Promise.all([ API.get('/api/status'), API.get(`/api/user/${userId}/oauth/bindings`), ]); if (statusRes.data?.success) { setStatusInfo(statusRes.data.data || {}); } else { showError(statusRes.data?.message || t('操作失败')); } if (customBindingRes.data?.success) { setCustomOAuthBindings(customBindingRes.data.data || []); } else { showError(customBindingRes.data?.message || t('操作失败')); } } catch (error) { showError( error.response?.data?.message || error.message || t('操作失败'), ); } finally { setBindingLoading(false); } }, [t, userId]); React.useEffect(() => { if (!visible) return; setShowUnboundOnly(false); setBindingActionLoading({}); loadBindingData(); }, [visible, loadBindingData]); const setBindingLoadingState = (key, value) => { setBindingActionLoading((prev) => ({ ...prev, [key]: value })); }; const handleUnbindBuiltInAccount = (bindingItem) => { if (!userId) return; Modal.confirm({ title: t('确认解绑'), content: t('确定要解绑 {{name}} 吗?', { name: bindingItem.name }), okText: t('确认'), cancelText: t('取消'), onOk: async () => { const loadingKey = `builtin-${bindingItem.key}`; setBindingLoadingState(loadingKey, true); try { const res = await API.delete( `/api/user/${userId}/bindings/${bindingItem.key}`, ); if (!res.data?.success) { showError(res.data?.message || t('操作失败')); return; } formApiRef.current?.setValue(bindingItem.field, ''); showSuccess(t('解绑成功')); } catch (error) { showError( error.response?.data?.message || error.message || t('操作失败'), ); } finally { setBindingLoadingState(loadingKey, false); } }, }); }; const handleUnbindCustomOAuthAccount = (provider) => { if (!userId) return; Modal.confirm({ title: t('确认解绑'), content: t('确定要解绑 {{name}} 吗?', { name: provider.name }), okText: t('确认'), cancelText: t('取消'), onOk: async () => { const loadingKey = `custom-${provider.id}`; setBindingLoadingState(loadingKey, true); try { const res = await API.delete( `/api/user/${userId}/oauth/bindings/${provider.id}`, ); if (!res.data?.success) { showError(res.data?.message || t('操作失败')); return; } setCustomOAuthBindings((prev) => prev.filter( (item) => Number(item.provider_id) !== Number(provider.id), ), ); showSuccess(t('解绑成功')); } catch (error) { showError( error.response?.data?.message || error.message || t('操作失败'), ); } finally { setBindingLoadingState(loadingKey, false); } }, }); }; const currentValues = formApiRef.current?.getValues?.() || {}; const builtInBindingItems = [ { key: 'email', field: 'email', name: t('邮箱'), enabled: true, value: currentValues.email, icon: ( ), }, { key: 'github', field: 'github_id', name: 'GitHub', enabled: Boolean(statusInfo.github_oauth), value: currentValues.github_id, icon: ( ), }, { key: 'discord', field: 'discord_id', name: 'Discord', enabled: Boolean(statusInfo.discord_oauth), value: currentValues.discord_id, icon: ( ), }, { key: 'oidc', field: 'oidc_id', name: 'OIDC', enabled: Boolean(statusInfo.oidc_enabled), value: currentValues.oidc_id, icon: ( ), }, { key: 'wechat', field: 'wechat_id', name: t('微信'), enabled: Boolean(statusInfo.wechat_login), value: currentValues.wechat_id, icon: ( ), }, { key: 'telegram', field: 'telegram_id', name: 'Telegram', enabled: Boolean(statusInfo.telegram_oauth), value: currentValues.telegram_id, icon: ( ), }, { key: 'linuxdo', field: 'linux_do_id', name: 'LinuxDO', enabled: Boolean(statusInfo.linuxdo_oauth), value: currentValues.linux_do_id, icon: ( ), }, ]; const customBindingMap = new Map( customOAuthBindings.map((item) => [Number(item.provider_id), item]), ); const customProviderMap = new Map( (statusInfo.custom_oauth_providers || []).map((provider) => [ Number(provider.id), provider, ]), ); customOAuthBindings.forEach((binding) => { if (!customProviderMap.has(Number(binding.provider_id))) { customProviderMap.set(Number(binding.provider_id), { id: binding.provider_id, name: binding.provider_name, icon: binding.provider_icon, }); } }); const customBindingItems = Array.from(customProviderMap.values()).map( (provider) => { const binding = customBindingMap.get(Number(provider.id)); return { key: `custom-${provider.id}`, providerId: provider.id, name: provider.name, enabled: true, value: binding?.provider_user_id || '', icon: getOAuthProviderIcon( provider.icon || binding?.provider_icon || '', 20, ), }; }, ); const allBindingItems = [ ...builtInBindingItems.map((item) => ({ ...item, type: 'builtin' })), ...customBindingItems.map((item) => ({ ...item, type: 'custom' })), ]; const visibleBindingItems = showUnboundOnly ? allBindingItems.filter((item) => !item.value) : allBindingItems; return ( {t('绑定信息')} } >
setShowUnboundOnly(Boolean(e.target.checked))} > {`${t('筛选')} ${t('未绑定')}`} {t('筛选')} · {visibleBindingItems.length}
{visibleBindingItems.length === 0 ? ( {t('暂无自定义 OAuth 提供商')} ) : (
{visibleBindingItems.map((item) => { const isBound = Boolean(item.value); const loadingKey = item.type === 'builtin' ? `builtin-${item.key}` : `custom-${item.providerId}`; const statusText = isBound ? item.value : item.enabled ? t('未绑定') : t('未启用'); return (
{item.icon}
{item.name} {item.type === 'builtin' ? 'Built-in' : 'Custom'}
{statusText}
); })}
)}
); }; export default UserBindingManagementModal;