Files
3x-ui-agent/frontend/src/pages/inbounds/qr/QrCodeModal.tsx
T
Muse 1bd63e6625 settings trim: remove dead settings, upstream remnants, JSON/Clash subscriptions
Per SETTINGS-AUDIT (all three tiers approved 2026-10-23):

Dead settings:
- panelOutbound, ipLimitAllowlist (+ orphan parser job), outboundDownThreshold
- tg fake events outbound.down/outbound.up/xray.crash (no publisher in lite)
- phantom routing sub-tabs (Happ/Clash/Incy) and subJsonRules direct-rules tab
  (no backend keys); subHideSettings moved to the subscription info tab first

Upstream remnants:
- ldap* / smtp* model+schema keys, EmailNotifications orphan component
- twoFactorEnable/twoFactorToken + login-page probe (no backend handler)
- externalTrafficInform*

Newly approved removals:
- JSON subscription family (producer, routes, UA auto-detect, mux/finalmask/
  observatory) and Clash subscription family (producer, routes, UA detect)
- SubBalancer (table, service, controller, settings tab)
- subThemeDir (subscription page falls back to the built-in template)
- datepicker jalalian option (gregorian only; Persian picker removed)

JSON/Clash endpoints now 404; link-format output pinned by regression tests.
Guarded keys untouched: webListen/webPort/webBasePath/certs, tg* chain,
expireDiff/trafficDiff/tgBotProxy, subHideSettings, pageSize/tgLang.
2026-10-22 19:21:07 +00:00

240 lines
6.6 KiB
TypeScript

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,
genAmneziaWGConfigs,
genAmneziaWGLinks,
genWireguardConfigs,
genWireguardLinks,
isPostQuantumLink,
preferPublicHost,
} from '@/lib/xray/inbound-link';
import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
import QrPanel from './QrPanel';
import type { SubSettings } from '../useInbounds';
interface ClientSetting {
email?: string;
subId?: string;
[k: string]: unknown;
}
interface QrCodeModalProps {
open: boolean;
onClose: () => void;
dbInbound: (DbInboundLike & { remark?: string }) | null;
client?: ClientSetting | null;
nodeAddress?: string;
subSettings?: SubSettings;
}
interface QrItem {
key: string;
header: string;
value: string;
downloadName?: string;
showQr?: boolean;
}
export default function QrCodeModal({
open,
onClose,
dbInbound,
client = null,
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 [amneziawgConfigs, setAmneziawgConfigs] = useState<string[]>([]);
const [amneziawgLinks, setAmneziawgLinks] = useState<string[]>([]);
const [subLink, setSubLink] = 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;
} | null>(null);
if (
open &&
dbInbound &&
(syncedProps === null ||
syncedProps.dbInbound !== dbInbound ||
syncedProps.client !== client ||
syncedProps.nodeAddress !== nodeAddress ||
syncedProps.subSettings !== subSettings)
) {
setSyncedProps({ dbInbound, client, nodeAddress, subSettings });
const inbound = inboundFromDb(dbInbound);
const fallbackHostname = preferPublicHost(
window.location.hostname,
subSettings?.publicHost ?? '',
);
if (inbound.protocol === Protocols.WIREGUARD) {
const peerRemark = client?.email
? `${dbInbound.remark}-${client.email}`
: dbInbound.remark || '';
setWireguardConfigs(
genWireguardConfigs({
inbound,
remark: peerRemark,
hostOverride: nodeAddress,
fallbackHostname,
}).split('\r\n'),
);
setWireguardLinks(
genWireguardLinks({
inbound,
remark: peerRemark,
hostOverride: nodeAddress,
fallbackHostname,
}).split('\r\n'),
);
setAmneziawgConfigs([]);
setAmneziawgLinks([]);
setLinks([]);
} else if (inbound.protocol === Protocols.AMNEZIAWG) {
const peerRemark = client?.email
? `${dbInbound.remark}-${client.email}`
: dbInbound.remark || '';
setAmneziawgConfigs(
genAmneziaWGConfigs({
inbound,
remark: peerRemark,
hostOverride: nodeAddress,
fallbackHostname,
}).split('\r\n'),
);
setAmneziawgLinks(
genAmneziaWGLinks({
inbound,
remark: peerRemark,
hostOverride: nodeAddress,
fallbackHostname,
}).split('\r\n'),
);
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 = '';
if (subSettings?.enable && subId) {
nextSub = (subSettings.subURI || '') + subId;
}
setSubLink(nextSub);
}
const qrItems = useMemo<QrItem[]>(() => {
const items: QrItem[] = [];
if (subLink) {
items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
}
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],
showQr: false,
});
}
});
amneziawgConfigs.forEach((cfg, idx) => {
items.push({
key: `ac${idx}`,
header: `Peer ${idx + 1} config`,
value: cfg,
downloadName: `peer-${idx + 1}.conf`,
});
if (amneziawgLinks[idx]) {
items.push({
key: `al${idx}`,
header: `Peer ${idx + 1} link`,
value: amneziawgLinks[idx],
showQr: false,
});
}
});
return items;
}, [subLink, 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>
);
}