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.
240 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|