fix(inbounds): keep the stored client list and enable on inbound save

Invariant: saving an inbound's configuration never changes which clients it
holds nor whether it is enabled; both have their own endpoints. The edit
modal posts back the clients and the enable flag it loaded when it opened.
A client added meanwhile (another admin, the bot, the API, LDAP) was
detached and its stats deleted; a client deleted meanwhile came back with
its credentials, restoring access that had been revoked; an inbound
switched off meanwhile was switched back on.

For every save but a master's node-sync push, UpdateInbound now takes the
client list and enable from the row it re-reads inside the writer; this
replaces the lifecycle-only carry from the previous commit. Client
validation (renewal schedule, Hysteria auth, TUIC credentials) moves after
that swap so it judges the clients actually saved: a protocol switch keeps
the stored clients, and #6268's refusal must apply to them.

The edit form no longer loads or sends clients, so neither the JSON editor
nor validation sees a copy the server ignores, and the enable switch shows
only when adding; the list toggle (/setEnable) covers existing inbounds.

Tests that added or re-keyed clients through a panel inbound save pinned
the old rule; they now drive the master-push path, where payload clients
still apply.
This commit is contained in:
MHSanaei
2026-09-28 13:23:48 +02:00
parent fb7418f7bd
commit 823db05966
13 changed files with 207 additions and 96 deletions
+15 -2
View File
@@ -353,9 +353,22 @@ export function dropLegacyOptionalEmpties(
}
}
export function formValuesToWirePayload(values: InboundFormValues): WireInboundPayload {
// An existing inbound's clients change only through the client endpoints, so
// the edit form neither loads them nor sends them back.
export function withoutClients(values: InboundFormValues): InboundFormValues {
const settings = { ...(values.settings as Record<string, unknown> | undefined) };
delete settings.clients;
return { ...values, settings } as InboundFormValues;
}
export function formValuesToWirePayload(
values: InboundFormValues,
options: { omitClients?: boolean } = {},
): WireInboundPayload {
const settingsPruned = (pruneEmpty(values.settings ?? {}) ?? {}) as Record<string, unknown>;
if (Array.isArray(settingsPruned.clients)) {
if (options.omitClients) {
delete settingsPruned.clients;
} else if (Array.isArray(settingsPruned.clients)) {
settingsPruned.clients = normalizeClients(values.protocol, settingsPruned.clients);
}
let streamPruned = values.streamSettings