mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-09 06:35:03 +03:00
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:
@@ -1684,6 +1684,35 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
||||
return needRestart, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) validateUpdatedInboundClients(inbound *model.Inbound) error {
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateClientsRenewal(clients); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, client := range clients {
|
||||
switch inbound.Protocol {
|
||||
case model.Hysteria:
|
||||
if client.Auth == "" {
|
||||
return common.NewError("empty client ID")
|
||||
}
|
||||
case model.TUIC:
|
||||
if client.ID == "" {
|
||||
return common.NewError("empty client ID")
|
||||
}
|
||||
if client.Password == "" {
|
||||
return common.NewError("tuic client requires a password")
|
||||
}
|
||||
if client.Email == "" {
|
||||
return common.NewError("empty client email")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
legacyShareAddr := legacyMtprotoShareAddr(inbound)
|
||||
inbound.TrafficResetDay = normalizeTrafficResetDay(inbound.TrafficResetDay)
|
||||
@@ -1706,34 +1735,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
}
|
||||
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
|
||||
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
if err := validateClientsRenewal(clients); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
if inbound.Protocol == model.Hysteria {
|
||||
for _, client := range clients {
|
||||
if client.Auth == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
}
|
||||
}
|
||||
if inbound.Protocol == model.TUIC {
|
||||
for _, client := range clients {
|
||||
if client.ID == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
if client.Password == "" {
|
||||
return inbound, false, common.NewError("tuic client requires a password")
|
||||
}
|
||||
if client.Email == "" {
|
||||
return inbound, false, common.NewError("empty client email")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grandfather a row that was already stored incomplete so it stays editable;
|
||||
// only a save that breaks a previously valid TLS block is refused.
|
||||
if !s.FromNodeSync {
|
||||
@@ -1776,8 +1777,15 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
return err
|
||||
}
|
||||
oldInbound = stored
|
||||
// The form posts back the clients and enable it loaded; both have their
|
||||
// own endpoints, so only a master's push may change them here.
|
||||
if !s.FromNodeSync {
|
||||
inbound.Settings = keepStoredClientLifecycle(inbound.Settings, stored.Settings)
|
||||
inbound.Settings = keepStoredClients(inbound.Settings, stored.Settings)
|
||||
inbound.Enable = stored.Enable
|
||||
}
|
||||
// On the clients actually saved: a protocol switch keeps the stored ones.
|
||||
if err := s.validateUpdatedInboundClients(inbound); err != nil {
|
||||
return err
|
||||
}
|
||||
conflict, cErr := checkPortConflictTx(tx, inbound, inbound.Id)
|
||||
if cErr != nil {
|
||||
|
||||
Reference in New Issue
Block a user