fix(forms): validate JSON tabs before applying or saving
InboundFormModal: switching out of the Advanced tab now parses the three JSON textareas and rebuilds the structured Inbound via Inbound.fromJson, so the Basic tab reflects what was pasted. Invalid JSON keeps the user on Advanced with a specific parse error. XrayPage: Save now parses xraySetting upfront and snaps the user back to the Advanced tab on invalid JSON instead of letting the backend reject a generic blob.
This commit is contained in:
@@ -70,6 +70,7 @@ const inbound = ref(null);
|
||||
const dbForm = ref(null);
|
||||
const saving = ref(false);
|
||||
const advancedJson = ref({ stream: '', sniffing: '', settings: '' });
|
||||
const activeTabKey = ref('basic');
|
||||
// Cached default cert/key paths from /panel/setting/defaultSettings —
|
||||
// powers the "Set default cert" button on the TLS form.
|
||||
const defaultCert = ref('');
|
||||
@@ -241,9 +242,60 @@ watch(() => props.open, (next) => {
|
||||
dbForm.value = freshDbForm();
|
||||
primeAdvancedJson();
|
||||
}
|
||||
activeTabKey.value = 'basic';
|
||||
fetchDefaultCertSettings();
|
||||
});
|
||||
|
||||
function applyAdvancedJsonToBasic() {
|
||||
if (!inbound.value) return true;
|
||||
let parsedSettings;
|
||||
let parsedStream;
|
||||
let parsedSniffing;
|
||||
try {
|
||||
parsedSettings = advancedJson.value.settings.trim()
|
||||
? JSON.parse(advancedJson.value.settings)
|
||||
: 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)
|
||||
: 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)
|
||||
: inbound.value.sniffing?.toJson?.();
|
||||
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
|
||||
|
||||
try {
|
||||
inbound.value = Inbound.fromJson({
|
||||
port: inbound.value.port,
|
||||
listen: inbound.value.listen,
|
||||
protocol: inbound.value.protocol,
|
||||
settings: parsedSettings,
|
||||
streamSettings: parsedStream,
|
||||
tag: inbound.value.tag,
|
||||
sniffing: parsedSniffing,
|
||||
clientStats: inbound.value.clientStats,
|
||||
});
|
||||
} catch (e) {
|
||||
message.error(`Advanced JSON: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let isRevertingTab = false;
|
||||
watch(activeTabKey, (next, prev) => {
|
||||
if (isRevertingTab) { isRevertingTab = false; return; }
|
||||
if (prev === 'advanced' && next !== 'advanced') {
|
||||
if (!applyAdvancedJsonToBasic()) {
|
||||
isRevertingTab = true;
|
||||
activeTabKey.value = 'advanced';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// In add mode, switching protocol restamps settings + re-syncs port.
|
||||
function onProtocolChange(next) {
|
||||
if (props.mode === 'edit' || !inbound.value) return;
|
||||
@@ -572,7 +624,7 @@ watch(
|
||||
<template>
|
||||
<a-modal :open="open" :title="title" :ok-text="okText" :cancel-text="t('close')" :confirm-loading="saving"
|
||||
:mask-closable="false" width="780px" @ok="submit" @cancel="close">
|
||||
<a-tabs v-if="inbound && dbForm" default-active-key="basic">
|
||||
<a-tabs v-if="inbound && dbForm" v-model:active-key="activeTabKey">
|
||||
<!-- ============================== BASICS ============================== -->
|
||||
<a-tab-pane key="basic" :tab="t('pages.xray.basicTemplate')">
|
||||
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
|
||||
|
||||
@@ -186,9 +186,6 @@ function onRemoveRoutingRules({ prefix }) {
|
||||
);
|
||||
}
|
||||
|
||||
// `message` is used by some of the in-progress UX flows (kept around
|
||||
// because future provisioning errors will surface through it).
|
||||
void message;
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
@@ -230,6 +227,17 @@ function onTabChange(key) {
|
||||
}
|
||||
}
|
||||
|
||||
function onSaveAll() {
|
||||
try {
|
||||
JSON.parse(xraySetting.value);
|
||||
} catch (e) {
|
||||
message.error(`Advanced JSON: ${e.message}`);
|
||||
activeTabKey.value = 'tpl-advanced';
|
||||
return;
|
||||
}
|
||||
saveAll();
|
||||
}
|
||||
|
||||
function syncTabFromHash() {
|
||||
const key = keyBySlug[window.location.hash.slice(1)];
|
||||
if (key) activeTabKey.value = key;
|
||||
@@ -268,7 +276,7 @@ onBeforeUnmount(() => {
|
||||
<a-row class="header-row">
|
||||
<a-col :xs="24" :sm="14" class="header-actions">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
||||
<a-button type="primary" :disabled="saveDisabled" @click="onSaveAll">
|
||||
{{ t('pages.xray.save') }}
|
||||
</a-button>
|
||||
<a-button type="primary" danger :disabled="!saveDisabled" @click="confirmRestart">
|
||||
|
||||
+36
-42
@@ -1,4 +1,21 @@
|
||||
{
|
||||
"api": {
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
],
|
||||
"tag": "api"
|
||||
},
|
||||
"inbounds": [{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "tunnel",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
},
|
||||
"tag": "api"
|
||||
}],
|
||||
"log": {
|
||||
"access": "none",
|
||||
"dnsLog": false,
|
||||
@@ -6,39 +23,21 @@
|
||||
"loglevel": "warning",
|
||||
"maskAddress": ""
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
]
|
||||
"metrics": {
|
||||
"listen": "127.0.0.1:11111",
|
||||
"tag": "metrics_out"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "api",
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "tunnel",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "direct",
|
||||
"outbounds": [{
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "AsIs",
|
||||
"redirect": "",
|
||||
"noises": []
|
||||
}
|
||||
"domainStrategy": "AsIs"
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"tag": "blocked",
|
||||
"protocol": "blackhole",
|
||||
"settings": {}
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
@@ -57,33 +56,28 @@
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"rules": [{
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api"
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
]
|
||||
],
|
||||
"outboundTag": "blocked",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
]
|
||||
],
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {},
|
||||
"metrics": {
|
||||
"tag": "metrics_out",
|
||||
"listen": "127.0.0.1:11111"
|
||||
}
|
||||
}
|
||||
"stats": {}
|
||||
}
|
||||
Reference in New Issue
Block a user