QrCodeModal.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Collapse, Modal } from 'antd';
  4. import type { CollapseProps } from 'antd';
  5. import { Protocols } from '@/schemas/primitives';
  6. import {
  7. genAllLinks,
  8. genAmneziaWGPeerConfigs,
  9. genAmneziaWGPeerLinks,
  10. genWireguardPeerConfigs,
  11. genWireguardPeerLinks,
  12. isPostQuantumLink,
  13. preferPublicHost,
  14. } from '@/lib/xray/inbound-link';
  15. import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
  16. import { withHostEndpoints } from '@/lib/hosts/host-link';
  17. import type { HostRecord } from '@/schemas/api/host';
  18. import QrPanel from './QrPanel';
  19. import { peerConfFileName } from '../info/helpers';
  20. import type { SubSettings } from '../useInbounds';
  21. interface ClientSetting {
  22. email?: string;
  23. subId?: string;
  24. [k: string]: unknown;
  25. }
  26. interface QrCodeModalProps {
  27. open: boolean;
  28. onClose: () => void;
  29. dbInbound: (DbInboundLike & { id: number; remark?: string }) | null;
  30. client?: ClientSetting | null;
  31. nodeAddress?: string;
  32. subSettings?: SubSettings;
  33. hosts?: HostRecord[];
  34. }
  35. interface QrItem {
  36. key: string;
  37. header: string;
  38. value: string;
  39. downloadName?: string;
  40. showQr?: boolean;
  41. }
  42. const EMPTY_HOSTS: HostRecord[] = [];
  43. export default function QrCodeModal({
  44. open,
  45. onClose,
  46. dbInbound,
  47. client = null,
  48. nodeAddress = '',
  49. subSettings,
  50. hosts = EMPTY_HOSTS,
  51. }: QrCodeModalProps) {
  52. const { t } = useTranslation();
  53. const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
  54. const [wireguardConfigs, setWireguardConfigs] = useState<string[][]>([]);
  55. const [wireguardLinks, setWireguardLinks] = useState<string[][]>([]);
  56. const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[][]>([]);
  57. const [amneziawgLinks, setAmneziawgLinks] = useState<string[][]>([]);
  58. const [subLink, setSubLink] = useState('');
  59. const [subJsonLink, setSubJsonLink] = useState('');
  60. const [activeKey, setActiveKey] = useState<string[]>([]);
  61. // Building the links is a pure function of the props, so it runs during
  62. // render; an effect would paint the previous inbound's QR first.
  63. const [syncedProps, setSyncedProps] = useState<{
  64. dbInbound: typeof dbInbound;
  65. client: typeof client;
  66. nodeAddress: typeof nodeAddress;
  67. subSettings: typeof subSettings;
  68. hosts: typeof hosts;
  69. } | null>(null);
  70. if (
  71. open &&
  72. dbInbound &&
  73. (syncedProps === null ||
  74. syncedProps.dbInbound !== dbInbound ||
  75. syncedProps.client !== client ||
  76. syncedProps.nodeAddress !== nodeAddress ||
  77. syncedProps.subSettings !== subSettings ||
  78. syncedProps.hosts !== hosts)
  79. ) {
  80. setSyncedProps({ dbInbound, client, nodeAddress, subSettings, hosts });
  81. const fallbackHostname = preferPublicHost(
  82. window.location.hostname,
  83. subSettings?.publicHost ?? '',
  84. );
  85. const inbound = withHostEndpoints(
  86. inboundFromDb(dbInbound),
  87. dbInbound.id,
  88. hosts,
  89. nodeAddress,
  90. fallbackHostname,
  91. );
  92. if (inbound.protocol === Protocols.WIREGUARD) {
  93. const peerRemark = client?.email
  94. ? `${dbInbound.remark}-${client.email}`
  95. : dbInbound.remark || '';
  96. setWireguardConfigs(
  97. genWireguardPeerConfigs({
  98. inbound,
  99. remark: peerRemark,
  100. hostOverride: nodeAddress,
  101. fallbackHostname,
  102. }),
  103. );
  104. setWireguardLinks(
  105. genWireguardPeerLinks({
  106. inbound,
  107. remark: peerRemark,
  108. hostOverride: nodeAddress,
  109. fallbackHostname,
  110. }),
  111. );
  112. setAmneziawgConfigs([]);
  113. setAmneziawgLinks([]);
  114. setLinks([]);
  115. } else if (inbound.protocol === Protocols.AMNEZIAWG) {
  116. const peerRemark = client?.email
  117. ? `${dbInbound.remark}-${client.email}`
  118. : dbInbound.remark || '';
  119. setAmneziawgConfigs(
  120. genAmneziaWGPeerConfigs({
  121. inbound,
  122. remark: peerRemark,
  123. hostOverride: nodeAddress,
  124. fallbackHostname,
  125. }),
  126. );
  127. setAmneziawgLinks(
  128. genAmneziaWGPeerLinks({
  129. inbound,
  130. remark: peerRemark,
  131. hostOverride: nodeAddress,
  132. fallbackHostname,
  133. }),
  134. );
  135. setWireguardConfigs([]);
  136. setWireguardLinks([]);
  137. setLinks([]);
  138. } else {
  139. setLinks(
  140. genAllLinks({
  141. inbound,
  142. remark: dbInbound.remark || '',
  143. client: client ?? {},
  144. hostOverride: nodeAddress,
  145. fallbackHostname,
  146. }),
  147. );
  148. setWireguardConfigs([]);
  149. setWireguardLinks([]);
  150. setAmneziawgConfigs([]);
  151. setAmneziawgLinks([]);
  152. }
  153. const subId = client?.subId;
  154. let nextSub = '';
  155. let nextSubJson = '';
  156. if (subSettings?.enable && subId) {
  157. nextSub = (subSettings.subURI || '') + subId;
  158. nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : '';
  159. }
  160. setSubLink(nextSub);
  161. setSubJsonLink(nextSubJson);
  162. }
  163. const qrItems = useMemo<QrItem[]>(() => {
  164. const items: QrItem[] = [];
  165. if (subLink) {
  166. items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
  167. }
  168. if (subJsonLink) {
  169. items.push({
  170. key: 'sub-json',
  171. header: `${t('subscription.title')} (JSON)`,
  172. value: subJsonLink,
  173. });
  174. }
  175. links.forEach((link, idx) => {
  176. items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
  177. });
  178. const pushTunnelPeers = (prefix: string, configs: string[][], peerLinks: string[][]) => {
  179. configs.forEach((peerConfigs, idx) => {
  180. peerConfigs.forEach((cfg, j) => {
  181. const multi = peerConfigs.length > 1 ? ` #${j + 1}` : '';
  182. items.push({
  183. key: `${prefix}c${idx}-${j}`,
  184. header: `Peer ${idx + 1} config${multi}`,
  185. value: cfg,
  186. downloadName: peerConfFileName(idx, j, peerConfigs.length),
  187. });
  188. const link = peerLinks[idx]?.[j];
  189. if (link) {
  190. items.push({
  191. key: `${prefix}l${idx}-${j}`,
  192. header: `Peer ${idx + 1} link${multi}`,
  193. value: link,
  194. showQr: false,
  195. });
  196. }
  197. });
  198. });
  199. };
  200. pushTunnelPeers('w', wireguardConfigs, wireguardLinks);
  201. pushTunnelPeers('a', amneziawgConfigs, amneziawgLinks);
  202. return items;
  203. }, [
  204. subLink,
  205. subJsonLink,
  206. links,
  207. wireguardConfigs,
  208. wireguardLinks,
  209. amneziawgConfigs,
  210. amneziawgLinks,
  211. t,
  212. ]);
  213. const collapseItems: CollapseProps['items'] = useMemo(
  214. () =>
  215. qrItems.map((item) => ({
  216. key: item.key,
  217. label: item.header,
  218. children: (
  219. <QrPanel
  220. value={item.value}
  221. remark={item.header}
  222. downloadName={item.downloadName || ''}
  223. showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
  224. />
  225. ),
  226. })),
  227. [qrItems],
  228. );
  229. const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null;
  230. const [syncedFirstKey, setSyncedFirstKey] = useState<string | null>(null);
  231. if (firstKey !== syncedFirstKey) {
  232. setSyncedFirstKey(firstKey);
  233. setActiveKey(firstKey ? [firstKey] : []);
  234. }
  235. return (
  236. <Modal
  237. open={open}
  238. onCancel={onClose}
  239. title={t('qrCode')}
  240. footer={null}
  241. width={420}
  242. destroyOnHidden
  243. >
  244. {dbInbound && collapseItems && collapseItems.length > 0 && (
  245. <Collapse
  246. ghost
  247. activeKey={activeKey}
  248. onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
  249. items={collapseItems}
  250. />
  251. )}
  252. </Modal>
  253. );
  254. }