From afe1e42b5fd3de9977c560c07fce458cd5f9623c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 20:34:44 +0200 Subject: [PATCH] fix(clients): fall back to inbound scan when ClientRecord is missing DeleteByEmail looked up the email in client_records and returned the raw "record not found" gorm error when nothing matched, even though the client could still live inside an inbound's settings.clients JSON (legacy entries that SyncInbound never picked up, or rows deleted out from under a stale inbound). The user-visible delete then fails mysteriously while xray happily keeps serving the client. When GetRecordByEmail returns ErrRecordNotFound, walk inbounds whose settings JSON references the email and run DelInboundClientByEmail on each. The traffic / IP rows are cleaned up at the end unless keepTraffic is set. If no inbound carries the email either, surface a clear "client %q not found in any inbound or client record" error instead. --- web/service/client.go | 68 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/web/service/client.go b/web/service/client.go index 34f5c84c..964bf0d5 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -804,10 +804,74 @@ func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string, return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) - if err != nil { + if err == nil { + return s.Delete(inboundSvc, rec.Id, keepTraffic) + } + if !errors.Is(err, gorm.ErrRecordNotFound) { return false, err } - return s.Delete(inboundSvc, rec.Id, keepTraffic) + inboundIds, idsErr := s.findInboundIdsByClientEmail(email) + if idsErr != nil { + return false, idsErr + } + if len(inboundIds) == 0 { + return false, common.NewError(fmt.Sprintf("client %q not found in any inbound or client record", email)) + } + needRestart := false + for _, ibId := range inboundIds { + nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email) + if delErr != nil { + return needRestart, delErr + } + if nr { + needRestart = true + } + } + if !keepTraffic { + db := database.GetDB() + if err := db.Where("email = ?", email).Delete(&xray.ClientTraffic{}).Error; err != nil { + return needRestart, err + } + if err := db.Where("client_email = ?", email).Delete(&model.InboundClientIps{}).Error; err != nil { + return needRestart, err + } + } + return needRestart, nil +} + +// findInboundIdsByClientEmail returns every inbound whose settings.clients[] +// JSON contains an entry with the given email. Driver-portable (no JSON +// operators) by parsing in Go — fine for the rare fallback path. +func (s *ClientService) findInboundIdsByClientEmail(email string) ([]int, error) { + var inbounds []model.Inbound + if err := database.GetDB(). + Select("id, settings"). + Where("settings LIKE ?", "%"+email+"%"). + Find(&inbounds).Error; err != nil { + return nil, err + } + out := make([]int, 0, len(inbounds)) + for _, ib := range inbounds { + var settings map[string]any + if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil { + continue + } + clients, ok := settings["clients"].([]any) + if !ok { + continue + } + for _, c := range clients { + cm, ok := c.(map[string]any) + if !ok { + continue + } + if cEmail, _ := cm["email"].(string); cEmail == email { + out = append(out, ib.Id) + break + } + } + } + return out, nil } func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string, updated model.Client) (bool, error) {