fix: refresh stale client_traffics row when an inbound-deleted client's email is reused (#6003)

* fix: refresh stale client_traffics row when an inbound-deleted client's email is reused

AddClientStat's OnConflict was DoNothing on email, so once an inbound is
deleted (DelInbound only removes the client_inbounds link, matching
ClientService.Detach's intentional Detach-then-later-Attach behavior) the
orphaned client_traffics row for that email survives untouched. Re-creating
a client under the same email on a new inbound silently kept the old
enable/expiry_time/reset/total/inbound_id instead of adopting the new
client's config.

Switch the conflict path to DoUpdates on inbound_id/total/expiry_time/
enable/reset. up/down stay excluded on purpose: every call for an
already-attached identity carries the same config values (one call per
inbound), so the refresh is a no-op for that legitimate multi-inbound
share, while zeroing usage counters on each additional attach would erase
real traffic.

Fixes #5958

* fix: don't let AddClientStat clobber import's forced-enabled ClientStats rows

github-actions[bot] review on #6003 found that AddInbound writes client_traffics
twice for the same import payload: first inserting each ClientStats row
(DoNothing, with Enable forced true by controller.importInbound), then calling
AddClientStat once per Settings-derived client. With AddClientStat's OnConflict
now DoUpdates, that second call was unconditionally overwriting enable (and
total/expiry_time/reset/inbound_id) with the Settings.clients[].enable value —
which still holds whatever the client had at export time, silently undoing the
controller's "always import as enabled" behavior for any client disabled at
export.

Fix: track which emails were already seeded by the ClientStats loop and skip
the AddClientStat call for those emails, leaving the import path's forced
values as authoritative. Plain (non-import) creates are unaffected since
ClientStats is empty there, so every client still goes through AddClientStat's
refresh as before.

Also updated a stale comment in addClientTraffic that still described
AddClientStat as DoNothing.

Added TestAddInbound_ImportForcedEnableSurvivesDisabledSettingsClient, which
reproduces the exact regression (verified it fails without this fix) and
passes with it.
This commit is contained in:
Mr. Nickson
2026-07-21 16:57:55 +03:00
committed by GitHub
parent 79e65f63df
commit 8cd71e07ea
4 changed files with 250 additions and 7 deletions
+10
View File
@@ -830,10 +830,17 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if err := tx.Omit("ClientStats").Save(inbound).Error; err != nil {
return err
}
// Emails seeded here (import's ClientStats, e.g. the controller's forced
// Enable=true on every imported stat row) are authoritative for this call
// and must not be clobbered by the AddClientStat loop below, which derives
// its enable/total/expiry/reset from Settings.clients[] instead — a second,
// possibly-stale source for the same columns on a plain (non-import) create.
statEmails := make(map[string]bool, len(inbound.ClientStats))
for i := range inbound.ClientStats {
if inbound.ClientStats[i].Email == "" {
continue
}
statEmails[inbound.ClientStats[i].Email] = true
inbound.ClientStats[i].Id = 0
inbound.ClientStats[i].InboundId = inbound.Id
if err := tx.Clauses(clause.OnConflict{
@@ -844,6 +851,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
}
}
for _, client := range clients {
if statEmails[client.Email] {
continue
}
if err := s.AddClientStat(tx, inbound.Id, &client); err != nil {
return err
}