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
+10 -32
View File
@@ -140,44 +140,22 @@ func mergeClientLists(base, ours, current []any) []any {
return out
}
// Client limits and state the inbound form never owns: the client endpoints and
// traffic jobs change them, so a form opened earlier must not write them back.
var storedClientLifecycleKeys = []string{"enable", "expiryTime", "totalGB", "reset", "resetDay", "resetWeekday", "resetMax"}
// keepStoredClientLifecycle copies those keys from the stored settings onto every
// payload client the inbound already holds; new clients keep what they carry.
func keepStoredClientLifecycle(payload, stored string) string {
// keepStoredClients puts the stored client list back into an inbound save's
// payload: clients change through the client endpoints, never this form.
func keepStoredClients(payload, stored string) string {
var payloadM, storedM map[string]any
if json.Unmarshal([]byte(payload), &payloadM) != nil || json.Unmarshal([]byte(stored), &storedM) != nil {
return payload
}
storedClients, _ := storedM["clients"].([]any)
storedBy := indexClientsByEmail(storedClients)
payloadClients, _ := payloadM["clients"].([]any)
changed := false
for _, entry := range payloadClients {
p, email := clientEntryEmail(entry)
s, known := storedBy[email]
if !known {
continue
}
for _, key := range storedClientLifecycleKeys {
sv, inStored := s[key]
pv, inPayload := p[key]
if inStored == inPayload && reflect.DeepEqual(sv, pv) {
continue
}
changed = true
if inStored {
p[key] = sv
} else {
delete(p, key)
}
}
}
if !changed {
storedClients, has := storedM["clients"]
if reflect.DeepEqual(payloadM["clients"], storedClients) {
return payload
}
if has {
payloadM["clients"] = storedClients
} else {
delete(payloadM, "clients")
}
b, err := json.MarshalIndent(payloadM, "", " ")
if err != nil {
return payload