diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index c01b8323..45351144 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/frontend/src/pages/inbounds/InboundFormModal.vue @@ -70,8 +70,11 @@ const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const inbound = ref(null); const dbForm = ref(null); const saving = ref(false); -const advancedJson = ref({ stream: '', sniffing: '', settings: '' }); +const advancedStreamText = ref(''); +const advancedSniffingText = ref(''); +const advancedSettingsText = ref(''); const activeTabKey = ref('basic'); +const advancedSectionKey = ref('all'); // Cached default cert/key paths from /panel/setting/defaultSettings — // powers the "Set default cert" button on the TLS form. const defaultCert = ref(''); @@ -224,13 +227,13 @@ function freshDbForm() { function primeAdvancedJson() { if (!inbound.value) return; try { - advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); + advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); } catch (_e) { /* keep prior text */ } try { - advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); + advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); } catch (_e) { /* keep prior text */ } try { - advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); + advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); } catch (_e) { /* keep prior text */ } } @@ -244,6 +247,7 @@ watch(() => props.open, (next) => { primeAdvancedJson(); } activeTabKey.value = 'basic'; + advancedSectionKey.value = 'all'; fetchDefaultCertSettings(); }); @@ -253,18 +257,18 @@ function applyAdvancedJsonToBasic() { let parsedStream; let parsedSniffing; try { - parsedSettings = advancedJson.value.settings.trim() - ? JSON.parse(advancedJson.value.settings) + parsedSettings = advancedSettingsText.value.trim() + ? JSON.parse(advancedSettingsText.value) : inbound.value.settings?.toJson?.(); } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; } try { - parsedStream = advancedJson.value.stream.trim() - ? JSON.parse(advancedJson.value.stream) + parsedStream = advancedStreamText.value.trim() + ? JSON.parse(advancedStreamText.value) : inbound.value.stream?.toJson?.(); } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; } try { - parsedSniffing = advancedJson.value.sniffing.trim() - ? JSON.parse(advancedJson.value.sniffing) + parsedSniffing = advancedSniffingText.value.trim() + ? JSON.parse(advancedSniffingText.value) : inbound.value.sniffing?.toJson?.(); } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; } @@ -324,6 +328,203 @@ function onNetworkChange(next) { } } +function parseAdvancedSliceOrFallback(rawText, fallbackValue) { + if (!rawText?.trim()) return fallbackValue; + return JSON.parse(rawText); +} + +function unwrapWrappedObject(parsed, key) { + if ( + parsed + && typeof parsed === 'object' + && !Array.isArray(parsed) + && parsed[key] !== undefined + ) { + return parsed[key]; + } + return parsed; +} + +const advancedAllConfig = computed({ + get: () => { + if (!inbound.value) return ''; + try { + const settings = parseAdvancedSliceOrFallback( + advancedSettingsText.value, + inbound.value.settings?.toJson?.() || {}, + ); + const streamSettings = parseAdvancedSliceOrFallback( + advancedStreamText.value, + inbound.value.stream?.toJson?.() || {}, + ); + const sniffing = parseAdvancedSliceOrFallback( + advancedSniffingText.value, + inbound.value.sniffing?.toJson?.() || {}, + ); + return JSON.stringify({ + listen: inbound.value.listen, + port: inbound.value.port, + protocol: inbound.value.protocol, + settings, + sniffing, + streamSettings, + tag: inbound.value.tag, + }, null, 2); + } catch (_e) { + return ''; + } + }, + set: (next) => { + let parsed; + try { + parsed = JSON.parse(next); + } catch (e) { + message.error(`All JSON invalid: ${e.message}`); + return; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + message.error('All JSON must be an inbound object.'); + return; + } + + try { + if (typeof parsed.listen === 'string') { + inbound.value.listen = parsed.listen; + } + if (parsed.port !== undefined) { + const parsedPort = Number(parsed.port); + if (!Number.isNaN(parsedPort) && Number.isFinite(parsedPort)) { + inbound.value.port = parsedPort; + } + } + if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) { + inbound.value.protocol = parsed.protocol; + } + if (typeof parsed.tag === 'string') { + inbound.value.tag = parsed.tag; + } + + const existingSettings = parseAdvancedSliceOrFallback( + advancedSettingsText.value, + inbound.value?.settings?.toJson?.() || {}, + ); + const settings = parsed.settings ?? existingSettings; + const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {}); + const sniffing = parsed.sniffing ?? (inbound.value?.sniffing?.toJson?.() || {}); + advancedSettingsText.value = JSON.stringify(settings, null, 2); + advancedStreamText.value = JSON.stringify(streamSettings, null, 2); + advancedSniffingText.value = JSON.stringify(sniffing, null, 2); + } catch (e) { + message.error(`All JSON invalid: ${e.message}`); + } + }, +}); + +const advancedSettingsConfig = computed({ + get: () => { + if (!inbound.value) return ''; + try { + const settings = parseAdvancedSliceOrFallback( + advancedSettingsText.value, + inbound.value.settings?.toJson?.() || {}, + ); + return JSON.stringify({ + settings, + }, null, 2); + } catch (_e) { + return ''; + } + }, + set: (next) => { + let parsed; + try { + parsed = JSON.parse(next); + } catch (e) { + message.error(`Settings JSON invalid: ${e.message}`); + return; + } + const unwrapped = unwrapWrappedObject(parsed, 'settings'); + if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) { + message.error('Settings JSON must be an object or { settings: { ... } }.'); + return; + } + + try { + advancedSettingsText.value = JSON.stringify(unwrapped, null, 2); + } catch (e) { + message.error(`Settings JSON invalid: ${e.message}`); + } + }, +}); + +const advancedSniffingConfig = computed({ + get: () => { + if (!inbound.value) return ''; + try { + const sniffing = parseAdvancedSliceOrFallback( + advancedSniffingText.value, + inbound.value.sniffing?.toJson?.() || {}, + ); + return JSON.stringify({ sniffing }, null, 2); + } catch (_e) { + return ''; + } + }, + set: (next) => { + let parsed; + try { + parsed = JSON.parse(next); + } catch (e) { + message.error(`Sniffing JSON invalid: ${e.message}`); + return; + } + const unwrapped = unwrapWrappedObject(parsed, 'sniffing'); + if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) { + message.error('Sniffing JSON must be an object or { sniffing: { ... } }.'); + return; + } + try { + advancedSniffingText.value = JSON.stringify(unwrapped, null, 2); + } catch (e) { + message.error(`Sniffing JSON invalid: ${e.message}`); + } + }, +}); + +const advancedStreamConfig = computed({ + get: () => { + if (!inbound.value) return ''; + try { + const streamSettings = parseAdvancedSliceOrFallback( + advancedStreamText.value, + inbound.value.stream?.toJson?.() || {}, + ); + return JSON.stringify({ streamSettings }, null, 2); + } catch (_e) { + return ''; + } + }, + set: (next) => { + let parsed; + try { + parsed = JSON.parse(next); + } catch (e) { + message.error(`Stream JSON invalid: ${e.message}`); + return; + } + const unwrapped = unwrapWrappedObject(parsed, 'streamSettings'); + if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) { + message.error('Stream JSON must be an object or { streamSettings: { ... } }.'); + return; + } + try { + advancedStreamText.value = JSON.stringify(unwrapped, null, 2); + } catch (e) { + message.error(`Stream JSON invalid: ${e.message}`); + } + }, +}); + // === Random helpers wired to the form's sync icons ================== function randomEmail(target) { if (target) target.email = RandomUtil.randomLowerAndNum(9); @@ -525,16 +726,16 @@ async function submit() { let settings; try { streamSettings = canEnableStream.value - ? JSON.stringify(JSON.parse(advancedJson.value.stream)) + ? JSON.stringify(JSON.parse(advancedStreamText.value)) : (inbound.value.stream?.sockopt ? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() }) : ''); } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; } try { - sniffing = JSON.stringify(JSON.parse(advancedJson.value.sniffing || inbound.value.sniffing.toString())); + sniffing = JSON.stringify(JSON.parse(advancedSniffingText.value || inbound.value.sniffing.toString())); } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; } try { - settings = JSON.stringify(JSON.parse(advancedJson.value.settings || inbound.value.settings.toString())); + settings = JSON.stringify(JSON.parse(advancedSettingsText.value || inbound.value.settings.toString())); } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; } // The structured form mutates `inbound.stream` directly when the @@ -598,7 +799,7 @@ watch( () => { if (!inbound.value?.stream) return; try { - advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); + advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); } catch (_e) { /* leave as is */ } }, ); @@ -607,7 +808,7 @@ watch( () => { if (!inbound.value?.sniffing) return; try { - advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); + advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); } catch (_e) { /* leave as is */ } }, ); @@ -616,7 +817,7 @@ watch( () => { if (!inbound.value?.settings) return; try { - advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); + advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); } catch (_e) { /* leave as is */ } }, ); @@ -1005,7 +1206,7 @@ watch( @@ -1944,20 +2145,48 @@ watch( - - - - - - - - - - - - +
+
+
+
+
Inbound JSON sections
+
+ Full inbound JSON and focused editors for settings, sniffing, and streamSettings. +
+
+
+ + + +
+ Full inbound object with all fields in one editor. +
+ +
+ +
+ Xray settings block wrapper: + { settings: { ... } }. +
+ +
+ +
+ Xray sniffing block wrapper: + { sniffing: { ... } }. +
+ +
+ +
+ Xray stream block wrapper: + { streamSettings: { ... } }. +
+ +
+
+
+
@@ -2033,6 +2262,73 @@ watch( margin-top: 4px; } +.advanced-shell { + display: flex; + flex-direction: column; + gap: 12px; +} + +.advanced-panel { + padding: 14px; + border: 1px solid rgba(128, 128, 128, 0.18); + border-radius: 12px; + background: rgba(128, 128, 128, 0.04); +} + +.advanced-panel__header { + margin-bottom: 12px; +} + +.advanced-panel__title { + font-size: 14px; + font-weight: 600; + line-height: 1.4; +} + +.advanced-panel__subtitle { + margin-top: 4px; + color: rgba(0, 0, 0, 0.6); + line-height: 1.5; +} + +.advanced-inner-tabs :deep(.ant-tabs-nav) { + margin-bottom: 12px; +} + +.advanced-inner-tabs :deep(.ant-tabs-tab) { + padding-inline: 14px; +} + +.advanced-editor-meta { + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.65); + line-height: 1.5; +} + +@media (max-width: 768px) { + .advanced-panel { + padding: 12px; + border-radius: 10px; + } + + .advanced-inner-tabs :deep(.ant-tabs-tab) { + padding-inline: 10px; + } +} + +:global(.dark) .advanced-panel__subtitle, +:global(.dark) .advanced-editor-meta, +:global(.ultra) .advanced-panel__subtitle, +:global(.ultra) .advanced-editor-meta { + color: rgba(255, 255, 255, 0.65); +} + +:global(.dark) .advanced-panel, +:global(.ultra) .advanced-panel { + border-color: rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.03); +} + .section-heading { font-weight: 500; margin: 12px 0 6px;