diff --git a/internal/web/service/client_apply_field_test.go b/internal/web/service/client_apply_field_test.go index fd68e71a6..e4857115e 100644 --- a/internal/web/service/client_apply_field_test.go +++ b/internal/web/service/client_apply_field_test.go @@ -144,3 +144,76 @@ func TestSetClientEnableByEmail_MultiInbound(t *testing.T) { } } } + +// Regression test: removing a client's Telegram user cleared tgId on one inbound +// only, so the next sync of the sibling bound the removed account again. +func TestSetClientTelegramUserID_MultiInbound(t *testing.T) { + dbDir := t.TempDir() + t.Setenv("XUI_DB_FOLDER", dbDir) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) + + db := database.GetDB() + + const email = "multitg@example.com" + const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c333" + clientJSON := `{"clients":[{"email":"` + email + `","id":"` + uid + `","enable":true,"subId":"sub-tg-1","tgId":4242}]}` + + first := &model.Inbound{ + Tag: "vless-tg-a", Enable: true, Port: 50021, Protocol: model.VLESS, + StreamSettings: `{"network":"tcp","security":"reality"}`, Settings: clientJSON, + } + second := &model.Inbound{ + Tag: "vless-tg-b", Enable: true, Port: 50022, 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) + } + } + traffic := &xray.ClientTraffic{InboundId: first.Id, Email: email, Enable: true} + if err := db.Create(traffic).Error; err != nil { + t.Fatalf("seed traffic: %v", err) + } + + if _, err := clientSvc.SetClientTelegramUserID(&inboundSvc, traffic.Id, 0); err != nil { + t.Fatalf("SetClientTelegramUserID: %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].TgID != 0 { + t.Errorf("inbound %s still binds the removed Telegram user: %+v", ib.Tag, clients) + } + if err := clientSvc.SyncInbound(nil, ib.Id, clients); err != nil { + t.Fatalf("SyncInbound(%s): %v", ib.Tag, err) + } + } + + rec, err := clientSvc.GetRecordByEmail(nil, email) + if err != nil { + t.Fatalf("GetRecordByEmail: %v", err) + } + if rec.TgID != 0 { + t.Errorf("client record tgId = %d after the siblings were synced, want 0", rec.TgID) + } +} diff --git a/internal/web/service/client_inbound_apply.go b/internal/web/service/client_inbound_apply.go index 9da44d104..cc563cdf2 100644 --- a/internal/web/service/client_inbound_apply.go +++ b/internal/web/service/client_inbound_apply.go @@ -1339,48 +1339,11 @@ func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, traf return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId) } - clientEmail := traffic.Email - - oldClients, err := inboundSvc.GetClients(inbound) - if err != nil { - return false, err - } - - found := false - for _, oldClient := range oldClients { - if oldClient.Email == clientEmail { - found = true - break - } - } - - if !found { - return 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, 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["tgId"] = tgId - 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, err - } - inbound.Settings = string(modifiedSettings) - needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail) - return needRestart, err + // On every attached inbound: a sibling left holding the old tgId hands it + // back to the client record on its next SyncInbound (#5039). + return s.applyClientFieldByEmail(inboundSvc, traffic.Email, func(c map[string]any) { + c["tgId"] = tgId + }) } func (s *ClientService) CheckIsEnabledByEmail(inboundSvc *InboundService, clientEmail string) (bool, error) {