mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-12 08:55:01 +03:00
refactor(inbounds): drop the single-user shadowsocks inbound paths
isSSMultiUser treated 2022-blake3-chacha20-poly1305 as a single-user inbound, and the inbounds page grew a row QR modal, a separate link panel in the info modal and a client-count skip for that case. The method is no longer allowed on inbounds, so every shadowsocks inbound carries clients and those paths cannot be reached: remove them along with the inbound-level QrCodeModal they alone opened.
This commit is contained in:
@@ -1387,11 +1387,8 @@ export function preferPublicHost(browserHost: string, publicHost: string): strin
|
||||
return publicHost && isLoopbackHost(browserHost) ? publicHost : browserHost;
|
||||
}
|
||||
|
||||
// Returns the client array for protocols that have one. SS returns its
|
||||
// clients only in 2022-blake3 multi-user mode (matches the legacy
|
||||
// `this.clients` getter, which used isSSMultiUser to gate). Returns null
|
||||
// for SS single-user, http, mixed, tunnel, wireguard, hysteria2-without-
|
||||
// clients, and any protocol without a clients array.
|
||||
// Returns the client array for protocols that have one; null for http, mixed,
|
||||
// tunnel, wireguard, hysteria2-without-clients, and any protocol without one.
|
||||
type ClientShape = {
|
||||
id?: string;
|
||||
uuid?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Pure-function ports of the legacy Inbound class capability predicates
|
||||
// (canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream,
|
||||
// canEnableVisionSeed, isSS2022, isSSMultiUser). Each accepts the minimal
|
||||
// canEnableVisionSeed, isSS2022). Each accepts the minimal
|
||||
// slice of an InboundFormValues it needs, so the same predicate can be
|
||||
// called against a partial-row, a full form value, or a hand-built test
|
||||
// fixture without the caller projecting a whole object.
|
||||
@@ -21,7 +21,6 @@ const STREAM_PROTOCOLS = [
|
||||
];
|
||||
const VISION_FLOW = 'xtls-rprx-vision';
|
||||
const SS_2022_PREFIX = '2022';
|
||||
const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
|
||||
|
||||
export interface CapabilityProtocolSlice {
|
||||
protocol: string;
|
||||
@@ -103,15 +102,6 @@ export function canEnableVisionSeed(values: CapabilityVlessSlice): boolean {
|
||||
return clients.some((c) => c?.flow === VISION_FLOW);
|
||||
}
|
||||
|
||||
// Why: legacy returns true on non-SS protocols too (the method getter
|
||||
// resolves to "" and "" !== blake3-chacha20-poly1305). Preserved for
|
||||
// parity with the legacy class; in practice the callers all narrow on
|
||||
// protocol === shadowsocks before checking.
|
||||
export function isSSMultiUser(values: CapabilityShadowsocksSlice): boolean {
|
||||
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
||||
return method !== SS_BLAKE3_CHACHA20;
|
||||
}
|
||||
|
||||
export function isSS2022(values: CapabilityShadowsocksSlice): boolean {
|
||||
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
||||
return method.substring(0, 4) === SS_2022_PREFIX;
|
||||
|
||||
@@ -51,7 +51,6 @@ import { LazyMount } from '@/components/utility';
|
||||
const InboundFormModal = lazy(() => import('./form/InboundFormModal'));
|
||||
const CloneInboundModal = lazy(() => import('./CloneInboundModal'));
|
||||
const InboundInfoModal = lazy(() => import('./info/InboundInfoModal'));
|
||||
const QrCodeModal = lazy(() => import('./qr/QrCodeModal'));
|
||||
const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
|
||||
const AttachExistingClientsModal = lazy(() => import('./clients/AttachExistingClientsModal'));
|
||||
const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
|
||||
@@ -60,7 +59,6 @@ const AddClientsToGroupModal = lazy(() => import('./clients/AddClientsToGroupMod
|
||||
type RowAction =
|
||||
| 'edit'
|
||||
| 'showInfo'
|
||||
| 'qrcode'
|
||||
| 'export'
|
||||
| 'subs'
|
||||
| 'clipboard'
|
||||
@@ -167,9 +165,6 @@ export default function InboundsPage() {
|
||||
const [infoDbInbound, setInfoDbInbound] = useState<DBInbound | null>(null);
|
||||
const [infoClientIndex, setInfoClientIndex] = useState(0);
|
||||
|
||||
const [qrOpen, setQrOpen] = useState(false);
|
||||
const [qrDbInbound, setQrDbInbound] = useState<DBInbound | null>(null);
|
||||
|
||||
const [attachOpen, setAttachOpen] = useState(false);
|
||||
const [attachSource, setAttachSource] = useState<DBInbound | null>(null);
|
||||
const [attachExistingOpen, setAttachExistingOpen] = useState(false);
|
||||
@@ -213,7 +208,6 @@ export default function InboundsPage() {
|
||||
() => hostOverrideFor(infoDbInbound),
|
||||
[infoDbInbound, hostOverrideFor],
|
||||
);
|
||||
const qrNodeAddress = useMemo(() => hostOverrideFor(qrDbInbound), [qrDbInbound, hostOverrideFor]);
|
||||
|
||||
const openText = useCallback(
|
||||
(opts: {
|
||||
@@ -637,7 +631,6 @@ export default function InboundsPage() {
|
||||
const hydratingKeys: RowAction[] = [
|
||||
'edit',
|
||||
'showInfo',
|
||||
'qrcode',
|
||||
'export',
|
||||
'subs',
|
||||
'clipboard',
|
||||
@@ -659,10 +652,6 @@ export default function InboundsPage() {
|
||||
setInfoClientIndex(findClientIndex(target, null));
|
||||
setInfoOpen(true);
|
||||
break;
|
||||
case 'qrcode':
|
||||
setQrDbInbound(checkFallback(target));
|
||||
setQrOpen(true);
|
||||
break;
|
||||
case 'export':
|
||||
exportInboundLinks(target);
|
||||
break;
|
||||
@@ -847,17 +836,6 @@ export default function InboundsPage() {
|
||||
nodeAddress={infoNodeAddress}
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={qrOpen}>
|
||||
<QrCodeModal
|
||||
open={qrOpen}
|
||||
onClose={() => setQrOpen(false)}
|
||||
dbInbound={qrDbInbound}
|
||||
client={null}
|
||||
nodeAddress={qrNodeAddress}
|
||||
subSettings={subSettings}
|
||||
hosts={hosts}
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={attachOpen}>
|
||||
<AttachClientsModal
|
||||
open={attachOpen}
|
||||
|
||||
@@ -1302,28 +1302,6 @@ export default function InboundInfoModal({
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0 && (
|
||||
<>
|
||||
<Divider>{t('pages.inbounds.copyLink')}</Divider>
|
||||
{links.map((link, idx) => (
|
||||
<div key={idx} className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">{link.remark || `Link ${idx + 1}`}</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(link.link, t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{link.link}</code>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ import { getMessage } from '@/utils/messageBus';
|
||||
import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
|
||||
import { Protocols } from '@/schemas/primitives';
|
||||
import { coerceInboundJsonField } from '@/models/dbinbound';
|
||||
import {
|
||||
canEnableTlsFlow,
|
||||
isSS2022 as isSS2022Helper,
|
||||
isSSMultiUser as isSSMultiUserHelper,
|
||||
} from '@/lib/xray/protocol-capabilities';
|
||||
import { canEnableTlsFlow, isSS2022 as isSS2022Helper } from '@/lib/xray/protocol-capabilities';
|
||||
|
||||
import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
|
||||
|
||||
@@ -123,10 +119,6 @@ export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
||||
isHttpupgrade: network === 'httpupgrade',
|
||||
isXHTTP: network === 'xhttp',
|
||||
isGrpc: network === 'grpc',
|
||||
isSSMultiUser: isSSMultiUserHelper({
|
||||
protocol: dbInbound.protocol,
|
||||
settings: settings as { method?: string },
|
||||
}),
|
||||
isSS2022: isSS2022Helper({
|
||||
protocol: dbInbound.protocol,
|
||||
settings: settings as { method?: string },
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface InboundInfo {
|
||||
isHttpupgrade: boolean;
|
||||
isXHTTP: boolean;
|
||||
isGrpc: boolean;
|
||||
isSSMultiUser: boolean;
|
||||
isSS2022: boolean;
|
||||
isVlessTlsFlow: boolean;
|
||||
host: string | null;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Button, Dropdown, type MenuProps } from 'antd';
|
||||
import {
|
||||
MoreOutlined,
|
||||
EditOutlined,
|
||||
QrcodeOutlined,
|
||||
CopyOutlined,
|
||||
ExportOutlined,
|
||||
RetweetOutlined,
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
UsergroupDeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { isInboundMultiUser, showQrCodeMenu } from './helpers';
|
||||
import { isInboundMultiUser } from './helpers';
|
||||
import type { DBInboundRecord, RowAction } from './types';
|
||||
|
||||
interface RowActionsMenuProps {
|
||||
@@ -43,9 +42,6 @@ export function buildRowActionsMenu({
|
||||
if (isMobile) {
|
||||
items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
|
||||
}
|
||||
if (showQrCodeMenu(record)) {
|
||||
items.push({ key: 'qrcode', icon: <QrcodeOutlined />, label: t('qrCode') });
|
||||
}
|
||||
if (isInboundMultiUser(record)) {
|
||||
items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
|
||||
if (subEnable) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
||||
import { coerceInboundJsonField } from '@/models/dbinbound';
|
||||
import type { HostRecord } from '@/schemas/api/host';
|
||||
|
||||
import type { DBInboundRecord, StreamHints } from './types';
|
||||
import type { StreamHints } from './types';
|
||||
|
||||
export function readStreamHints(streamSettings: unknown): StreamHints {
|
||||
const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
|
||||
@@ -82,11 +81,12 @@ export function readSettings(settings: unknown): {
|
||||
};
|
||||
}
|
||||
|
||||
export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
|
||||
export function isInboundMultiUser(record: { protocol: string }): boolean {
|
||||
switch (record.protocol) {
|
||||
case 'vmess':
|
||||
case 'vless':
|
||||
case 'trojan':
|
||||
case 'shadowsocks':
|
||||
case 'hysteria':
|
||||
case 'mtproto':
|
||||
case 'wireguard':
|
||||
@@ -94,20 +94,11 @@ export function isInboundMultiUser(record: { protocol: string; settings: unknown
|
||||
case 'tuic':
|
||||
case 'masque':
|
||||
return true;
|
||||
case 'shadowsocks':
|
||||
return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
|
||||
if (dbInbound.isSS) {
|
||||
return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Max host remarks shown inline before truncating with "+N". */
|
||||
export const HOST_REMARK_VISIBLE_LIMIT = 2;
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ export interface InboundSpeedEntry {
|
||||
export type RowAction =
|
||||
| 'edit'
|
||||
| 'showInfo'
|
||||
| 'qrcode'
|
||||
| 'export'
|
||||
| 'subs'
|
||||
| 'clipboard'
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Collapse, Modal } from 'antd';
|
||||
import type { CollapseProps } from 'antd';
|
||||
|
||||
import { Protocols } from '@/schemas/primitives';
|
||||
import {
|
||||
genAllLinks,
|
||||
genAmneziaWGPeerConfigs,
|
||||
genAmneziaWGPeerLinks,
|
||||
genWireguardPeerConfigs,
|
||||
genWireguardPeerLinks,
|
||||
isPostQuantumLink,
|
||||
preferPublicHost,
|
||||
} from '@/lib/xray/inbound-link';
|
||||
import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
|
||||
import { withHostEndpoints } from '@/lib/hosts/host-link';
|
||||
import type { HostRecord } from '@/schemas/api/host';
|
||||
import QrPanel from './QrPanel';
|
||||
import { peerConfFileName } from '../info/helpers';
|
||||
import type { SubSettings } from '../useInbounds';
|
||||
|
||||
interface ClientSetting {
|
||||
email?: string;
|
||||
subId?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
interface QrCodeModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
dbInbound: (DbInboundLike & { id: number; remark?: string }) | null;
|
||||
client?: ClientSetting | null;
|
||||
nodeAddress?: string;
|
||||
subSettings?: SubSettings;
|
||||
hosts?: HostRecord[];
|
||||
}
|
||||
|
||||
interface QrItem {
|
||||
key: string;
|
||||
header: string;
|
||||
value: string;
|
||||
downloadName?: string;
|
||||
showQr?: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_HOSTS: HostRecord[] = [];
|
||||
|
||||
export default function QrCodeModal({
|
||||
open,
|
||||
onClose,
|
||||
dbInbound,
|
||||
client = null,
|
||||
nodeAddress = '',
|
||||
subSettings,
|
||||
hosts = EMPTY_HOSTS,
|
||||
}: QrCodeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
|
||||
const [wireguardConfigs, setWireguardConfigs] = useState<string[][]>([]);
|
||||
const [wireguardLinks, setWireguardLinks] = useState<string[][]>([]);
|
||||
const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[][]>([]);
|
||||
const [amneziawgLinks, setAmneziawgLinks] = useState<string[][]>([]);
|
||||
const [subLink, setSubLink] = useState('');
|
||||
const [subJsonLink, setSubJsonLink] = useState('');
|
||||
const [activeKey, setActiveKey] = useState<string[]>([]);
|
||||
|
||||
// Building the links is a pure function of the props, so it runs during
|
||||
// render; an effect would paint the previous inbound's QR first.
|
||||
const [syncedProps, setSyncedProps] = useState<{
|
||||
dbInbound: typeof dbInbound;
|
||||
client: typeof client;
|
||||
nodeAddress: typeof nodeAddress;
|
||||
subSettings: typeof subSettings;
|
||||
hosts: typeof hosts;
|
||||
} | null>(null);
|
||||
if (
|
||||
open &&
|
||||
dbInbound &&
|
||||
(syncedProps === null ||
|
||||
syncedProps.dbInbound !== dbInbound ||
|
||||
syncedProps.client !== client ||
|
||||
syncedProps.nodeAddress !== nodeAddress ||
|
||||
syncedProps.subSettings !== subSettings ||
|
||||
syncedProps.hosts !== hosts)
|
||||
) {
|
||||
setSyncedProps({ dbInbound, client, nodeAddress, subSettings, hosts });
|
||||
const fallbackHostname = preferPublicHost(
|
||||
window.location.hostname,
|
||||
subSettings?.publicHost ?? '',
|
||||
);
|
||||
const inbound = withHostEndpoints(
|
||||
inboundFromDb(dbInbound),
|
||||
dbInbound.id,
|
||||
hosts,
|
||||
nodeAddress,
|
||||
fallbackHostname,
|
||||
);
|
||||
if (inbound.protocol === Protocols.WIREGUARD) {
|
||||
const peerRemark = client?.email
|
||||
? `${dbInbound.remark}-${client.email}`
|
||||
: dbInbound.remark || '';
|
||||
setWireguardConfigs(
|
||||
genWireguardPeerConfigs({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}),
|
||||
);
|
||||
setWireguardLinks(
|
||||
genWireguardPeerLinks({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}),
|
||||
);
|
||||
setAmneziawgConfigs([]);
|
||||
setAmneziawgLinks([]);
|
||||
setLinks([]);
|
||||
} else if (inbound.protocol === Protocols.AMNEZIAWG) {
|
||||
const peerRemark = client?.email
|
||||
? `${dbInbound.remark}-${client.email}`
|
||||
: dbInbound.remark || '';
|
||||
setAmneziawgConfigs(
|
||||
genAmneziaWGPeerConfigs({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}),
|
||||
);
|
||||
setAmneziawgLinks(
|
||||
genAmneziaWGPeerLinks({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}),
|
||||
);
|
||||
setWireguardConfigs([]);
|
||||
setWireguardLinks([]);
|
||||
setLinks([]);
|
||||
} else {
|
||||
setLinks(
|
||||
genAllLinks({
|
||||
inbound,
|
||||
remark: dbInbound.remark || '',
|
||||
client: client ?? {},
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}),
|
||||
);
|
||||
setWireguardConfigs([]);
|
||||
setWireguardLinks([]);
|
||||
setAmneziawgConfigs([]);
|
||||
setAmneziawgLinks([]);
|
||||
}
|
||||
|
||||
const subId = client?.subId;
|
||||
let nextSub = '';
|
||||
let nextSubJson = '';
|
||||
if (subSettings?.enable && subId) {
|
||||
nextSub = (subSettings.subURI || '') + subId;
|
||||
nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : '';
|
||||
}
|
||||
setSubLink(nextSub);
|
||||
setSubJsonLink(nextSubJson);
|
||||
}
|
||||
|
||||
const qrItems = useMemo<QrItem[]>(() => {
|
||||
const items: QrItem[] = [];
|
||||
if (subLink) {
|
||||
items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
|
||||
}
|
||||
if (subJsonLink) {
|
||||
items.push({
|
||||
key: 'sub-json',
|
||||
header: `${t('subscription.title')} (JSON)`,
|
||||
value: subJsonLink,
|
||||
});
|
||||
}
|
||||
links.forEach((link, idx) => {
|
||||
items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
|
||||
});
|
||||
const pushTunnelPeers = (prefix: string, configs: string[][], peerLinks: string[][]) => {
|
||||
configs.forEach((peerConfigs, idx) => {
|
||||
peerConfigs.forEach((cfg, j) => {
|
||||
const multi = peerConfigs.length > 1 ? ` #${j + 1}` : '';
|
||||
items.push({
|
||||
key: `${prefix}c${idx}-${j}`,
|
||||
header: `Peer ${idx + 1} config${multi}`,
|
||||
value: cfg,
|
||||
downloadName: peerConfFileName(idx, j, peerConfigs.length),
|
||||
});
|
||||
const link = peerLinks[idx]?.[j];
|
||||
if (link) {
|
||||
items.push({
|
||||
key: `${prefix}l${idx}-${j}`,
|
||||
header: `Peer ${idx + 1} link${multi}`,
|
||||
value: link,
|
||||
showQr: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
pushTunnelPeers('w', wireguardConfigs, wireguardLinks);
|
||||
pushTunnelPeers('a', amneziawgConfigs, amneziawgLinks);
|
||||
return items;
|
||||
}, [
|
||||
subLink,
|
||||
subJsonLink,
|
||||
links,
|
||||
wireguardConfigs,
|
||||
wireguardLinks,
|
||||
amneziawgConfigs,
|
||||
amneziawgLinks,
|
||||
t,
|
||||
]);
|
||||
|
||||
const collapseItems: CollapseProps['items'] = useMemo(
|
||||
() =>
|
||||
qrItems.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.header,
|
||||
children: (
|
||||
<QrPanel
|
||||
value={item.value}
|
||||
remark={item.header}
|
||||
downloadName={item.downloadName || ''}
|
||||
showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[qrItems],
|
||||
);
|
||||
|
||||
const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null;
|
||||
const [syncedFirstKey, setSyncedFirstKey] = useState<string | null>(null);
|
||||
if (firstKey !== syncedFirstKey) {
|
||||
setSyncedFirstKey(firstKey);
|
||||
setActiveKey(firstKey ? [firstKey] : []);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title={t('qrCode')}
|
||||
footer={null}
|
||||
width={420}
|
||||
destroyOnHidden
|
||||
>
|
||||
{dbInbound && collapseItems && collapseItems.length > 0 && (
|
||||
<Collapse
|
||||
ghost
|
||||
activeKey={activeKey}
|
||||
onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
|
||||
items={collapseItems}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export { default as QrPanel } from './QrPanel';
|
||||
export { default as QrCodeModal } from './QrCodeModal';
|
||||
|
||||
@@ -6,7 +6,6 @@ import { parseMsg } from '@/utils/zodValidate';
|
||||
import { DBInbound, coerceInboundJsonField } from '@/models/dbinbound';
|
||||
import type { ClientStats, DBInboundInit } from '@/models/dbinbound';
|
||||
import { Protocols } from '@/schemas/primitives';
|
||||
import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
||||
import { setDatepicker } from '@/hooks/useDatepicker';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
|
||||
@@ -351,10 +350,8 @@ export function useInbounds() {
|
||||
const protocol = dbInbound.protocol;
|
||||
if (!TRACKED_PROTOCOLS.includes(protocol)) continue;
|
||||
const settings = coerceInboundJsonField(dbInbound.settings) as {
|
||||
method?: string;
|
||||
clients?: Array<{ email?: string; enable?: boolean; comment?: string }>;
|
||||
};
|
||||
if (protocol === Protocols.SHADOWSOCKS && !isSSMultiUser({ protocol, settings })) continue;
|
||||
counts[dbInbound.id] = rollupClients(dbInbound, { clients: settings.clients });
|
||||
}
|
||||
return counts;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import {
|
||||
genWireguardLinks,
|
||||
getInboundClients,
|
||||
} from '@/lib/xray/inbound-link';
|
||||
import { canEnableTlsFlow, isSS2022, isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
||||
import { canEnableTlsFlow, isSS2022 } from '@/lib/xray/protocol-capabilities';
|
||||
|
||||
const FALLBACK_HOST = 'panel.example.test';
|
||||
|
||||
@@ -171,24 +171,6 @@ describe('inboundFromDb', () => {
|
||||
});
|
||||
|
||||
describe('protocol-capability helpers with raw coerced shapes', () => {
|
||||
it('isSSMultiUser returns true for legacy SS methods', () => {
|
||||
expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('isSSMultiUser returns false for single-user blake3-chacha20 method', () => {
|
||||
expect(
|
||||
isSSMultiUser({
|
||||
protocol: 'shadowsocks',
|
||||
settings: { method: '2022-blake3-chacha20-poly1305' },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('isSS2022 detects 2022-blake3 family', () => {
|
||||
expect(
|
||||
isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
canEnableStream,
|
||||
canEnableVisionSeed,
|
||||
isSS2022,
|
||||
isSSMultiUser,
|
||||
} from '@/lib/xray/protocol-capabilities';
|
||||
|
||||
// Pure-function tests for the capability predicates. Each fixture × stream
|
||||
@@ -67,7 +66,6 @@ describe('protocol capability predicates', () => {
|
||||
canEnableStream: canEnableStream(values),
|
||||
canEnableVisionSeed: canEnableVisionSeed(values),
|
||||
isSS2022: isSS2022(values),
|
||||
isSSMultiUser: isSSMultiUser(values),
|
||||
};
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user