From 376ab34c0fe0c4fa780cd9fe35c323b756230096 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 00:02:25 +0100 Subject: [PATCH] fix(clients): purge depleted clients by their exact usage key DelDepleted turned the emails of depleted usage rows into client identities through BulkDelete, which trims each email for request bodies. A depleted row keyed by a padded email, which a node's stats could store and which belongs to no client, therefore resolved to the healthy client it resembled and deleted it. Rows whose key carries surrounding white space name no client and are skipped. --- internal/web/service/bulk_traffic_test.go | 27 +++++++++++++++++++++++ internal/web/service/client_bulk.go | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/web/service/bulk_traffic_test.go b/internal/web/service/bulk_traffic_test.go index 2fbb2a878..b3fc9fdfa 100644 --- a/internal/web/service/bulk_traffic_test.go +++ b/internal/web/service/bulk_traffic_test.go @@ -204,3 +204,30 @@ func TestGetClientTrafficByEmailReadsClientsTable(t *testing.T) { t.Fatalf("SubId not enriched from clients table, got %q", tr.SubId) } } + +// A depleted usage row keyed by a padded email belongs to no client; trimming it on the +// way to BulkDelete deleted the healthy client it resembles. +func TestDelDepletedIgnoresPaddedUsageRow(t *testing.T) { + setupBulkDB(t) + svc := &ClientService{} + inboundSvc := &InboundService{} + + source := []model.Client{{Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true}} + ib := mkInbound(t, 21003, model.VLESS, clientsSettings(t, source)) + if err := svc.SyncInbound(nil, ib.Id, source); err != nil { + t.Fatalf("seed linkage: %v", err) + } + mkTraffic(t, ib.Id, "alice@x", 0, 0, 0, 0, true) + mkTraffic(t, ib.Id, "alice@x ", 0, 0, 0, 1, false) + + deleted, _, err := svc.DelDepleted(inboundSvc) + if err != nil { + t.Fatalf("DelDepleted: %v", err) + } + if deleted != 0 { + t.Fatalf("deleted %d clients, want 0", deleted) + } + if _, err := svc.GetRecordByEmail(nil, "alice@x"); err != nil { + t.Fatalf("alice was deleted through a look-alike usage row: %v", err) + } +} diff --git a/internal/web/service/client_bulk.go b/internal/web/service/client_bulk.go index a7d5733fe..1f500c8ac 100644 --- a/internal/web/service/client_bulk.go +++ b/internal/web/service/client_bulk.go @@ -1533,8 +1533,9 @@ func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, erro seen := make(map[string]struct{}, len(rows)) emails := make([]string, 0, len(rows)) + // BulkDelete trims each email, so a padded usage row would resolve to the client it resembles. for _, r := range rows { - if r.Email == "" { + if r.Email == "" || r.Email != strings.TrimSpace(r.Email) { continue } if _, ok := seen[r.Email]; ok {