diff --git a/internal/web/service/client_apply_field_test.go b/internal/web/service/client_apply_field_test.go index 56db2df2e..952258208 100644 --- a/internal/web/service/client_apply_field_test.go +++ b/internal/web/service/client_apply_field_test.go @@ -7,6 +7,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/xray" ) // TestResetClientExpiryTimeByEmail_MultiInbound reproduces #5039: a client @@ -86,3 +87,68 @@ func TestResetClientExpiryTimeByEmail_MultiInbound(t *testing.T) { t.Errorf("client record expiry = %d, want %d", rec.ExpiryTime, newExpiry) } } + +// A disable-by-email (Telegram bot / LDAP sync) must flip enable on every +// inbound the client is attached to. Patching only the first inbound left the +// client connecting through its siblings' running Xray. +func TestSetClientEnableByEmail_MultiInbound(t *testing.T) { + dbDir := t.TempDir() + t.Setenv("XUI_DB_FOLDER", dbDir) + if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + + db := database.GetDB() + + const email = "multienable@example.com" + const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c222" + clientJSON := `{"clients":[{"email":"` + email + `","id":"` + uid + `","enable":true,"subId":"sub-en-1"}]}` + + first := &model.Inbound{ + Tag: "vless-en-a", Enable: true, Port: 50011, Protocol: model.VLESS, + StreamSettings: `{"network":"tcp","security":"reality"}`, Settings: clientJSON, + } + second := &model.Inbound{ + Tag: "vless-en-b", Enable: true, Port: 50012, Protocol: model.VLESS, + StreamSettings: `{"network":"ws","security":"tls"}`, Settings: clientJSON, + } + for _, ib := range []*model.Inbound{first, second} { + if err := db.Create(ib).Error; err != nil { + t.Fatalf("create inbound %s: %v", ib.Tag, err) + } + } + + clientSvc := ClientService{} + inboundSvc := InboundService{} + for _, ib := range []*model.Inbound{first, second} { + clients, err := inboundSvc.GetClients(ib) + if err != nil { + t.Fatalf("GetClients(%s): %v", ib.Tag, err) + } + if err := clientSvc.SyncInbound(nil, ib.Id, clients); err != nil { + t.Fatalf("SyncInbound(%s): %v", ib.Tag, err) + } + } + if err := db.Create(&xray.ClientTraffic{InboundId: first.Id, Email: email, Enable: true}).Error; err != nil { + t.Fatalf("seed traffic: %v", err) + } + + if _, _, err := clientSvc.SetClientEnableByEmail(&inboundSvc, email, false); err != nil { + t.Fatalf("SetClientEnableByEmail: %v", err) + } + + for _, ib := range []*model.Inbound{first, second} { + fresh, err := inboundSvc.GetInbound(ib.Id) + if err != nil { + t.Fatalf("GetInbound(%s): %v", ib.Tag, err) + } + clients, err := inboundSvc.GetClients(fresh) + if err != nil { + t.Fatalf("GetClients(%s): %v", ib.Tag, err) + } + if len(clients) != 1 || clients[0].Enable { + t.Errorf("inbound %s: client still enabled after disable-by-email; a sibling inbound kept access", ib.Tag) + } + } +} diff --git a/internal/web/service/client_inbound_apply.go b/internal/web/service/client_inbound_apply.go index 89e787fa0..80105abd9 100644 --- a/internal/web/service/client_inbound_apply.go +++ b/internal/web/service/client_inbound_apply.go @@ -1142,62 +1142,18 @@ func (s *ClientService) CheckIsEnabledByEmail(inboundSvc *InboundService, client } func (s *ClientService) ToggleClientEnableByEmail(inboundSvc *InboundService, clientEmail string) (bool, bool, error) { - _, inbound, err := inboundSvc.GetClientInboundByEmail(clientEmail) + current, err := s.CheckIsEnabledByEmail(inboundSvc, clientEmail) if err != nil { return false, false, err } - if inbound == nil { - return false, false, common.NewError("Inbound Not Found For Email:", clientEmail) - } - - oldClients, err := inboundSvc.GetClients(inbound) - if err != nil { - return false, false, err - } - - found := false - clientOldEnabled := false - - for _, oldClient := range oldClients { - if oldClient.Email == clientEmail { - found = true - clientOldEnabled = oldClient.Enable - break - } - } - - if !found { - return false, false, common.NewError("Client Not Found For Email:", clientEmail) - } - - var settings map[string]any - err = json.Unmarshal([]byte(inbound.Settings), &settings) - if err != nil { - return false, false, err - } - clients := settings["clients"].([]any) - var newClients []any - for client_index := range clients { - c := clients[client_index].(map[string]any) - if c["email"] == clientEmail { - c["enable"] = !clientOldEnabled - c["updated_at"] = time.Now().Unix() * 1000 - newClients = append(newClients, any(c)) - } - } - settings["clients"] = newClients - modifiedSettings, err := json.MarshalIndent(settings, "", " ") - if err != nil { - return false, false, err - } - inbound.Settings = string(modifiedSettings) - - needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail) + target := !current + needRestart, err := s.applyClientFieldByEmail(inboundSvc, clientEmail, func(c map[string]any) { + c["enable"] = target + }) if err != nil { return false, needRestart, err } - - return !clientOldEnabled, needRestart, nil + return target, needRestart, nil } func (s *ClientService) SetClientEnableByEmail(inboundSvc *InboundService, clientEmail string, enable bool) (bool, bool, error) { @@ -1208,11 +1164,13 @@ func (s *ClientService) SetClientEnableByEmail(inboundSvc *InboundService, clien if current == enable { return false, false, nil } - newEnabled, needRestart, err := s.ToggleClientEnableByEmail(inboundSvc, clientEmail) + needRestart, err := s.applyClientFieldByEmail(inboundSvc, clientEmail, func(c map[string]any) { + c["enable"] = enable + }) if err != nil { return false, needRestart, err } - return newEnabled == enable, needRestart, nil + return true, needRestart, nil } // applyClientFieldByEmail loads the inbound currently hosting clientEmail,