diff --git a/internal/web/service/client_inbound_apply.go b/internal/web/service/client_inbound_apply.go index 80105abd9..afce4d888 100644 --- a/internal/web/service/client_inbound_apply.go +++ b/internal/web/service/client_inbound_apply.go @@ -807,7 +807,9 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo } // Rename the client record in the same transaction as the settings JSON // so no concurrent SyncInbound can see one renamed without the other. - if len(oldEmail) > 0 && !strings.EqualFold(oldEmail, clients[0].Email) { + // Byte-level compare (not EqualFold): case-only edits must rename too, + // otherwise SyncInbound's case-sensitive lookup creates a duplicate row. + if len(oldEmail) > 0 && oldEmail != clients[0].Email { var renameTaken int64 if e := tx.Model(&model.ClientRecord{}).Where("email = ?", clients[0].Email).Count(&renameTaken).Error; e != nil { return e diff --git a/internal/web/service/client_update_rename_test.go b/internal/web/service/client_update_rename_test.go index d14823cb0..5240c4aeb 100644 --- a/internal/web/service/client_update_rename_test.go +++ b/internal/web/service/client_update_rename_test.go @@ -46,6 +46,36 @@ func TestUpdateInboundClientRenameDoesNotDuplicateRecord(t *testing.T) { } } +func TestUpdateInboundClientCaseOnlyRenameDoesNotDuplicateRecord(t *testing.T) { + setupBulkDB(t) + svc := &ClientService{} + inboundSvc := &InboundService{} + + source := []model.Client{{Email: "test", ID: "aaaaaaaa-0000-0000-0000-000000000002", SubID: "sub-case", Enable: true}} + ib := mkInbound(t, 22002, model.VLESS, clientsSettings(t, source)) + if err := svc.SyncInbound(nil, ib.Id, source); err != nil { + t.Fatalf("seed linkage: %v", err) + } + origId := lookupClientRecord(t, "test").Id + + updated := source[0] + updated.Email = "Test" + if _, err := svc.Update(inboundSvc, origId, updated); err != nil { + t.Fatalf("Update case-only email: %v", err) + } + + if n := countClientRecords(t); n != 1 { + t.Fatalf("client records after case-only rename = %d, want 1", n) + } + rec := lookupClientRecord(t, "Test") + if rec.Id != origId { + t.Fatalf("record id after case-only rename = %d, want %d", rec.Id, origId) + } + if rec.Email != "Test" { + t.Fatalf("email after case-only rename = %q, want %q", rec.Email, "Test") + } +} + func TestClientUpdateDuplicateSubIDDoesNotRenameEmail(t *testing.T) { setupBulkDB(t) svc := &ClientService{}