3x-ui/frontend/src/pages/inbounds/QrCodeModal.tsx
MHSanaei 4ce2503c1e
refactor(frontend): lift Protocols + TLS_FLOW_CONTROL consts to schemas/primitives
Step 4b. The Protocols and TLS_FLOW_CONTROL enums on models/inbound.ts
were dragging five page files into that 3,300-line module just to read
literal string constants. Lifting them to schemas/primitives lets those
pages drop the @/models/inbound import entirely.

  - schemas/primitives/protocol.ts now exports a Protocols const map
    alongside the existing ProtocolSchema. TUN stays in the const for
    parity (legacy panel deployments may have saved TUN inbounds) even
    though the Go validator no longer accepts it as a new write.
  - schemas/primitives/flow.ts now exports TLS_FLOW_CONTROL. The
    empty-string default isn't keyed because the legacy never had a
    NONE entry — call sites compare against the two real flow values.

Updated five consumers:
  - useInbounds.ts: TRACKED_PROTOCOLS now annotated readonly string[]
    so .includes(string) keeps narrowing through the array literal
  - QrCodeModal.tsx, InboundInfoModal.tsx: Protocols
  - ClientFormModal.tsx, ClientBulkAddModal.tsx: TLS_FLOW_CONTROL

Suite: 89 tests across 8 files; typecheck + lint clean.

models/inbound.ts is now imported by:
  - InboundFormModal.tsx (heavy use of Inbound class + getSettings)
  - test/inbound-link.test.ts + test/shadow.test.ts + test/headers.test.ts
    (intentional — these are parity tests against the legacy class)

OutboundFormModal still imports from models/outbound. Both form modals
are the multi-day Pattern A rewrites the plan scopes separately.
2026-05-26 00:51:52 +02:00

150 lines
4.6 KiB
TypeScript

import { useEffect, 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 QrPanel from './QrPanel';
import type { SubSettings } from './useInbounds';
interface ClientSetting {
email?: string;
subId?: string;
[k: string]: unknown;
}
interface DBInboundLike {
remark?: string;
toInbound: () => InboundLike;
}
interface InboundLike {
protocol: string;
genWireguardConfigs: (remark: string, model: string, host: string) => string;
genWireguardLinks: (remark: string, model: string, host: string) => string;
genAllLinks: (remark: string, model: string, client: ClientSetting | null, host: string) => { remark?: string; link: string }[];
}
interface QrCodeModalProps {
open: boolean;
onClose: () => void;
dbInbound: DBInboundLike | null;
client?: ClientSetting | null;
remarkModel?: string;
nodeAddress?: string;
subSettings?: SubSettings;
}
interface QrItem {
key: string;
header: string;
value: string;
downloadName?: string;
}
export default function QrCodeModal({
open,
onClose,
dbInbound,
client = null,
remarkModel = '-ieo',
nodeAddress = '',
subSettings,
}: QrCodeModalProps) {
const { t } = useTranslation();
const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
const [wireguardConfigs, setWireguardConfigs] = useState<string[]>([]);
const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
const [subLink, setSubLink] = useState('');
const [subJsonLink, setSubJsonLink] = useState('');
const [activeKey, setActiveKey] = useState<string[]>([]);
useEffect(() => {
if (!open || !dbInbound) return;
const inbound = dbInbound.toInbound();
if (inbound.protocol === Protocols.WIREGUARD) {
const peerRemark = client?.email
? `${dbInbound.remark}-${client.email}`
: dbInbound.remark || '';
setWireguardConfigs(inbound.genWireguardConfigs(peerRemark, '-ieo', nodeAddress).split('\r\n'));
setWireguardLinks(inbound.genWireguardLinks(peerRemark, '-ieo', nodeAddress).split('\r\n'));
setLinks([]);
} else {
setLinks(inbound.genAllLinks(dbInbound.remark || '', remarkModel, client, nodeAddress) as { remark?: string; link: string }[]);
setWireguardConfigs([]);
setWireguardLinks([]);
}
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);
}, [open, dbInbound, client, remarkModel, nodeAddress, subSettings]);
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 });
});
wireguardConfigs.forEach((cfg, idx) => {
items.push({
key: `wc${idx}`,
header: `Peer ${idx + 1} config`,
value: cfg,
downloadName: `peer-${idx + 1}.conf`,
});
if (wireguardLinks[idx]) {
items.push({ key: `wl${idx}`, header: `Peer ${idx + 1} link`, value: wireguardLinks[idx] });
}
});
return items;
}, [subLink, subJsonLink, links, wireguardConfigs, wireguardLinks, 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.value.includes('mldsa65') && !item.value.includes('ML-KEM-768')}
/>
),
})),
[qrItems],
);
useEffect(() => {
if (!open) {
setActiveKey([]);
return;
}
setActiveKey(qrItems.length > 0 ? [qrItems[0].key] : []);
}, [open, qrItems]);
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>
);
}