diff --git a/internal/web/service/client_update_rename_test.go b/internal/web/service/client_update_rename_test.go index 163cfd137..7ead6fc83 100644 --- a/internal/web/service/client_update_rename_test.go +++ b/internal/web/service/client_update_rename_test.go @@ -76,6 +76,43 @@ func TestUpdateInboundClientCaseOnlyRenameDoesNotDuplicateRecord(t *testing.T) { } } +// The IP-limit job keys its tracking rows on the casing Xray reports, so an +// inbound whose settings JSON drifted in case leaves a row under each spelling. +func TestUpdateInboundClientCaseOnlyRenameSurvivesExistingClientIpsRow(t *testing.T) { + setupBulkDB(t) + svc := &ClientService{} + inboundSvc := &InboundService{} + + source := []model.Client{{Email: "Sanaei", ID: "aaaaaaaa-0000-0000-0000-000000000009", SubID: "sub-ips", Enable: true}} + ib := mkInbound(t, 22011, model.VLESS, clientsSettings(t, source)) + if err := svc.SyncInbound(nil, ib.Id, source); err != nil { + t.Fatalf("seed linkage: %v", err) + } + for _, email := range []string{"Sanaei", "sanaei"} { + row := &model.InboundClientIps{ClientEmail: email, Ips: `[{"ip":"1.2.3.4","timestamp":1700000000}]`} + if err := database.GetDB().Create(row).Error; err != nil { + t.Fatalf("seed client ips for %q: %v", email, err) + } + } + + lowered := source + lowered[0].Email = "sanaei" + if _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{ + Id: ib.Id, + Settings: clientsSettings(t, lowered), + }, "sanaei"); err != nil { + t.Fatalf("UpdateInboundClient with a colliding client ips row: %v", err) + } + + var rows []model.InboundClientIps + if err := database.GetDB().Find(&rows).Error; err != nil { + t.Fatalf("read client ips: %v", err) + } + if len(rows) != 1 || rows[0].ClientEmail != "sanaei" { + t.Fatalf("client ips rows after rename = %+v, want a single row for %q", rows, "sanaei") + } +} + func TestClientUpdateDuplicateSubIDDoesNotRenameEmail(t *testing.T) { setupBulkDB(t) svc := &ClientService{} diff --git a/internal/web/service/inbound_client_ips.go b/internal/web/service/inbound_client_ips.go index 1b54a69e3..dabd049ac 100644 --- a/internal/web/service/inbound_client_ips.go +++ b/internal/web/service/inbound_client_ips.go @@ -152,6 +152,13 @@ func (s *InboundService) MergeInboundClientIps(incomingIps []model.InboundClient } func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error { + // The caller only renames onto a free identity, so a row already sitting on + // newEmail is stale tracking data — drop it instead of failing the edit. + if oldEmail != newEmail { + if err := tx.Where("client_email = ?", newEmail).Delete(model.InboundClientIps{}).Error; err != nil { + return err + } + } return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error }