diff --git a/web/job/ldap_sync_job.go b/web/job/ldap_sync_job.go index 142e9524..40045e56 100644 --- a/web/job/ldap_sync_job.go +++ b/web/job/ldap_sync_job.go @@ -1,18 +1,15 @@ package job import ( + "strings" "time" - "strings" + "github.com/google/uuid" "github.com/mhsanaei/3x-ui/v3/database/model" "github.com/mhsanaei/3x-ui/v3/logger" ldaputil "github.com/mhsanaei/3x-ui/v3/util/ldap" "github.com/mhsanaei/3x-ui/v3/web/service" - - "strconv" - - "github.com/google/uuid" ) var DefaultTruthyValues = []string{"true", "1", "yes", "on"} @@ -20,6 +17,7 @@ var DefaultTruthyValues = []string{"true", "1", "yes", "on"} type LdapSyncJob struct { settingService service.SettingService inboundService service.InboundService + clientService service.ClientService xrayService service.XrayService } @@ -135,18 +133,29 @@ func (j *LdapSyncJob) Run() { } } - // --- Execute batch create --- for tag, newClients := range clientsToCreate { if len(newClients) == 0 { continue } - payload := &model.Inbound{Id: inboundMap[tag].Id} - payload.Settings = j.clientsToJSON(newClients) - if _, err := j.inboundService.AddInboundClient(payload); err != nil { - logger.Warningf("Failed to add clients for tag %s: %v", tag, err) - } else { - logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag) - j.xrayService.SetToNeedRestart() + ib := inboundMap[tag] + created := 0 + restartNeeded := false + for _, c := range newClients { + nr, err := j.clientService.CreateOne(&j.inboundService, ib.Id, c) + if err != nil { + logger.Warningf("Failed to add client %s for tag %s: %v", c.Email, tag, err) + continue + } + created++ + if nr { + restartNeeded = true + } + } + if created > 0 { + logger.Infof("LDAP auto-create: %d clients for %s", created, tag) + if restartNeeded { + j.xrayService.SetToNeedRestart() + } } } @@ -206,34 +215,31 @@ func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExp return c } -// batchSetEnable enables/disables clients in batch through a single call func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) { if len(emails) == 0 { return } - - // Prepare JSON for mass update - clients := make([]model.Client, 0, len(emails)) + restartNeeded := false + changed := 0 for _, email := range emails { - clients = append(clients, model.Client{ - Email: email, - Enable: enable, - }) + ok, needRestart, err := j.inboundService.SetClientEnableByEmail(email, enable) + if err != nil { + logger.Warningf("Batch set enable failed for %s in inbound %s: %v", email, ib.Tag, err) + continue + } + if ok { + changed++ + } + if needRestart { + restartNeeded = true + } } - - payload := &model.Inbound{ - Id: ib.Id, - Settings: j.clientsToJSON(clients), + if changed > 0 { + logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, changed, ib.Tag) } - - // Use a single AddInboundClient call to update enable - if _, err := j.inboundService.AddInboundClient(payload); err != nil { - logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err) - return + if restartNeeded { + j.xrayService.SetToNeedRestart() } - - logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag) - j.xrayService.SetToNeedRestart() } // deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart @@ -269,90 +275,28 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s continue } - // Delete in batches for i := 0; i < len(toDelete); i += batchSize { end := min(i+batchSize, len(toDelete)) batch := toDelete[i:end] for _, c := range batch { - var clientKey string - switch ib.Protocol { - case model.Trojan: - clientKey = c.Password - case model.Shadowsocks: - clientKey = c.Email - default: // vless/vmess - clientKey = c.ID - } - - if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil { + nr, err := j.clientService.DetachByEmail(&j.inboundService, ib.Id, c.Email) + if err != nil { logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v", c.Email, ib.Id, ib.Tag, err) - } else { - logger.Infof("Deleted client %s from inbound id=%d(tag=%s)", - c.Email, ib.Id, ib.Tag) - // do not restart here + continue + } + logger.Infof("Deleted client %s from inbound id=%d(tag=%s)", + c.Email, ib.Id, ib.Tag) + if nr { restartNeeded = true } } } } - // One time after all batches if restartNeeded { j.xrayService.SetToNeedRestart() logger.Info("Xray restart scheduled after batch deletion") } } - -// clientsToJSON serializes an array of clients to JSON -func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string { - b := strings.Builder{} - b.WriteString("{\"clients\":[") - for i, c := range clients { - if i > 0 { - b.WriteString(",") - } - b.WriteString(j.clientToJSON(c)) - } - b.WriteString("]}") - return b.String() -} - -// clientToJSON serializes minimal client fields to JSON object string without extra deps -func (j *LdapSyncJob) clientToJSON(c model.Client) string { - // construct minimal JSON manually to avoid importing json for simple case - b := strings.Builder{} - b.WriteString("{") - if c.ID != "" { - b.WriteString("\"id\":\"") - b.WriteString(c.ID) - b.WriteString("\",") - } - if c.Password != "" { - b.WriteString("\"password\":\"") - b.WriteString(c.Password) - b.WriteString("\",") - } - b.WriteString("\"email\":\"") - b.WriteString(c.Email) - b.WriteString("\",") - b.WriteString("\"enable\":") - if c.Enable { - b.WriteString("true") - } else { - b.WriteString("false") - } - b.WriteString(",") - b.WriteString("\"limitIp\":") - b.WriteString(strconv.Itoa(c.LimitIP)) - b.WriteString(",") - b.WriteString("\"totalGB\":") - b.WriteString(strconv.FormatInt(c.TotalGB, 10)) - if c.ExpiryTime > 0 { - b.WriteString(",\"expiryTime\":") - b.WriteString(strconv.FormatInt(c.ExpiryTime, 10)) - } - b.WriteString("}") - return b.String() -} diff --git a/web/service/client.go b/web/service/client.go index c498d2a7..15372201 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -475,6 +475,24 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds [] return needRestart, nil } +func (s *ClientService) CreateOne(inboundSvc *InboundService, inboundId int, client model.Client) (bool, error) { + return s.Create(inboundSvc, &ClientCreatePayload{ + Client: client, + InboundIds: []int{inboundId}, + }) +} + +func (s *ClientService) DetachByEmail(inboundSvc *InboundService, inboundId int, email string) (bool, error) { + if email == "" { + return false, common.NewError("client email is required") + } + rec, err := s.GetRecordByEmail(nil, email) + if err != nil { + return false, err + } + return s.Detach(inboundSvc, rec.Id, []int{inboundId}) +} + func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email string) (bool, error) { if email == "" { return false, common.NewError("client email is required") diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 179c082f..0da1a1c2 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -118,6 +118,7 @@ type LoginAttempt struct { // It handles bot commands, user interactions, and status reporting via Telegram. type Tgbot struct { inboundService InboundService + clientService ClientService settingService SettingService serverService ServerService xrayService XrayService @@ -2209,27 +2210,43 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { return jsonString, nil } -// SubmitAddClient submits the client addition request to the inbound service. +// SubmitAddClient submits the client addition request to the client service. func (t *Tgbot) SubmitAddClient() (bool, error) { - inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { logger.Warning("getIboundClients run failed:", err) return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } - jsonString, err := t.BuildJSONForProtocol(inbound.Protocol) - if err != nil { - logger.Warning("BuildJSONForProtocol run failed:", err) - return false, errors.New("failed to build JSON for protocol") + tgIDInt, _ := strconv.ParseInt(client_TgID, 10, 64) + client := model.Client{ + Email: client_Email, + Enable: client_Enable, + LimitIP: client_LimitIP, + TotalGB: client_TotalGB, + ExpiryTime: client_ExpiryTime, + SubID: client_SubID, + Comment: client_Comment, + Reset: client_Reset, + TgID: tgIDInt, } - newInbound := &model.Inbound{ - Id: receiver_inbound_ID, - Settings: jsonString, + switch inbound.Protocol { + case model.VMESS: + client.ID = client_Id + client.Security = client_Security + case model.VLESS: + client.ID = client_Id + client.Flow = client_Flow + case model.Trojan: + client.Password = client_TrPassword + case model.Shadowsocks: + client.Password = client_ShPassword + default: + return false, errors.New("unknown protocol") } - return t.inboundService.AddInboundClient(newInbound) + return t.clientService.CreateOne(&t.inboundService, receiver_inbound_ID, client) } // checkAdmin checks if the given Telegram ID is an admin.