import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd'; import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons'; import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils } from '@/utils'; import { Protocols } from '@/schemas/primitives'; import { InfinityIcon } from '@/components/ui'; import { useDatepicker } from '@/hooks/useDatepicker'; import { genAllLinks, genWireguardConfigs, genWireguardLinks, preferPublicHost, } from '@/lib/xray/inbound-link'; import { inboundFromDb } from '@/lib/xray/inbound-from-db'; import { buildInboundInfo, copyText, downloadText, formatIpInfo, hasShareLink, statsColor, } from './helpers'; import type { ClientSetting, ClientStats, InboundInfo, InboundInfoModalProps } from './types'; import './InboundInfoModal.css'; export default function InboundInfoModal({ open, onClose, dbInbound, clientIndex = 0, remarkModel = '-io', expireDiff = 0, trafficDiff = 0, ipLimitEnable = false, tgBotEnable = false, nodeAddress = '', subSettings, lastOnlineMap = {}, }: InboundInfoModalProps) { const { t } = useTranslation(); const { datepicker } = useDatepicker(); const [inbound, setInbound] = useState(null); const [clientSettings, setClientSettings] = useState(null); const [clientStats, setClientStats] = useState(null); const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]); const [wireguardConfigs, setWireguardConfigs] = useState([]); const [wireguardLinks, setWireguardLinks] = useState([]); const [subLink, setSubLink] = useState(''); const [subJsonLink, setSubJsonLink] = useState(''); const [refreshing, setRefreshing] = useState(false); const [clientIpsArray, setClientIpsArray] = useState([]); const [clientIpsText, setClientIpsText] = useState(''); const [activeTab, setActiveTab] = useState('client'); const loadClientIps = useCallback(async () => { if (!clientStats?.email) return; setRefreshing(true); try { const msg = await HttpUtil.post(`/panel/api/clients/ips/${clientStats.email}`); if (!msg?.success) { setClientIpsText((msg?.obj as string) || 'No IP record'); setClientIpsArray([]); return; } let ips: unknown = msg.obj; if (typeof ips === 'string') { try { ips = JSON.parse(ips); } catch { setClientIpsText(String(ips)); setClientIpsArray([String(ips)]); return; } } if (ips && !Array.isArray(ips) && typeof ips === 'object') ips = [ips]; if (Array.isArray(ips) && ips.length > 0) { const arr = (ips as unknown[]).map(formatIpInfo).filter(Boolean) as string[]; setClientIpsArray(arr); setClientIpsText(arr.join(' | ')); } else { setClientIpsArray([]); setClientIpsText(String(ips || t('tgbot.noIpRecord'))); } } finally { setRefreshing(false); } }, [clientStats, t]); const clearClientIps = useCallback(async () => { if (!clientStats?.email) return; const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${clientStats.email}`); if (msg?.success) { setClientIpsArray([]); setClientIpsText(t('tgbot.noIpRecord')); } }, [clientStats, t]); useEffect(() => { if (!open || !dbInbound) return; const info = buildInboundInfo(dbInbound); setInbound(info); setActiveTab(info.clients.length > 0 ? 'client' : 'inbound'); const idx = clientIndex ?? 0; const clientSet = info.clients.length > 0 ? (info.clients[idx] || null) : null; setClientSettings(clientSet); const stats = clientSet ? (dbInbound.clientStats || []).find((s) => s.email === clientSet.email) || null : null; setClientStats(stats); const inboundForLinks = inboundFromDb(dbInbound); const fallbackHostname = preferPublicHost(window.location.hostname, subSettings?.publicHost ?? ''); if (info.protocol === Protocols.WIREGUARD) { setWireguardConfigs( genWireguardConfigs({ inbound: inboundForLinks, remark: dbInbound.remark, remarkModel: '-io', hostOverride: nodeAddress, fallbackHostname, }).split('\r\n'), ); setWireguardLinks( genWireguardLinks({ inbound: inboundForLinks, remark: dbInbound.remark, remarkModel: '-io', hostOverride: nodeAddress, fallbackHostname, }).split('\r\n'), ); setLinks([]); } else { setLinks( genAllLinks({ inbound: inboundForLinks, remark: dbInbound.remark, remarkModel, client: (clientSet ?? {}) as Parameters[0]['client'], hostOverride: nodeAddress, fallbackHostname, }), ); setWireguardConfigs([]); setWireguardLinks([]); } if (clientSet?.subId) { setSubLink((subSettings?.subURI || '') + clientSet.subId); setSubJsonLink( subSettings?.subJsonEnable ? (subSettings?.subJsonURI || '') + clientSet.subId : '', ); } else { setSubLink(''); setSubJsonLink(''); } setClientIpsArray([]); setClientIpsText(''); if (ipLimitEnable && (clientSet?.limitIp ?? 0) > 0 && stats?.email) { void HttpUtil.post(`/panel/api/clients/ips/${stats.email}`).then((msg) => { if (!msg?.success) { setClientIpsText((msg?.obj as string) || 'No IP record'); return; } let ips: unknown = msg.obj; if (typeof ips === 'string') { try { ips = JSON.parse(ips); } catch { setClientIpsText(String(ips)); setClientIpsArray([String(ips)]); return; } } if (ips && !Array.isArray(ips) && typeof ips === 'object') ips = [ips]; if (Array.isArray(ips) && ips.length > 0) { const arr = (ips as unknown[]).map(formatIpInfo).filter(Boolean) as string[]; setClientIpsArray(arr); setClientIpsText(arr.join(' | ')); } else { setClientIpsText(String(ips || t('tgbot.noIpRecord'))); } }); } }, [open, dbInbound, clientIndex, remarkModel, nodeAddress, subSettings, ipLimitEnable, t]); const isEnable = useMemo(() => { if (clientSettings) return !!clientSettings.enable; return dbInbound?.enable ?? true; }, [clientSettings, dbInbound]); const isDepleted = useMemo(() => { if (!clientStats || !clientSettings) return false; const total = clientStats.total ?? 0; const used = (clientStats.up ?? 0) + (clientStats.down ?? 0); if (total > 0 && used >= total) return true; const expiry = clientSettings.expiryTime ?? 0; if (expiry > 0 && Date.now() >= expiry) return true; return false; }, [clientStats, clientSettings]); const remainingStats = useMemo(() => { if (!clientStats || !clientSettings) return '-'; const remained = clientStats.total - clientStats.up - clientStats.down; return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-'; }, [clientStats, clientSettings]); const formatLastOnline = useCallback( (email: string) => { const ts = lastOnlineMap[email]; if (!ts) return '-'; return IntlUtil.formatDate(ts, datepicker); }, [lastOnlineMap, datepicker], ); const networkLabel = inbound?.stream?.network || ''; const securityLabel = inbound?.stream?.security || 'none'; const securityColor = securityLabel === 'none' ? 'red' : 'green'; const encryptionLabel = (inbound?.settings?.encryption as string) || ''; const serverNameLabel = inbound?.serverName || ''; const showClientTab = !!clientSettings; const showSubscriptionTab = !!(subSettings?.enable && clientSettings?.subId); if (!dbInbound || !inbound) { return ( ); } const clientTab = ( <> {clientSettings?.id && ( )} {dbInbound.isVMess && ( )} {inbound.isVlessTlsFlow && ( )} {clientSettings?.password && ( )} {clientStats && ( )} {clientSettings?.comment && ( )} {ipLimitEnable && ( )} {ipLimitEnable && (clientSettings?.limitIp ?? 0) > 0 && ( )}
{t('pages.inbounds.email')} {clientSettings?.email ? ( {clientSettings.email} ) : ( {t('none')} )}
ID{clientSettings.id}
{t('security')}{clientSettings?.security}
{t('pages.clients.flow')} {clientSettings?.flow ? {clientSettings.flow} : {t('none')}}
{t('password')} {clientSettings.password}
{t('status')} {isDepleted ? ( {t('depleted')} ) : isEnable ? ( {t('enabled')} ) : ( {t('disabled')} )}
{t('usage')} {SizeFormatter.sizeFormat(clientStats.up + clientStats.down)} ↑ {SizeFormatter.sizeFormat(clientStats.up)} / {' '}{SizeFormatter.sizeFormat(clientStats.down)} ↓
{t('pages.inbounds.createdAt')} {clientSettings?.created_at ? ( {IntlUtil.formatDate(clientSettings.created_at, datepicker)} ) : -}
{t('pages.inbounds.updatedAt')} {clientSettings?.updated_at ? ( {IntlUtil.formatDate(clientSettings.updated_at, datepicker)} ) : -}
{t('lastOnline')} {formatLastOnline(clientSettings?.email || '')}
{t('comment')}{clientSettings.comment}
{t('pages.inbounds.IPLimit')}{clientSettings?.limitIp ?? 0}
{t('pages.inbounds.IPLimitlog')}
{clientIpsArray.length > 0 ? (
{clientIpsArray.map((item, idx) => ( {item} ))}
) : ( {clientIpsText || t('tgbot.noIpRecord')} )}
loadClientIps()} /> clearClientIps()} />
{t('remained')} {t('pages.inbounds.totalUsage')} {t('pages.inbounds.expireDate')}
{clientStats && (clientSettings?.totalGB ?? 0) > 0 ? ( {remainingStats} ) : !clientSettings?.totalGB || clientSettings.totalGB <= 0 ? ( ) : null} {(clientSettings?.totalGB ?? 0) > 0 ? ( {SizeFormatter.sizeFormat(clientSettings!.totalGB!)} ) : ( )} {(clientSettings?.expiryTime ?? 0) > 0 ? ( {IntlUtil.formatDate(clientSettings!.expiryTime!, datepicker)} ) : (clientSettings?.expiryTime ?? 0) < 0 ? ( {clientSettings!.expiryTime! / -86400000} {t('day')} ) : ( )}
{tgBotEnable && clientSettings?.tgId && ( <> Telegram
{clientSettings.tgId}
)} {hasShareLink(dbInbound.protocol) && links.length > 0 && ( <> {t('pages.inbounds.copyLink')} {links.map((link, idx) => (
{link.remark || `Link ${idx + 1}`}
{link.link}
))} )} {showSubscriptionTab && ( <> {t('subscription.title')}
{t('subscription.title')}
{subLink}
{subSettings?.subJsonEnable && subJsonLink && (
JSON
{subJsonLink}
)} )} ); const inboundTab = ( <>
{t('pages.inbounds.protocol')}
{dbInbound.protocol}
{t('pages.inbounds.address')}
{dbInbound.address}
{t('pages.inbounds.port')}
{dbInbound.port}
{(dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS) && ( <>
{t('transmission')}
{networkLabel}
{(inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP) && ( <>
{t('host')}
{inbound.host ? {inbound.host} : {t('none')}}
{t('path')}
{inbound.path ? {inbound.path} : {t('none')}}
)} {inbound.isXHTTP && (
{t('pages.inbounds.info.mode')}
{inbound.stream?.xhttp?.mode}
)} {inbound.isGrpc && ( <>
{t('pages.inbounds.info.grpcServiceName')}
{inbound.serviceName}
{t('pages.inbounds.info.grpcMultiMode')}
{String(inbound.stream?.grpc?.multiMode)}
)} )} {hasShareLink(dbInbound.protocol) && ( <>
{t('security')}
{securityLabel}
{encryptionLabel && (
{t('encryption')}
{encryptionLabel}
)} {securityLabel !== 'none' && (
{t('domainName')}
{serverNameLabel ? ( {serverNameLabel} ) : ( {t('none')} )}
)} )}
{dbInbound.isSS && inbound.settings && ( {inbound.isSS2022 && ( )}
{t('encryption')} {inbound.settings.method as string}
{t('password')} {inbound.settings.password as string}
{t('pages.inbounds.network')} {inbound.settings.network as string}
)} {inbound.protocol === Protocols.TUN && inbound.settings && (
{t('pages.inbounds.info.interfaceName')}
{inbound.settings.name as string}
{t('pages.inbounds.info.mtu')}
{inbound.settings.mtu as number}
{Array.isArray(inbound.settings.gateway) && (inbound.settings.gateway as string[]).length > 0 && (
{t('pages.inbounds.info.gateway')}
{(inbound.settings.gateway as string[]).map((ip, j) => ( {ip} ))}
)} {Array.isArray(inbound.settings.dns) && (inbound.settings.dns as string[]).length > 0 && (
{t('pages.inbounds.info.dns')}
{(inbound.settings.dns as string[]).map((ip, j) => ( {ip} ))}
)}
{t('pages.inbounds.info.outboundsInterface')}
{(inbound.settings.autoOutboundsInterface as string) || 'auto'}
{Array.isArray(inbound.settings.autoSystemRoutingTable) && (inbound.settings.autoSystemRoutingTable as string[]).length > 0 && (
{t('pages.inbounds.info.autoSystemRoutes')}
{(inbound.settings.autoSystemRoutingTable as string[]).map((cidr, j) => ( {cidr} ))}
)}
)} {inbound.protocol === Protocols.TUNNEL && inbound.settings && (
{t('pages.inbounds.targetAddress')}
{inbound.settings.rewriteAddress as string}
{t('pages.inbounds.destinationPort')}
{inbound.settings.rewritePort as number}
{t('pages.inbounds.network')}
{inbound.settings.allowedNetwork as string}
{t('pages.inbounds.info.followRedirect')}
{inbound.settings.followRedirect ? t('enabled') : t('disabled')}
)} {dbInbound.isMixed && inbound.settings && (
{t('pages.inbounds.info.auth')}
{inbound.settings.auth as string}
UDP
{inbound.settings.udp ? t('enabled') : t('disabled')}
{(inbound.settings.ip as string) && (
IP
{inbound.settings.ip as string}
)} {inbound.settings.auth === 'password' && Array.isArray(inbound.settings.accounts) && ( <> {(inbound.settings.accounts as { user: string; pass: string }[]).map((account, idx) => (
{t('username')} #{idx + 1}
{account.user} : {account.pass}
))} )} {inbound.settings.auth === 'noauth' && (
{t('copy')}
)}
)} {dbInbound.isHTTP && Array.isArray(inbound.settings?.accounts) && (inbound.settings!.accounts as unknown[]).length > 0 && (
{(inbound.settings!.accounts as { user: string; pass: string }[]).map((account, idx) => (
{t('username')} #{idx + 1}
{account.user} : {account.pass}
))}
)} {dbInbound.isWireguard && inbound.settings && ( <>
{t('pages.xray.wireguard.secretKey')}
{inbound.settings.secretKey as string}
{t('pages.xray.wireguard.publicKey')}
{inbound.settings.pubKey as string}
{t('pages.inbounds.info.mtu')}
{inbound.settings.mtu as number}
{t('pages.inbounds.info.noKernelTun')}
{String(inbound.settings.noKernelTun)}
{Array.isArray(inbound.settings.peers) && (inbound.settings.peers as { privateKey: string; publicKey: string; psk: string; allowedIPs?: string[]; keepAlive?: number }[]).map((peer, idx) => ( {t('pages.inbounds.info.peerNumber', { n: idx + 1 })}
{t('pages.xray.wireguard.secretKey')}
{peer.privateKey}
{t('pages.xray.wireguard.publicKey')}
{peer.publicKey}
PSK
{peer.psk}
{t('pages.xray.wireguard.allowedIPs')}
{(peer.allowedIPs || []).map((ip, j) => ( {ip} ))}
{t('pages.inbounds.info.keepAlive')}
{peer.keepAlive}
{wireguardConfigs[idx] && (
{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}
{wireguardConfigs[idx]}
)} {wireguardLinks[idx] && (
Peer {idx + 1} link
{wireguardLinks[idx]}
)}
))} )} {dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0 && ( <> {t('pages.inbounds.copyLink')} {links.map((link, idx) => (
{link.remark || `Link ${idx + 1}`}
{link.link}
))} )} ); const tabItems = []; if (showClientTab) { tabItems.push({ key: 'client', label: t('pages.inbounds.client'), children: clientTab }); } tabItems.push({ key: 'inbound', label: t('pages.xray.rules.inbound'), children: inboundTab }); return ( ); }