fix(ldap): attach auto-created clients to every configured inbound tag

The sync job built an independent client per configured tag and called
CreateOne once per tag. Each call generated a fresh random subId, and
the email-uniqueness check in ClientService.Create only re-admits a
taken email when the incoming subId matches the stored one - so the
first tag succeeded and every other tag failed with "email already in
use", leaving new LDAP users on a single inbound.

Build the client once per email and hand ClientService.Create the full
list of resolved inbound ids, the same path the panel's own client
create endpoint uses: one identity (email, subId) attached to all
configured tags, with per-protocol credentials filled per inbound.
Unknown tags are now skipped with a warning instead of building
clients against a nil inbound.

Closes #5846
This commit is contained in:
MHSanaei
2026-07-07 12:39:00 +02:00
parent 7cb2adf429
commit 52d4af71bc
2 changed files with 142 additions and 48 deletions
+57 -48
View File
@@ -4,8 +4,6 @@ import (
"strings"
"time"
"github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
@@ -114,52 +112,41 @@ func (j *LdapSyncJob) Run() {
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
clientsToCreate := map[string][]model.Client{} // tag -> []new clients
clientsToEnable := map[string][]string{} // tag -> []email
clientsToDisable := map[string][]string{} // tag -> []email
for email, allowed := range flags {
exists := allClients[email] != nil
for _, tag := range inboundTags {
if !exists && allowed && autoCreate {
newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
} else if exists {
if allowed && !allClients[email].Enable {
clientsToEnable[tag] = append(clientsToEnable[tag], email)
} else if !allowed && allClients[email].Enable {
clientsToDisable[tag] = append(clientsToDisable[tag], email)
}
}
}
}
for tag, newClients := range clientsToCreate {
if len(newClients) == 0 {
resolvedInboundIds := make([]int, 0, len(inboundTags))
resolvedTags := make([]string, 0, len(inboundTags))
for _, tag := range inboundTags {
ib := inboundMap[tag]
if ib == nil {
logger.Warningf("LDAP inbound tag %s does not match any inbound", tag)
continue
}
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
resolvedInboundIds = append(resolvedInboundIds, ib.Id)
resolvedTags = append(resolvedTags, tag)
}
clientsToCreate := []model.Client{}
clientsToEnable := map[string][]string{} // tag -> []email
clientsToDisable := map[string][]string{} // tag -> []email
for email, allowed := range flags {
existing := allClients[email]
if existing == nil {
if allowed && autoCreate {
clientsToCreate = append(clientsToCreate, j.buildClient(email, defGB, defExpiryDays, defLimitIP))
}
continue
}
if created > 0 {
logger.Infof("LDAP auto-create: %d clients for %s", created, tag)
if restartNeeded {
j.xrayService.SetToNeedRestart()
for _, tag := range resolvedTags {
if allowed && !existing.Enable {
clientsToEnable[tag] = append(clientsToEnable[tag], email)
} else if !allowed && existing.Enable {
clientsToDisable[tag] = append(clientsToDisable[tag], email)
}
}
}
j.createClients(clientsToCreate, resolvedInboundIds, resolvedTags)
// --- Execute enable/disable batch ---
for tag, emails := range clientsToEnable {
j.batchSetEnable(inboundMap[tag], emails, true)
@@ -196,8 +183,8 @@ func splitCsv(s string) []string {
return out
}
// buildClient creates a new client for auto-create
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
// buildClient creates a new client for auto-create; ClientService.Create fills per-protocol credentials
func (j *LdapSyncJob) buildClient(email string, defGB, defExpiryDays, defLimitIP int) model.Client {
c := model.Client{
Email: email,
Enable: true,
@@ -207,15 +194,37 @@ func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExp
if defExpiryDays > 0 {
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
}
switch ib.Protocol {
case model.Trojan, model.Shadowsocks:
c.Password = uuid.NewString()
default:
c.ID = uuid.NewString()
}
return c
}
// createClients adds each new LDAP client once, attached to every configured inbound
func (j *LdapSyncJob) createClients(newClients []model.Client, inboundIds []int, tags []string) {
if len(newClients) == 0 || len(inboundIds) == 0 {
return
}
tagList := strings.Join(tags, ",")
created := 0
restartNeeded := false
for _, c := range newClients {
nr, err := j.clientService.Create(&j.inboundService, &service.ClientCreatePayload{Client: c, InboundIds: inboundIds})
if err != nil {
logger.Warningf("Failed to add client %s for tags %s: %v", c.Email, tagList, err)
continue
}
created++
if nr {
restartNeeded = true
}
}
if created == 0 {
return
}
logger.Infof("LDAP auto-create: %d clients for %s", created, tagList)
if restartNeeded {
j.xrayService.SetToNeedRestart()
}
}
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
if len(emails) == 0 {
return