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.
This commit is contained in:
MHSanaei
2026-11-01 00:02:25 +01:00
parent 7907843941
commit 376ab34c0f
2 changed files with 29 additions and 1 deletions
+27
View File
@@ -204,3 +204,30 @@ func TestGetClientTrafficByEmailReadsClientsTable(t *testing.T) {
t.Fatalf("SubId not enriched from clients table, got %q", tr.SubId) 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)
}
}
+2 -1
View File
@@ -1533,8 +1533,9 @@ func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, erro
seen := make(map[string]struct{}, len(rows)) seen := make(map[string]struct{}, len(rows))
emails := make([]string, 0, 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 { for _, r := range rows {
if r.Email == "" { if r.Email == "" || r.Email != strings.TrimSpace(r.Email) {
continue continue
} }
if _, ok := seen[r.Email]; ok { if _, ok := seen[r.Email]; ok {