mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-03 08:57:15 +00:00
fix(client): match clients by email for delete/update, not credentials
Delete/update located the client in an inbound's settings JSON by the record's credential (uuid/password/auth). When that credential drifted from the inbound JSON -- e.g. a rotated UUID left behind, or duplicated by a past partial-update bug -- the lookup failed with "Client Not Found In Inbound For ID: <uuid>" and aborted the whole operation, making the client impossible to remove from the panel. Key every delete/update/detach path on email, the client's stable identity. This survives credential drift and heals duplicate-email entries by removing all of them. - Delete/DeleteByEmail/Detach/DetachByEmailMany -> DelInboundClientByEmail - delInboundClients / bulkDelInboundClients: match settings by email - UpdateInboundClient: locate the entry to replace by email (param clientId -> oldEmail); update all callers to pass the email - bulkAdjustInboundClients: match by email - writeBackClientSubID: pass email; drop unused sourceProtocol param - make per-inbound deletion idempotent via ErrClientNotInInbound - remove now-orphaned DelInboundClient, clientKeyForProtocol and getClientPrimaryKey; scale test deletes by email
This commit is contained in:
+95
-337
@@ -63,24 +63,14 @@ func (c ClientWithAttachments) MarshalJSON() ([]byte, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientKeyForProtocol(p model.Protocol, rec *model.ClientRecord) string {
|
|
||||||
if rec == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
switch p {
|
|
||||||
case model.Trojan:
|
|
||||||
return rec.Password
|
|
||||||
case model.Shadowsocks:
|
|
||||||
return rec.Email
|
|
||||||
case model.Hysteria:
|
|
||||||
return rec.Auth
|
|
||||||
default:
|
|
||||||
return rec.UUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientService struct{}
|
type ClientService struct{}
|
||||||
|
|
||||||
|
// ErrClientNotInInbound is returned (wrapped) when a client cannot be located
|
||||||
|
// in an inbound's settings during deletion. Deletion treats it as non-fatal so
|
||||||
|
// the operation stays idempotent and tolerant of pre-existing data drift
|
||||||
|
// between the clients table and the inbound's settings JSON.
|
||||||
|
var ErrClientNotInInbound = errors.New("client not found in inbound")
|
||||||
|
|
||||||
// Short-lived tombstone of just-deleted client emails so that a node snapshot
|
// Short-lived tombstone of just-deleted client emails so that a node snapshot
|
||||||
// arriving between delete and node-side processing doesn't resurrect them.
|
// arriving between delete and node-side processing doesn't resurrect them.
|
||||||
var (
|
var (
|
||||||
@@ -836,8 +826,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
|||||||
}
|
}
|
||||||
return needRestart, getErr
|
return needRestart, getErr
|
||||||
}
|
}
|
||||||
oldKey := clientKeyForProtocol(inbound.Protocol, existing)
|
if existing.Email == "" {
|
||||||
if oldKey == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
|
if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
|
||||||
@@ -850,7 +839,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
|||||||
nr, upErr := s.UpdateInboundClient(inboundSvc, &model.Inbound{
|
nr, upErr := s.UpdateInboundClient(inboundSvc, &model.Inbound{
|
||||||
Id: ibId,
|
Id: ibId,
|
||||||
Settings: string(settingsPayload),
|
Settings: string(settingsPayload),
|
||||||
}, oldKey)
|
}, existing.Email)
|
||||||
if upErr != nil {
|
if upErr != nil {
|
||||||
return needRestart, upErr
|
return needRestart, upErr
|
||||||
}
|
}
|
||||||
@@ -893,19 +882,27 @@ func (s *ClientService) Delete(inboundSvc *InboundService, id int, keepTraffic b
|
|||||||
|
|
||||||
needRestart := false
|
needRestart := false
|
||||||
for _, ibId := range inboundIds {
|
for _, ibId := range inboundIds {
|
||||||
inbound, getErr := inboundSvc.GetInbound(ibId)
|
if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
|
||||||
if getErr != nil {
|
|
||||||
if errors.Is(getErr, gorm.ErrRecordNotFound) {
|
if errors.Is(getErr, gorm.ErrRecordNotFound) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return needRestart, getErr
|
return needRestart, getErr
|
||||||
}
|
}
|
||||||
key := clientKeyForProtocol(inbound.Protocol, existing)
|
|
||||||
if key == "" {
|
// Always delete by email — the client's stable identity. This removes
|
||||||
|
// every matching entry from the inbound's settings even when the stored
|
||||||
|
// credential (UUID/password/auth) drifted from the inbound JSON, or a
|
||||||
|
// duplicate entry with the same email exists.
|
||||||
|
if existing.Email == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nr, delErr := s.DelInboundClient(inboundSvc, ibId, key, false)
|
nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, false)
|
||||||
if delErr != nil {
|
if delErr != nil {
|
||||||
|
// The client is already absent from this inbound (data drift or a
|
||||||
|
// retried delete). Skip it — deletion stays idempotent.
|
||||||
|
if errors.Is(delErr, ErrClientNotInInbound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return needRestart, delErr
|
return needRestart, delErr
|
||||||
}
|
}
|
||||||
if nr {
|
if nr {
|
||||||
@@ -1220,8 +1217,8 @@ func (s *ClientService) BulkDetach(inboundSvc *InboundService, emails []string,
|
|||||||
// delInboundClients removes several clients from a single inbound in one pass:
|
// delInboundClients removes several clients from a single inbound in one pass:
|
||||||
// one settings rewrite, one runtime sweep, one Save and one SyncInbound for the
|
// one settings rewrite, one runtime sweep, one Save and one SyncInbound for the
|
||||||
// whole batch, instead of repeating the full per-client cycle. It mirrors the
|
// whole batch, instead of repeating the full per-client cycle. It mirrors the
|
||||||
// semantics of DelInboundClient for each removed client. needRestart is the OR
|
// semantics of DelInboundClientByEmail for each removed client. needRestart is
|
||||||
// across all removals.
|
// the OR across all removals.
|
||||||
func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId int, recs []*model.ClientRecord, keepTraffic bool) (bool, error) {
|
func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId int, recs []*model.ClientRecord, keepTraffic bool) (bool, error) {
|
||||||
if len(recs) == 0 {
|
if len(recs) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -1239,20 +1236,12 @@ func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientKey := "id"
|
// Match by email — the client's stable identity (see Delete). Removes every
|
||||||
switch oldInbound.Protocol {
|
// entry carrying a wanted email, independent of credential drift.
|
||||||
case "trojan":
|
|
||||||
clientKey = "password"
|
|
||||||
case "shadowsocks":
|
|
||||||
clientKey = "email"
|
|
||||||
case "hysteria":
|
|
||||||
clientKey = "auth"
|
|
||||||
}
|
|
||||||
|
|
||||||
wanted := make(map[string]struct{}, len(recs))
|
wanted := make(map[string]struct{}, len(recs))
|
||||||
for _, rec := range recs {
|
for _, rec := range recs {
|
||||||
if k := clientKeyForProtocol(oldInbound.Protocol, rec); k != "" {
|
if rec.Email != "" {
|
||||||
wanted[k] = struct{}{}
|
wanted[rec.Email] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,9 +1262,8 @@ func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId
|
|||||||
newClients = append(newClients, client)
|
newClients = append(newClients, client)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cid, _ := c[clientKey].(string)
|
email, _ := c["email"].(string)
|
||||||
if _, hit := wanted[cid]; hit && cid != "" {
|
if _, hit := wanted[email]; hit && email != "" {
|
||||||
email, _ := c["email"].(string)
|
|
||||||
enable, _ := c["enable"].(bool)
|
enable, _ := c["enable"].(bool)
|
||||||
removed = append(removed, removedClient{email: email, needApiDel: enable})
|
removed = append(removed, removedClient{email: email, needApiDel: enable})
|
||||||
continue
|
continue
|
||||||
@@ -1417,6 +1405,9 @@ func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string,
|
|||||||
for _, ibId := range inboundIds {
|
for _, ibId := range inboundIds {
|
||||||
nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email, false)
|
nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email, false)
|
||||||
if delErr != nil {
|
if delErr != nil {
|
||||||
|
if errors.Is(delErr, ErrClientNotInInbound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return needRestart, delErr
|
return needRestart, delErr
|
||||||
}
|
}
|
||||||
if nr {
|
if nr {
|
||||||
@@ -2720,29 +2711,15 @@ func (s *ClientService) bulkAdjustInboundClients(
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
clientKey := "id"
|
// Match by email — the client's stable identity (see Delete). Credentials
|
||||||
switch oldInbound.Protocol {
|
// can drift from the inbound JSON, so they are never used for matching.
|
||||||
case model.Trojan:
|
wantedEmails := make(map[string]struct{}, len(emails))
|
||||||
clientKey = "password"
|
|
||||||
case model.Shadowsocks:
|
|
||||||
clientKey = "email"
|
|
||||||
case model.Hysteria:
|
|
||||||
clientKey = "auth"
|
|
||||||
}
|
|
||||||
|
|
||||||
keyToEmail := make(map[string]string, len(emails))
|
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
entry := plan[email]
|
if plan[email] == nil {
|
||||||
if entry == nil {
|
|
||||||
res.perEmailSkipped[email] = "client not found"
|
res.perEmailSkipped[email] = "client not found"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := clientKeyForProtocol(oldInbound.Protocol, entry.record)
|
wantedEmails[email] = struct{}{}
|
||||||
if key == "" {
|
|
||||||
res.perEmailSkipped[email] = "missing client key for protocol"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keyToEmail[key] = email
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaceClients, _ := settings["clients"].([]any)
|
interfaceClients, _ := settings["clients"].([]any)
|
||||||
@@ -2753,9 +2730,8 @@ func (s *ClientService) bulkAdjustInboundClients(
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cKey, _ := c[clientKey].(string)
|
targetEmail, _ := c["email"].(string)
|
||||||
targetEmail, found := keyToEmail[cKey]
|
if _, want := wantedEmails[targetEmail]; !want || targetEmail == "" {
|
||||||
if !found {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entry := plan[targetEmail]
|
entry := plan[targetEmail]
|
||||||
@@ -2770,7 +2746,7 @@ func (s *ClientService) bulkAdjustInboundClients(
|
|||||||
foundEmails[targetEmail] = true
|
foundEmails[targetEmail] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, email := range keyToEmail {
|
for email := range wantedEmails {
|
||||||
if !foundEmails[email] {
|
if !foundEmails[email] {
|
||||||
res.perEmailSkipped[email] = "Client Not Found In Inbound"
|
res.perEmailSkipped[email] = "Client Not Found In Inbound"
|
||||||
}
|
}
|
||||||
@@ -3031,29 +3007,15 @@ func (s *ClientService) bulkDelInboundClients(
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
clientKey := "id"
|
// Match by email — the client's stable identity (see Delete). Removes every
|
||||||
switch oldInbound.Protocol {
|
// entry carrying a wanted email, independent of credential drift.
|
||||||
case model.Trojan:
|
wantedEmails := make(map[string]struct{}, len(emails))
|
||||||
clientKey = "password"
|
|
||||||
case model.Shadowsocks:
|
|
||||||
clientKey = "email"
|
|
||||||
case model.Hysteria:
|
|
||||||
clientKey = "auth"
|
|
||||||
}
|
|
||||||
|
|
||||||
keyToEmail := make(map[string]string, len(emails))
|
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
rec := records[email]
|
if records[email] == nil {
|
||||||
if rec == nil {
|
|
||||||
res.perEmailSkipped[email] = "client not found"
|
res.perEmailSkipped[email] = "client not found"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := clientKeyForProtocol(oldInbound.Protocol, rec)
|
wantedEmails[email] = struct{}{}
|
||||||
if key == "" {
|
|
||||||
res.perEmailSkipped[email] = "missing client key for protocol"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keyToEmail[key] = email
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaceClients, _ := settings["clients"].([]any)
|
interfaceClients, _ := settings["clients"].([]any)
|
||||||
@@ -3066,19 +3028,17 @@ func (s *ClientService) bulkDelInboundClients(
|
|||||||
newClients = append(newClients, client)
|
newClients = append(newClients, client)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cKey, _ := c[clientKey].(string)
|
em, _ := c["email"].(string)
|
||||||
if targetEmail, found := keyToEmail[cKey]; found {
|
if _, found := wantedEmails[em]; found && em != "" {
|
||||||
foundEmails[targetEmail] = true
|
foundEmails[em] = true
|
||||||
if em, _ := c["email"].(string); em != "" {
|
en, _ := c["enable"].(bool)
|
||||||
en, _ := c["enable"].(bool)
|
enableByEmail[em] = en
|
||||||
enableByEmail[em] = en
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newClients = append(newClients, client)
|
newClients = append(newClients, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, email := range keyToEmail {
|
for email := range wantedEmails {
|
||||||
if !foundEmails[email] {
|
if !foundEmails[email] {
|
||||||
res.perEmailSkipped[email] = "Client Not Found In Inbound"
|
res.perEmailSkipped[email] = "Client Not Found In Inbound"
|
||||||
}
|
}
|
||||||
@@ -3547,16 +3507,18 @@ func (s *ClientService) Detach(inboundSvc *InboundService, id int, inboundIds []
|
|||||||
if _, attached := have[ibId]; !attached {
|
if _, attached := have[ibId]; !attached {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
inbound, getErr := inboundSvc.GetInbound(ibId)
|
if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
|
||||||
if getErr != nil {
|
|
||||||
return needRestart, getErr
|
return needRestart, getErr
|
||||||
}
|
}
|
||||||
key := clientKeyForProtocol(inbound.Protocol, existing)
|
// Detach by email — the client's stable identity (see Delete).
|
||||||
if key == "" {
|
if existing.Email == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nr, delErr := s.DelInboundClient(inboundSvc, ibId, key, true)
|
nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, true)
|
||||||
if delErr != nil {
|
if delErr != nil {
|
||||||
|
if errors.Is(delErr, ErrClientNotInInbound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return needRestart, delErr
|
return needRestart, delErr
|
||||||
}
|
}
|
||||||
if nr {
|
if nr {
|
||||||
@@ -3782,7 +3744,7 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model
|
|||||||
return needRestart, nil
|
return needRestart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *model.Inbound, clientId string) (bool, error) {
|
func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *model.Inbound, oldEmail string) (bool, error) {
|
||||||
defer lockInbound(data.Id).Unlock()
|
defer lockInbound(data.Id).Unlock()
|
||||||
|
|
||||||
clients, err := inboundSvc.GetClients(data)
|
clients, err := inboundSvc.GetClients(data)
|
||||||
@@ -3808,56 +3770,30 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
oldEmail := ""
|
|
||||||
newClientId := ""
|
newClientId := ""
|
||||||
|
switch oldInbound.Protocol {
|
||||||
|
case "trojan":
|
||||||
|
newClientId = clients[0].Password
|
||||||
|
case "shadowsocks":
|
||||||
|
newClientId = clients[0].Email
|
||||||
|
case "hysteria":
|
||||||
|
newClientId = clients[0].Auth
|
||||||
|
default:
|
||||||
|
newClientId = clients[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate the client to replace by email — the client's stable identity.
|
||||||
|
// Credentials (uuid/password/auth) can drift from the inbound JSON, so they
|
||||||
|
// are never used for matching.
|
||||||
clientIndex := -1
|
clientIndex := -1
|
||||||
for index, oldClient := range oldClients {
|
for index, oldClient := range oldClients {
|
||||||
oldClientId := ""
|
if strings.EqualFold(oldClient.Email, oldEmail) {
|
||||||
switch oldInbound.Protocol {
|
|
||||||
case "trojan":
|
|
||||||
oldClientId = oldClient.Password
|
|
||||||
newClientId = clients[0].Password
|
|
||||||
case "shadowsocks":
|
|
||||||
oldClientId = oldClient.Email
|
|
||||||
newClientId = clients[0].Email
|
|
||||||
case "hysteria":
|
|
||||||
oldClientId = oldClient.Auth
|
|
||||||
newClientId = clients[0].Auth
|
|
||||||
default:
|
|
||||||
oldClientId = oldClient.ID
|
|
||||||
newClientId = clients[0].ID
|
|
||||||
}
|
|
||||||
if clientId == oldClientId {
|
|
||||||
oldEmail = oldClient.Email
|
oldEmail = oldClient.Email
|
||||||
clientIndex = index
|
clientIndex = index
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if clientIndex == -1 {
|
|
||||||
var rec model.ClientRecord
|
|
||||||
var lookupErr error
|
|
||||||
switch oldInbound.Protocol {
|
|
||||||
case "trojan":
|
|
||||||
lookupErr = database.GetDB().Where("password = ?", clientId).First(&rec).Error
|
|
||||||
case "shadowsocks":
|
|
||||||
lookupErr = database.GetDB().Where("email = ?", clientId).First(&rec).Error
|
|
||||||
case "hysteria":
|
|
||||||
lookupErr = database.GetDB().Where("auth = ?", clientId).First(&rec).Error
|
|
||||||
default:
|
|
||||||
lookupErr = database.GetDB().Where("uuid = ?", clientId).First(&rec).Error
|
|
||||||
}
|
|
||||||
if lookupErr == nil && rec.Email != "" {
|
|
||||||
for index, oldClient := range oldClients {
|
|
||||||
if oldClient.Email == rec.Email {
|
|
||||||
oldEmail = oldClient.Email
|
|
||||||
clientIndex = index
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newClientId == "" || clientIndex == -1 {
|
if newClientId == "" || clientIndex == -1 {
|
||||||
return false, common.NewError("empty client ID")
|
return false, common.NewError("empty client ID")
|
||||||
}
|
}
|
||||||
@@ -4080,145 +4016,6 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
|||||||
return needRestart, nil
|
return needRestart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId int, clientId string, keepTraffic bool) (bool, error) {
|
|
||||||
defer lockInbound(inboundId).Unlock()
|
|
||||||
|
|
||||||
oldInbound, err := inboundSvc.GetInbound(inboundId)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Load Old Data Error")
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
var settings map[string]any
|
|
||||||
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
email := ""
|
|
||||||
client_key := "id"
|
|
||||||
switch oldInbound.Protocol {
|
|
||||||
case "trojan":
|
|
||||||
client_key = "password"
|
|
||||||
case "shadowsocks":
|
|
||||||
client_key = "email"
|
|
||||||
case "hysteria":
|
|
||||||
client_key = "auth"
|
|
||||||
}
|
|
||||||
|
|
||||||
interfaceClients := settings["clients"].([]any)
|
|
||||||
var newClients []any
|
|
||||||
needApiDel := false
|
|
||||||
clientFound := false
|
|
||||||
for _, client := range interfaceClients {
|
|
||||||
c := client.(map[string]any)
|
|
||||||
c_id := c[client_key].(string)
|
|
||||||
if c_id == clientId {
|
|
||||||
clientFound = true
|
|
||||||
email, _ = c["email"].(string)
|
|
||||||
needApiDel, _ = c["enable"].(bool)
|
|
||||||
} else {
|
|
||||||
newClients = append(newClients, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !clientFound {
|
|
||||||
return false, common.NewError("Client Not Found In Inbound For ID:", clientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
db := database.GetDB()
|
|
||||||
newClients = compactOrphans(db, newClients)
|
|
||||||
if newClients == nil {
|
|
||||||
newClients = []any{}
|
|
||||||
}
|
|
||||||
settings["clients"] = newClients
|
|
||||||
newSettings, err := json.MarshalIndent(settings, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound.Settings = string(newSettings)
|
|
||||||
|
|
||||||
emailShared, err := inboundSvc.emailUsedByOtherInbounds(email, inboundId)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !emailShared && !keepTraffic {
|
|
||||||
err = inboundSvc.DelClientIPs(db, email)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Error in delete client IPs")
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
needRestart := false
|
|
||||||
markDirty := false
|
|
||||||
|
|
||||||
if len(email) > 0 {
|
|
||||||
var enables []bool
|
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Limit(1).Pluck("enable", &enables).Error
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Get stats error")
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
notDepleted := len(enables) > 0 && enables[0]
|
|
||||||
if !emailShared && !keepTraffic {
|
|
||||||
err = inboundSvc.DelClientStat(db, email)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Delete stats Data Error")
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if needApiDel && notDepleted && oldInbound.NodeID == nil {
|
|
||||||
rt, rterr := inboundSvc.runtimeFor(oldInbound)
|
|
||||||
if rterr != nil {
|
|
||||||
needRestart = true
|
|
||||||
} else {
|
|
||||||
err1 := rt.RemoveUser(context.Background(), oldInbound, email)
|
|
||||||
if err1 == nil {
|
|
||||||
logger.Debug("Client deleted on", rt.Name(), ":", email)
|
|
||||||
needRestart = false
|
|
||||||
} else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
|
|
||||||
logger.Debug("User is already deleted. Nothing to do more...")
|
|
||||||
} else {
|
|
||||||
logger.Debug("Error in deleting client on", rt.Name(), ":", err1)
|
|
||||||
needRestart = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if oldInbound.NodeID != nil && len(email) > 0 {
|
|
||||||
rt, push, dirty, perr := inboundSvc.nodePushPlan(oldInbound)
|
|
||||||
if perr != nil {
|
|
||||||
return false, perr
|
|
||||||
}
|
|
||||||
if dirty {
|
|
||||||
markDirty = true
|
|
||||||
}
|
|
||||||
if push {
|
|
||||||
if err1 := rt.DeleteUser(context.Background(), oldInbound, email); err1 != nil {
|
|
||||||
logger.Warning("Error in deleting client on", rt.Name(), ":", err1)
|
|
||||||
markDirty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := db.Save(oldInbound).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
finalClients, gcErr := inboundSvc.GetClients(oldInbound)
|
|
||||||
if gcErr != nil {
|
|
||||||
return false, gcErr
|
|
||||||
}
|
|
||||||
if err := s.SyncInbound(db, inboundId, finalClients); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if markDirty && oldInbound.NodeID != nil {
|
|
||||||
if dErr := (&NodeService{}).MarkNodeDirty(*oldInbound.NodeID); dErr != nil {
|
|
||||||
logger.Warning("mark node dirty failed:", dErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return needRestart, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string, keepTraffic bool) (bool, error) {
|
func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string, keepTraffic bool) (bool, error) {
|
||||||
defer lockInbound(inboundId).Unlock()
|
defer lockInbound(inboundId).Unlock()
|
||||||
|
|
||||||
@@ -4256,7 +4053,7 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return false, common.NewError(fmt.Sprintf("client with email %s not found", email))
|
return false, fmt.Errorf("%w for email: %s", ErrClientNotInInbound, email)
|
||||||
}
|
}
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
newClients = compactOrphans(db, newClients)
|
newClients = compactOrphans(db, newClients)
|
||||||
@@ -4363,23 +4160,15 @@ func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, traf
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId := ""
|
found := false
|
||||||
|
|
||||||
for _, oldClient := range oldClients {
|
for _, oldClient := range oldClients {
|
||||||
if oldClient.Email == clientEmail {
|
if oldClient.Email == clientEmail {
|
||||||
switch inbound.Protocol {
|
found = true
|
||||||
case "trojan":
|
|
||||||
clientId = oldClient.Password
|
|
||||||
case "shadowsocks":
|
|
||||||
clientId = oldClient.Email
|
|
||||||
default:
|
|
||||||
clientId = oldClient.ID
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(clientId) == 0 {
|
if !found {
|
||||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4404,7 +4193,7 @@ func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, traf
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
inbound.Settings = string(modifiedSettings)
|
inbound.Settings = string(modifiedSettings)
|
||||||
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
|
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4448,25 +4237,18 @@ func (s *ClientService) ToggleClientEnableByEmail(inboundSvc *InboundService, cl
|
|||||||
return false, false, err
|
return false, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId := ""
|
found := false
|
||||||
clientOldEnabled := false
|
clientOldEnabled := false
|
||||||
|
|
||||||
for _, oldClient := range oldClients {
|
for _, oldClient := range oldClients {
|
||||||
if oldClient.Email == clientEmail {
|
if oldClient.Email == clientEmail {
|
||||||
switch inbound.Protocol {
|
found = true
|
||||||
case "trojan":
|
|
||||||
clientId = oldClient.Password
|
|
||||||
case "shadowsocks":
|
|
||||||
clientId = oldClient.Email
|
|
||||||
default:
|
|
||||||
clientId = oldClient.ID
|
|
||||||
}
|
|
||||||
clientOldEnabled = oldClient.Enable
|
clientOldEnabled = oldClient.Enable
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(clientId) == 0 {
|
if !found {
|
||||||
return false, false, common.NewError("Client Not Found For Email:", clientEmail)
|
return false, false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4492,7 +4274,7 @@ func (s *ClientService) ToggleClientEnableByEmail(inboundSvc *InboundService, cl
|
|||||||
}
|
}
|
||||||
inbound.Settings = string(modifiedSettings)
|
inbound.Settings = string(modifiedSettings)
|
||||||
|
|
||||||
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
|
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, needRestart, err
|
return false, needRestart, err
|
||||||
}
|
}
|
||||||
@@ -4529,23 +4311,15 @@ func (s *ClientService) ResetClientIpLimitByEmail(inboundSvc *InboundService, cl
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId := ""
|
found := false
|
||||||
|
|
||||||
for _, oldClient := range oldClients {
|
for _, oldClient := range oldClients {
|
||||||
if oldClient.Email == clientEmail {
|
if oldClient.Email == clientEmail {
|
||||||
switch inbound.Protocol {
|
found = true
|
||||||
case "trojan":
|
|
||||||
clientId = oldClient.Password
|
|
||||||
case "shadowsocks":
|
|
||||||
clientId = oldClient.Email
|
|
||||||
default:
|
|
||||||
clientId = oldClient.ID
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(clientId) == 0 {
|
if !found {
|
||||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4570,7 +4344,7 @@ func (s *ClientService) ResetClientIpLimitByEmail(inboundSvc *InboundService, cl
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
inbound.Settings = string(modifiedSettings)
|
inbound.Settings = string(modifiedSettings)
|
||||||
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
|
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4588,23 +4362,15 @@ func (s *ClientService) ResetClientExpiryTimeByEmail(inboundSvc *InboundService,
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId := ""
|
found := false
|
||||||
|
|
||||||
for _, oldClient := range oldClients {
|
for _, oldClient := range oldClients {
|
||||||
if oldClient.Email == clientEmail {
|
if oldClient.Email == clientEmail {
|
||||||
switch inbound.Protocol {
|
found = true
|
||||||
case "trojan":
|
|
||||||
clientId = oldClient.Password
|
|
||||||
case "shadowsocks":
|
|
||||||
clientId = oldClient.Email
|
|
||||||
default:
|
|
||||||
clientId = oldClient.ID
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(clientId) == 0 {
|
if !found {
|
||||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4629,7 +4395,7 @@ func (s *ClientService) ResetClientExpiryTimeByEmail(inboundSvc *InboundService,
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
inbound.Settings = string(modifiedSettings)
|
inbound.Settings = string(modifiedSettings)
|
||||||
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
|
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4650,23 +4416,15 @@ func (s *ClientService) ResetClientTrafficLimitByEmail(inboundSvc *InboundServic
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId := ""
|
found := false
|
||||||
|
|
||||||
for _, oldClient := range oldClients {
|
for _, oldClient := range oldClients {
|
||||||
if oldClient.Email == clientEmail {
|
if oldClient.Email == clientEmail {
|
||||||
switch inbound.Protocol {
|
found = true
|
||||||
case "trojan":
|
|
||||||
clientId = oldClient.Password
|
|
||||||
case "shadowsocks":
|
|
||||||
clientId = oldClient.Email
|
|
||||||
default:
|
|
||||||
clientId = oldClient.ID
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(clientId) == 0 {
|
if !found {
|
||||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4691,6 +4449,6 @@ func (s *ClientService) ResetClientTrafficLimitByEmail(inboundSvc *InboundServic
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
inbound.Settings = string(modifiedSettings)
|
inbound.Settings = string(modifiedSettings)
|
||||||
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
|
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-19
@@ -1402,25 +1402,11 @@ func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inb
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) getClientPrimaryKey(protocol model.Protocol, client model.Client) string {
|
func (s *InboundService) writeBackClientSubID(sourceInboundID int, client model.Client, subID string) (bool, error) {
|
||||||
switch protocol {
|
|
||||||
case model.Trojan:
|
|
||||||
return client.Password
|
|
||||||
case model.Shadowsocks:
|
|
||||||
return client.Email
|
|
||||||
case model.Hysteria:
|
|
||||||
return client.Auth
|
|
||||||
default:
|
|
||||||
return client.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtocol model.Protocol, client model.Client, subID string) (bool, error) {
|
|
||||||
client.SubID = subID
|
client.SubID = subID
|
||||||
client.UpdatedAt = time.Now().UnixMilli()
|
client.UpdatedAt = time.Now().UnixMilli()
|
||||||
clientID := s.getClientPrimaryKey(sourceProtocol, client)
|
if client.Email == "" {
|
||||||
if clientID == "" {
|
return false, common.NewError("empty client email")
|
||||||
return false, common.NewError("empty client ID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsBytes, err := json.Marshal(map[string][]model.Client{
|
settingsBytes, err := json.Marshal(map[string][]model.Client{
|
||||||
@@ -1434,7 +1420,7 @@ func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtoco
|
|||||||
Id: sourceInboundID,
|
Id: sourceInboundID,
|
||||||
Settings: string(settingsBytes),
|
Settings: string(settingsBytes),
|
||||||
}
|
}
|
||||||
return s.clientService.UpdateInboundClient(s, updatePayload, clientID)
|
return s.clientService.UpdateInboundClient(s, updatePayload, client.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string {
|
func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string {
|
||||||
@@ -1554,7 +1540,7 @@ func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID
|
|||||||
|
|
||||||
if sourceClient.SubID == "" {
|
if sourceClient.SubID == "" {
|
||||||
newSubID := uuid.NewString()
|
newSubID := uuid.NewString()
|
||||||
subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceInbound.Protocol, sourceClient, newSubID)
|
subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceClient, newSubID)
|
||||||
if subErr != nil {
|
if subErr != nil {
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr))
|
result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr))
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -215,10 +215,10 @@ func TestAddDelClientPostgresScale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
addDur := time.Since(start)
|
addDur := time.Since(start)
|
||||||
|
|
||||||
delId := clients[n/2].ID
|
delEmail := clients[n/2].Email
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
if _, err := svc.DelInboundClient(inboundSvc, ib.Id, delId, false); err != nil {
|
if _, err := svc.DelInboundClientByEmail(inboundSvc, ib.Id, delEmail, false); err != nil {
|
||||||
t.Fatalf("DelInboundClient: %v", err)
|
t.Fatalf("DelInboundClientByEmail: %v", err)
|
||||||
}
|
}
|
||||||
delDur := time.Since(start)
|
delDur := time.Since(start)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user