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.
This commit is contained in:
MHSanaei 2026-05-27 20:34:44 +02:00
parent 3046d96145
commit afe1e42b5f
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A

View file

@ -804,10 +804,74 @@ func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string,
return false, common.NewError("client email is required") return false, common.NewError("client email is required")
} }
rec, err := s.GetRecordByEmail(nil, email) 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 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) { func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string, updated model.Client) (bool, error) {