mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1045378e23
commit
9db91cda37
1 changed files with 16 additions and 132 deletions
|
|
@ -219,14 +219,6 @@ func (s *InboundService) getAllEmailSubIDs() (map[string]string, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lowerAll(in []string) []string {
|
|
||||||
out := make([]string, len(in))
|
|
||||||
for i, s := range in {
|
|
||||||
out[i] = strings.ToLower(s)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// emailUsedByOtherInbounds reports whether email lives in any inbound other
|
// emailUsedByOtherInbounds reports whether email lives in any inbound other
|
||||||
// than exceptInboundId. Empty email returns false.
|
// than exceptInboundId. Empty email returns false.
|
||||||
func (s *InboundService) emailUsedByOtherInbounds(email string, exceptInboundId int) (bool, error) {
|
func (s *InboundService) emailUsedByOtherInbounds(email string, exceptInboundId int) (bool, error) {
|
||||||
|
|
@ -1662,80 +1654,31 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
||||||
return false, 0, nil
|
return false, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rowByEmail := make(map[string]*xray.ClientTraffic, len(depletedRows))
|
|
||||||
depletedEmails := make([]string, 0, len(depletedRows))
|
depletedEmails := make([]string, 0, len(depletedRows))
|
||||||
for i := range depletedRows {
|
for i := range depletedRows {
|
||||||
if depletedRows[i].Email == "" {
|
if depletedRows[i].Email == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rowByEmail[strings.ToLower(depletedRows[i].Email)] = &depletedRows[i]
|
|
||||||
depletedEmails = append(depletedEmails, depletedRows[i].Email)
|
depletedEmails = append(depletedEmails, depletedRows[i].Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve inbound membership only for the depleted emails — pushing the
|
|
||||||
// filter into SQLite avoids dragging every panel client through Go for
|
|
||||||
// the common case where most clients are healthy.
|
|
||||||
var memberships []struct {
|
|
||||||
InboundId int
|
|
||||||
Tag string
|
|
||||||
Email string
|
|
||||||
SubID string `gorm:"column:sub_id"`
|
|
||||||
}
|
|
||||||
if len(depletedEmails) > 0 {
|
|
||||||
err = tx.Raw(`
|
|
||||||
SELECT inbounds.id AS inbound_id,
|
|
||||||
inbounds.tag AS tag,
|
|
||||||
JSON_EXTRACT(client.value, '$.email') AS email,
|
|
||||||
JSON_EXTRACT(client.value, '$.subId') AS sub_id
|
|
||||||
FROM inbounds,
|
|
||||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
|
||||||
WHERE LOWER(JSON_EXTRACT(client.value, '$.email')) IN ?
|
|
||||||
`, lowerAll(depletedEmails)).Scan(&memberships).Error
|
|
||||||
if err != nil {
|
|
||||||
return false, 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover the row holder's subId per email. Only siblings sharing it
|
|
||||||
// get cascaded; legacy data where two identities reuse the same email
|
|
||||||
// stays isolated to the row owner.
|
|
||||||
holderSub := make(map[string]string, len(rowByEmail))
|
|
||||||
for _, m := range memberships {
|
|
||||||
email := strings.ToLower(strings.Trim(m.Email, "\""))
|
|
||||||
row, ok := rowByEmail[email]
|
|
||||||
if !ok || m.InboundId != row.InboundId {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
holderSub[email] = strings.Trim(m.SubID, "\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
type target struct {
|
type target struct {
|
||||||
InboundId int
|
|
||||||
Tag string
|
Tag string
|
||||||
Email string
|
Email string
|
||||||
}
|
}
|
||||||
var targets []target
|
var targets []target
|
||||||
for _, m := range memberships {
|
if len(depletedEmails) > 0 {
|
||||||
email := strings.ToLower(strings.Trim(m.Email, "\""))
|
err = tx.Raw(`
|
||||||
row, ok := rowByEmail[email]
|
SELECT inbounds.tag AS tag, clients.email AS email
|
||||||
if !ok {
|
FROM clients
|
||||||
continue
|
JOIN client_inbounds ON client_inbounds.client_id = clients.id
|
||||||
|
JOIN inbounds ON inbounds.id = client_inbounds.inbound_id
|
||||||
|
WHERE inbounds.node_id IS NULL
|
||||||
|
AND clients.email IN ?
|
||||||
|
`, depletedEmails).Scan(&targets).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, 0, err
|
||||||
}
|
}
|
||||||
expected, hasSub := holderSub[email]
|
|
||||||
mSub := strings.Trim(m.SubID, "\"")
|
|
||||||
switch {
|
|
||||||
case !hasSub || expected == "":
|
|
||||||
if m.InboundId != row.InboundId {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case mSub != expected:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
targets = append(targets, target{
|
|
||||||
InboundId: m.InboundId,
|
|
||||||
Tag: m.Tag,
|
|
||||||
Email: strings.Trim(m.Email, "\""),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if p != nil && len(targets) > 0 {
|
if p != nil && len(targets) > 0 {
|
||||||
|
|
@ -1764,70 +1707,11 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
||||||
return needRestart, count, err
|
return needRestart, count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(targets) == 0 {
|
if len(depletedEmails) > 0 {
|
||||||
return needRestart, count, nil
|
if err := tx.Model(&model.ClientRecord{}).
|
||||||
}
|
Where("email IN ?", depletedEmails).
|
||||||
|
Updates(map[string]any{"enable": false, "updated_at": now}).Error; err != nil {
|
||||||
inboundEmailMap := make(map[int]map[string]struct{})
|
logger.Warning("disableInvalidClients update clients.enable:", err)
|
||||||
for _, t := range targets {
|
|
||||||
if inboundEmailMap[t.InboundId] == nil {
|
|
||||||
inboundEmailMap[t.InboundId] = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
inboundEmailMap[t.InboundId][t.Email] = struct{}{}
|
|
||||||
}
|
|
||||||
inboundIds := make([]int, 0, len(inboundEmailMap))
|
|
||||||
for id := range inboundEmailMap {
|
|
||||||
inboundIds = append(inboundIds, id)
|
|
||||||
}
|
|
||||||
var inbounds []*model.Inbound
|
|
||||||
if err = tx.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error; err != nil {
|
|
||||||
logger.Warning("disableInvalidClients fetch inbounds:", err)
|
|
||||||
return needRestart, count, nil
|
|
||||||
}
|
|
||||||
dirty := make([]*model.Inbound, 0, len(inbounds))
|
|
||||||
for _, inbound := range inbounds {
|
|
||||||
settings := map[string]any{}
|
|
||||||
if jsonErr := json.Unmarshal([]byte(inbound.Settings), &settings); jsonErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
clientsRaw, ok := settings["clients"].([]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
emailSet := inboundEmailMap[inbound.Id]
|
|
||||||
changed := false
|
|
||||||
for i := range clientsRaw {
|
|
||||||
c, ok := clientsRaw[i].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
email, _ := c["email"].(string)
|
|
||||||
if _, shouldDisable := emailSet[email]; !shouldDisable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c["enable"] = false
|
|
||||||
if row, ok := rowByEmail[strings.ToLower(email)]; ok {
|
|
||||||
c["totalGB"] = row.Total
|
|
||||||
c["expiryTime"] = row.ExpiryTime
|
|
||||||
}
|
|
||||||
c["updated_at"] = now
|
|
||||||
clientsRaw[i] = c
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if !changed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
settings["clients"] = clientsRaw
|
|
||||||
modifiedSettings, jsonErr := json.MarshalIndent(settings, "", " ")
|
|
||||||
if jsonErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
inbound.Settings = string(modifiedSettings)
|
|
||||||
dirty = append(dirty, inbound)
|
|
||||||
}
|
|
||||||
if len(dirty) > 0 {
|
|
||||||
if err = tx.Save(dirty).Error; err != nil {
|
|
||||||
logger.Warning("disableInvalidClients update inbound settings:", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue