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
@@ -317,4 +317,34 @@ describe('InboundFormModal', () => {
);
});
});
// Clients and enable change through their own endpoints; the server keeps the
// stored ones, so the edit form must neither send nor validate its stale copy.
it('edit save neither sends nor validates the clients it loaded', async () => {
const post = vi.mocked(HttpUtil.post);
post.mockClear();
const dbInbound = cloneLikeVlessInbound('example.com:443');
const legacy = new DBInbound({
...dbInbound,
settings: {
...(dbInbound.settings as Record<string, unknown>),
clients: [{ email: 'legacy', id: '' }],
},
});
renderCloneLikeEdit(legacy);
fireEvent.click(primaryButton());
await waitFor(() => expect(post).toHaveBeenCalled());
const payload = post.mock.calls[0][1] as { settings: string };
expect(JSON.parse(payload.settings)).not.toHaveProperty('clients');
});
it('offers the enable switch when adding an inbound but not when editing one', () => {
renderModal();
expect(document.getElementById('inbound-enable')).not.toBeNull();
cleanup();
renderCloneLikeEdit(cloneLikeVlessInbound('example.com:443'));
expect(document.getElementById('inbound-enable')).toBeNull();
});
});