ソースを参照

feat(frontend): protocol tab VLESS auth on InboundFormModal.new.tsx

Adds the protocol tab to the sibling-file rewrite — currently only the
VLESS section, which lays out decryption/encryption inputs and the three
buttons that drive them: Get New x25519, Get New mlkem768, Clear.

getNewVlessEnc + clearVlessEnc are ported from the legacy modal as
pure setFieldValue paths into ['settings', 'decryption'] /
['settings', 'encryption'] — no class methods, no inboundRef. The
matchesVlessAuth helper mirrors the legacy fuzzy label-matching so the
backend response shape stays the only source of truth.

selectedVlessAuth derives the displayed auth label from the encryption
string via Form.useWatch — same heuristic as the legacy modal
(.length > 300 → mlkem768, otherwise x25519).

Tab spread is conditional: the protocol tab only appears when
protocol === 'vless' right now. As more protocol sections land
(shadowsocks, http/mixed, tunnel, tun, wireguard) the condition will
widen to cover each one.
MHSanaei 22 時間 前
コミット
102465f9d1
1 ファイル変更83 行追加0 行削除
  1. 83 0
      frontend/src/pages/inbounds/InboundFormModal.new.tsx

+ 83 - 0
frontend/src/pages/inbounds/InboundFormModal.new.tsx

@@ -2,15 +2,18 @@ import { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import dayjs from 'dayjs';
 import {
+  Button,
   Checkbox,
   Form,
   Input,
   InputNumber,
   Modal,
   Select,
+  Space,
   Switch,
   Tabs,
   Tooltip,
+  Typography,
   message,
 } from 'antd';
 
@@ -36,6 +39,8 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery';
 // InboundsPage continues to render the old InboundFormModal.tsx until the
 // atomic swap at the end (Core Decision 7).
 
+const { Text } = Typography;
+
 const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
 const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
 const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
@@ -89,6 +94,52 @@ export default function InboundFormModalNew({
   const protocol = Form.useWatch('protocol', form) ?? '';
   const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol);
   const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false;
+  const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? '';
+
+  const matchesVlessAuth = (
+    block: { id?: string; label?: string } | undefined | null,
+    authId: string,
+  ) => {
+    if (block?.id === authId) return true;
+    const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
+    if (authId === 'mlkem768') return label.includes('mlkem768');
+    if (authId === 'x25519') return label.includes('x25519');
+    return false;
+  };
+
+  const getNewVlessEnc = async (authId: string) => {
+    if (!authId) return;
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
+      if (!msg?.success) return;
+      const obj = msg.obj as {
+        auths?: { decryption: string; encryption: string; label?: string; id?: string }[];
+      };
+      const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
+      if (!block) return;
+      form.setFieldValue(['settings', 'decryption'], block.decryption);
+      form.setFieldValue(['settings', 'encryption'], block.encryption);
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const clearVlessEnc = () => {
+    form.setFieldValue(['settings', 'decryption'], 'none');
+    form.setFieldValue(['settings', 'encryption'], 'none');
+  };
+
+  const selectedVlessAuth = (() => {
+    const enc = typeof vlessEncryption === 'string' ? vlessEncryption : '';
+    if (!enc || enc === 'none') return 'None';
+    const parts = enc.split('.').filter(Boolean);
+    const authKey = parts[parts.length - 1] || '';
+    if (!authKey) return t('pages.inbounds.vlessAuthCustom');
+    return authKey.length > 300
+      ? t('pages.inbounds.vlessAuthMlkem768')
+      : t('pages.inbounds.vlessAuthX25519');
+  })();
 
   useEffect(() => {
     if (!open) return;
@@ -273,6 +324,35 @@ export default function InboundFormModalNew({
     </>
   );
 
+  const protocolTab = (
+    <>
+      {protocol === Protocols.VLESS && (
+        <>
+          <Form.Item name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
+            <Input />
+          </Form.Item>
+          <Form.Item name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
+            <Input />
+          </Form.Item>
+          <Form.Item label=" ">
+            <Space size={8} wrap>
+              <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
+                {t('pages.inbounds.vlessAuthX25519')}
+              </Button>
+              <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
+                {t('pages.inbounds.vlessAuthMlkem768')}
+              </Button>
+              <Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
+            </Space>
+            <Text type="secondary" className="vless-auth-state">
+              {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
+            </Text>
+          </Form.Item>
+        </>
+      )}
+    </>
+  );
+
   const sniffingTab = (
     <>
       <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
@@ -357,6 +437,9 @@ export default function InboundFormModalNew({
         >
           <Tabs items={[
             { key: 'basic', label: t('pages.xray.basicTemplate'), children: basicTab },
+            ...(protocol === Protocols.VLESS
+              ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab }]
+              : []),
             { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
           ]} />
         </Form>