精简: 去掉 LDAP(目录登录+用户同步): util/ldap 包、同步 job、登录分支、全部 ldap* 设置
This commit is contained in:
@@ -36,7 +36,6 @@ type updateSettingForm struct {
|
||||
entity.AllSetting
|
||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||
ClearTgBotToken bool `json:"clearTgBotToken" form:"clearTgBotToken"`
|
||||
ClearLdapPassword bool `json:"clearLdapPassword" form:"clearLdapPassword"`
|
||||
ClearSmtpPassword bool `json:"clearSmtpPassword" form:"clearSmtpPassword"`
|
||||
}
|
||||
|
||||
@@ -145,7 +144,6 @@ func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
}
|
||||
err := a.settingService.UpdateAllSetting(allSetting, service.SecretClears{
|
||||
TgBotToken: form.ClearTgBotToken,
|
||||
LdapPassword: form.ClearLdapPassword,
|
||||
SmtpPassword: form.ClearSmtpPassword,
|
||||
})
|
||||
if err == nil && twoFactorErr == nil && !oldTwoFactor && allSetting.TwoFactorEnable {
|
||||
|
||||
@@ -68,66 +68,44 @@ type AllSetting struct {
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
||||
SubJsonAutoDetect bool `json:"subJsonAutoDetect" form:"subJsonAutoDetect"`
|
||||
SubJsonAlwaysArray bool `json:"subJsonAlwaysArray" form:"subJsonAlwaysArray"`
|
||||
SubJsonUserAgentRegex string `json:"subJsonUserAgentRegex" form:"subJsonUserAgentRegex"`
|
||||
SubClashAutoDetect bool `json:"subClashAutoDetect" form:"subClashAutoDetect"`
|
||||
SubClashUserAgentRegex string `json:"subClashUserAgentRegex" form:"subClashUserAgentRegex"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubSupportUrl string `json:"subSupportUrl" form:"subSupportUrl"`
|
||||
SubProfileUrl string `json:"subProfileUrl" form:"subProfileUrl"`
|
||||
SubAnnounce string `json:"subAnnounce" form:"subAnnounce"`
|
||||
SubEnableRouting bool `json:"subEnableRouting" form:"subEnableRouting"`
|
||||
SubRoutingRules string `json:"subRoutingRules" form:"subRoutingRules"`
|
||||
SubIncyEnableRouting bool `json:"subIncyEnableRouting" form:"subIncyEnableRouting"`
|
||||
SubIncyRoutingRules string `json:"subIncyRoutingRules" form:"subIncyRoutingRules"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort" validate:"gte=1,lte=65535"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates" validate:"gte=0,lte=525600"`
|
||||
RestartXrayOnClientDisable bool `json:"restartXrayOnClientDisable" form:"restartXrayOnClientDisable"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubURI string `json:"subURI" form:"subURI"`
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubClashEnable bool `json:"subClashEnable" form:"subClashEnable"`
|
||||
SubClashPath string `json:"subClashPath" form:"subClashPath"`
|
||||
SubClashURI string `json:"subClashURI" form:"subClashURI"`
|
||||
SubClashEnableRouting bool `json:"subClashEnableRouting" form:"subClashEnableRouting"`
|
||||
SubClashRules string `json:"subClashRules" form:"subClashRules"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
SubJsonFinalMask string `json:"subJsonFinalMask" form:"subJsonFinalMask"`
|
||||
SubJsonObservatory string `json:"subJsonObservatory" form:"subJsonObservatory"`
|
||||
SubThemeDir string `json:"subThemeDir" form:"subThemeDir"`
|
||||
SubHideSettings bool `json:"subHideSettings" form:"subHideSettings"`
|
||||
|
||||
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
|
||||
LdapHost string `json:"ldapHost" form:"ldapHost"`
|
||||
LdapPort int `json:"ldapPort" form:"ldapPort" validate:"gte=0,lte=65535"`
|
||||
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
|
||||
LdapInsecureSkipVerify bool `json:"ldapInsecureSkipVerify" form:"ldapInsecureSkipVerify"`
|
||||
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
|
||||
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
|
||||
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
|
||||
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
|
||||
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"`
|
||||
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
|
||||
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
|
||||
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
|
||||
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
|
||||
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
|
||||
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
|
||||
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
|
||||
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
|
||||
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB" validate:"gte=0"`
|
||||
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays" validate:"gte=0"`
|
||||
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP" validate:"gte=0"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
||||
SubJsonAutoDetect bool `json:"subJsonAutoDetect" form:"subJsonAutoDetect"`
|
||||
SubJsonAlwaysArray bool `json:"subJsonAlwaysArray" form:"subJsonAlwaysArray"`
|
||||
SubJsonUserAgentRegex string `json:"subJsonUserAgentRegex" form:"subJsonUserAgentRegex"`
|
||||
SubClashAutoDetect bool `json:"subClashAutoDetect" form:"subClashAutoDetect"`
|
||||
SubClashUserAgentRegex string `json:"subClashUserAgentRegex" form:"subClashUserAgentRegex"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubSupportUrl string `json:"subSupportUrl" form:"subSupportUrl"`
|
||||
SubProfileUrl string `json:"subProfileUrl" form:"subProfileUrl"`
|
||||
SubAnnounce string `json:"subAnnounce" form:"subAnnounce"`
|
||||
SubEnableRouting bool `json:"subEnableRouting" form:"subEnableRouting"`
|
||||
SubRoutingRules string `json:"subRoutingRules" form:"subRoutingRules"`
|
||||
SubIncyEnableRouting bool `json:"subIncyEnableRouting" form:"subIncyEnableRouting"`
|
||||
SubIncyRoutingRules string `json:"subIncyRoutingRules" form:"subIncyRoutingRules"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort" validate:"gte=1,lte=65535"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates" validate:"gte=0,lte=525600"`
|
||||
RestartXrayOnClientDisable bool `json:"restartXrayOnClientDisable" form:"restartXrayOnClientDisable"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubURI string `json:"subURI" form:"subURI"`
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubClashEnable bool `json:"subClashEnable" form:"subClashEnable"`
|
||||
SubClashPath string `json:"subClashPath" form:"subClashPath"`
|
||||
SubClashURI string `json:"subClashURI" form:"subClashURI"`
|
||||
SubClashEnableRouting bool `json:"subClashEnableRouting" form:"subClashEnableRouting"`
|
||||
SubClashRules string `json:"subClashRules" form:"subClashRules"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
SubJsonFinalMask string `json:"subJsonFinalMask" form:"subJsonFinalMask"`
|
||||
SubJsonObservatory string `json:"subJsonObservatory" form:"subJsonObservatory"`
|
||||
SubThemeDir string `json:"subThemeDir" form:"subThemeDir"`
|
||||
SubHideSettings bool `json:"subHideSettings" form:"subHideSettings"`
|
||||
|
||||
WarpUpdateInterval int `json:"warpUpdateInterval" form:"warpUpdateInterval" validate:"gte=0"`
|
||||
}
|
||||
@@ -137,7 +115,6 @@ type AllSettingView struct {
|
||||
|
||||
HasTgBotToken bool `json:"hasTgBotToken"`
|
||||
HasTwoFactorToken bool `json:"hasTwoFactorToken"`
|
||||
HasLdapPassword bool `json:"hasLdapPassword"`
|
||||
HasApiToken bool `json:"hasApiToken"`
|
||||
HasWarpSecret bool `json:"hasWarpSecret"`
|
||||
HasNordSecret bool `json:"hasNordSecret"`
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package job
|
||||
|
||||
import "testing"
|
||||
|
||||
// A bind that succeeds with an unusable search returns (empty, nil); the only
|
||||
// guard was `err != nil`, so that answer detached the entire inbound.
|
||||
func TestAutoDeleteAllowed(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
previous int64
|
||||
fetched int
|
||||
want bool
|
||||
}{
|
||||
{"empty fetch on a fresh job is refused", 0, 0, false},
|
||||
{"empty fetch after a healthy sync is refused", 500, 0, false},
|
||||
{"full fetch on a fresh job is allowed", 0, 500, true},
|
||||
{"steady fetch is allowed", 500, 500, true},
|
||||
{"growth is allowed", 500, 900, true},
|
||||
{"shrink above the retention floor is allowed", 500, 250, true},
|
||||
{"shrink below the retention floor is refused", 500, 249, false},
|
||||
{"collapse to a single user is refused", 500, 1, false},
|
||||
{"single user with no history is allowed", 0, 1, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
j := NewLdapSyncJob()
|
||||
j.lastFlagCount.Store(c.previous)
|
||||
if got := j.autoDeleteSafeForFetch(c.fetched); got != c.want {
|
||||
t.Fatalf("autoDeleteSafeForFetch(previous=%d, fetched=%d) = %v, want %v",
|
||||
c.previous, c.fetched, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
)
|
||||
|
||||
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||
|
||||
// Share of the previous successful fetch a new one must still return to be
|
||||
// trusted: a sudden collapse means a broken directory far more often than churn.
|
||||
const ldapAutoDeleteMinRetainPercent = 50
|
||||
|
||||
type LdapSyncJob struct {
|
||||
settingService service.SettingService
|
||||
inboundService service.InboundService
|
||||
clientService service.ClientService
|
||||
xrayService service.XrayService
|
||||
lastFlagCount atomic.Int64
|
||||
}
|
||||
|
||||
// --- Helper functions for mustGet ---
|
||||
func mustGetString(fn func() (string, error)) string {
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func mustGetInt(fn func() (int, error)) int {
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func mustGetBool(fn func() (bool, error)) bool {
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func mustGetStringOr(fn func() (string, error), fallback string) string {
|
||||
v, err := fn()
|
||||
if err != nil || v == "" {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func NewLdapSyncJob() *LdapSyncJob {
|
||||
return new(LdapSyncJob)
|
||||
}
|
||||
|
||||
func (j *LdapSyncJob) Run() {
|
||||
logger.Info("LDAP sync job started")
|
||||
|
||||
enabled, err := j.settingService.GetLdapEnable()
|
||||
if err != nil || !enabled {
|
||||
logger.Warning("LDAP disabled or failed to fetch flag")
|
||||
return
|
||||
}
|
||||
|
||||
// --- LDAP fetch ---
|
||||
cfg := ldaputil.Config{
|
||||
Host: mustGetString(j.settingService.GetLdapHost),
|
||||
Port: mustGetInt(j.settingService.GetLdapPort),
|
||||
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
|
||||
InsecureSkipVerify: mustGetBool(j.settingService.GetLdapInsecureSkipVerify),
|
||||
BindDN: mustGetString(j.settingService.GetLdapBindDN),
|
||||
Password: mustGetString(j.settingService.GetLdapPassword),
|
||||
BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
|
||||
UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
|
||||
UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
|
||||
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
|
||||
TruthyVals: truthyValuesOrDefault(mustGetString(j.settingService.GetLdapTruthyValues)),
|
||||
Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
|
||||
}
|
||||
|
||||
flags, err := ldaputil.FetchVlessFlags(cfg)
|
||||
if err != nil {
|
||||
logger.Warning("LDAP fetch failed:", err)
|
||||
return
|
||||
}
|
||||
logger.Infof("Fetched %d LDAP flags", len(flags))
|
||||
|
||||
// --- Load all inbounds and all clients once ---
|
||||
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
|
||||
inbounds, err := j.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get inbounds:", err)
|
||||
return
|
||||
}
|
||||
|
||||
allClients := map[string]*model.Client{} // email -> client
|
||||
inboundMap := map[string]*model.Inbound{} // tag -> inbound
|
||||
for _, ib := range inbounds {
|
||||
inboundMap[ib.Tag] = ib
|
||||
clients, _ := j.inboundService.GetClients(ib)
|
||||
for i := range clients {
|
||||
allClients[clients[i].Email] = &clients[i]
|
||||
}
|
||||
}
|
||||
|
||||
// --- Prepare batch operations ---
|
||||
autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
|
||||
defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
|
||||
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
|
||||
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
for tag, emails := range clientsToDisable {
|
||||
j.batchSetEnable(inboundMap[tag], emails, false)
|
||||
}
|
||||
|
||||
// --- Auto delete clients not in LDAP ---
|
||||
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
|
||||
if autoDelete && j.autoDeleteSafeForFetch(len(flags)) {
|
||||
ldapEmailSet := map[string]struct{}{}
|
||||
for e := range flags {
|
||||
ldapEmailSet[e] = struct{}{}
|
||||
}
|
||||
for _, tag := range inboundTags {
|
||||
j.deleteClientsNotInLDAP(tag, ldapEmailSet)
|
||||
}
|
||||
}
|
||||
j.lastFlagCount.Store(int64(len(flags)))
|
||||
}
|
||||
|
||||
// FetchVlessFlags returns (empty, nil) when the bind succeeds but the search
|
||||
// yields nothing — a renamed OU, a lost read grant — which is not "all gone".
|
||||
func (j *LdapSyncJob) autoDeleteSafeForFetch(fetched int) bool {
|
||||
if fetched == 0 {
|
||||
logger.Warning("LDAP auto-delete skipped: directory returned no usable users")
|
||||
return false
|
||||
}
|
||||
previous := j.lastFlagCount.Load()
|
||||
if previous > 0 && int64(fetched)*100 < previous*ldapAutoDeleteMinRetainPercent {
|
||||
logger.Warningf("LDAP auto-delete skipped: fetched %d users, previous successful sync saw %d (below %d%% retention)",
|
||||
fetched, previous, ldapAutoDeleteMinRetainPercent)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func truthyValuesOrDefault(s string) []string {
|
||||
if vals := splitCsv(s); len(vals) > 0 {
|
||||
return vals
|
||||
}
|
||||
return DefaultTruthyValues
|
||||
}
|
||||
|
||||
func splitCsv(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
v := strings.TrimSpace(p)
|
||||
if v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 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,
|
||||
LimitIP: defLimitIP,
|
||||
TotalGB: int64(defGB) * 1024 * 1024 * 1024,
|
||||
}
|
||||
if defExpiryDays > 0 {
|
||||
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
||||
}
|
||||
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
|
||||
}
|
||||
restartNeeded := false
|
||||
changed := 0
|
||||
for _, email := range emails {
|
||||
ok, needRestart, err := j.clientService.SetClientEnableByEmail(&j.inboundService, 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
|
||||
}
|
||||
}
|
||||
if changed > 0 {
|
||||
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, changed, ib.Tag)
|
||||
}
|
||||
if restartNeeded {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
||||
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
||||
inbounds, err := j.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get inbounds for deletion:", err)
|
||||
return
|
||||
}
|
||||
|
||||
batchSize := 50 // clients in 1 batch
|
||||
restartNeeded := false
|
||||
|
||||
for _, ib := range inbounds {
|
||||
if ib.Tag != inboundTag {
|
||||
continue
|
||||
}
|
||||
clients, err := j.inboundService.GetClients(ib)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect clients for deletion
|
||||
toDelete := []model.Client{}
|
||||
for _, c := range clients {
|
||||
if _, ok := ldapEmails[c.Email]; !ok {
|
||||
toDelete = append(toDelete, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toDelete) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < len(toDelete); i += batchSize {
|
||||
end := min(i+batchSize, len(toDelete))
|
||||
batch := toDelete[i:end]
|
||||
|
||||
for _, c := range batch {
|
||||
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)
|
||||
continue
|
||||
}
|
||||
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
||||
c.Email, ib.Id, ib.Tag)
|
||||
if nr {
|
||||
restartNeeded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if restartNeeded {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
logger.Info("Xray restart scheduled after batch deletion")
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
)
|
||||
|
||||
func initLdapJobDB(t *testing.T) {
|
||||
t.Helper()
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
}
|
||||
|
||||
func TestBuildClient_ConvertsDefaultTotalGBToBytes(t *testing.T) {
|
||||
j := NewLdapSyncJob()
|
||||
c := j.buildClient("user@example.com", 10, 0, 0)
|
||||
if want := int64(10) * 1024 * 1024 * 1024; c.TotalGB != want {
|
||||
t.Errorf("TotalGB = %d, want %d", c.TotalGB, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLdapCreateClients_AttachesToAllConfiguredInbounds(t *testing.T) {
|
||||
initLdapJobDB(t)
|
||||
db := database.GetDB()
|
||||
|
||||
tags := []string{"in-1080-tcp", "in-1081-tcp", "in-1082-tcp"}
|
||||
protocols := []model.Protocol{model.VLESS, model.Trojan, model.VLESS}
|
||||
inboundIds := make([]int, 0, len(tags))
|
||||
for i, tag := range tags {
|
||||
ib := &model.Inbound{
|
||||
UserId: 1,
|
||||
Tag: tag,
|
||||
Enable: true,
|
||||
Port: 42080 + i,
|
||||
Protocol: protocols[i],
|
||||
Settings: `{"clients": []}`,
|
||||
StreamSettings: `{"network":"tcp","security":"none"}`,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound %s: %v", tag, err)
|
||||
}
|
||||
inboundIds = append(inboundIds, ib.Id)
|
||||
}
|
||||
|
||||
j := NewLdapSyncJob()
|
||||
const email = "user@example.com"
|
||||
j.createClients([]model.Client{j.buildClient(email, 0, 0, 0)}, inboundIds, tags)
|
||||
|
||||
rec := &model.ClientRecord{}
|
||||
if err := db.Where("email = ?", email).First(rec).Error; err != nil {
|
||||
t.Fatalf("client record for %s not created: %v", email, err)
|
||||
}
|
||||
if rec.SubID == "" {
|
||||
t.Error("created LDAP client must carry a subId")
|
||||
}
|
||||
|
||||
clientSvc := &service.ClientService{}
|
||||
for i, id := range inboundIds {
|
||||
clients, err := clientSvc.ListForInbound(nil, id)
|
||||
if err != nil {
|
||||
t.Fatalf("ListForInbound(%s): %v", tags[i], err)
|
||||
}
|
||||
if len(clients) != 1 || clients[0].Email != email {
|
||||
t.Fatalf("inbound %s must carry exactly the LDAP client, got %d clients", tags[i], len(clients))
|
||||
}
|
||||
if clients[0].SubID != rec.SubID {
|
||||
t.Errorf("inbound %s client subId = %q, want the shared %q", tags[i], clients[0].SubID, rec.SubID)
|
||||
}
|
||||
}
|
||||
|
||||
trojanClients, err := clientSvc.ListForInbound(nil, inboundIds[1])
|
||||
if err != nil {
|
||||
t.Fatalf("ListForInbound(trojan): %v", err)
|
||||
}
|
||||
if trojanClients[0].Password == "" {
|
||||
t.Error("trojan inbound client must get a generated password")
|
||||
}
|
||||
vlessClients, err := clientSvc.ListForInbound(nil, inboundIds[0])
|
||||
if err != nil {
|
||||
t.Fatalf("ListForInbound(vless): %v", err)
|
||||
}
|
||||
if vlessClients[0].ID == "" {
|
||||
t.Error("vless inbound client must get a generated uuid")
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
|
||||
ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
)
|
||||
|
||||
@@ -52,36 +51,7 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
||||
}
|
||||
|
||||
if !crypto.CheckPasswordHash(user.Password, password) {
|
||||
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
||||
if !ldapEnabled {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
host, _ := s.settingService.GetLdapHost()
|
||||
port, _ := s.settingService.GetLdapPort()
|
||||
useTLS, _ := s.settingService.GetLdapUseTLS()
|
||||
skipVerify, _ := s.settingService.GetLdapInsecureSkipVerify()
|
||||
bindDN, _ := s.settingService.GetLdapBindDN()
|
||||
ldapPass, _ := s.settingService.GetLdapPassword()
|
||||
baseDN, _ := s.settingService.GetLdapBaseDN()
|
||||
userFilter, _ := s.settingService.GetLdapUserFilter()
|
||||
userAttr, _ := s.settingService.GetLdapUserAttr()
|
||||
|
||||
cfg := ldaputil.Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
UseTLS: useTLS,
|
||||
InsecureSkipVerify: skipVerify,
|
||||
BindDN: bindDN,
|
||||
Password: ldapPass,
|
||||
BaseDN: baseDN,
|
||||
UserFilter: userFilter,
|
||||
UserAttr: userAttr,
|
||||
}
|
||||
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
||||
if err != nil || !ok {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
||||
|
||||
+75
-194
@@ -55,103 +55,80 @@ var defaultValueMap = map[string]string{
|
||||
// cert are minted lazily on first use, and the node-side trust CA is pasted
|
||||
// in by the operator. Kept out of entity.AllSetting so private keys never
|
||||
// reach the settings UI/export.
|
||||
"nodeMtlsCaCertPem": "",
|
||||
"nodeMtlsCaKeyPem": "",
|
||||
"nodeMtlsClientCertPem": "",
|
||||
"nodeMtlsClientKeyPem": "",
|
||||
"nodeMtlsClientCertSha256": "",
|
||||
"nodeMtlsClientCAPem": "",
|
||||
"webBasePath": normalizeBasePath(getEnv("XUI_INIT_WEB_BASE_PATH", "/")),
|
||||
"sessionMaxAge": "360",
|
||||
"trustedProxyCIDRs": DefaultTrustedProxyCIDRs,
|
||||
"ipLimitAllowlist": "",
|
||||
"pageSize": "25",
|
||||
"expireDiff": "0",
|
||||
"trafficDiff": "0",
|
||||
"remarkTemplate": DefaultRemarkTemplate,
|
||||
"subShowIdentityOnAllLinks": "false",
|
||||
"timeLocation": "Local",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
"tgBotProxy": "",
|
||||
"tgBotAPIServer": "",
|
||||
"tgBotChatId": "",
|
||||
"tgRunTime": "@daily",
|
||||
"tgBotBackup": "false",
|
||||
"tgCpu": "80",
|
||||
"tgMemory": "80",
|
||||
"tgLang": "en-US",
|
||||
"twoFactorEnable": "false",
|
||||
"twoFactorToken": "",
|
||||
"subEnable": "true",
|
||||
"subJsonEnable": "false",
|
||||
"subJsonAutoDetect": "false",
|
||||
"subJsonAlwaysArray": "false",
|
||||
"subJsonUserAgentRegex": "",
|
||||
"subClashAutoDetect": "false",
|
||||
"subClashUserAgentRegex": "",
|
||||
"subTitle": "",
|
||||
"subSupportUrl": "",
|
||||
"subProfileUrl": "",
|
||||
"subAnnounce": "",
|
||||
"subEnableRouting": "false",
|
||||
"subRoutingRules": "",
|
||||
"subHideSettings": "false",
|
||||
"subIncyEnableRouting": "false",
|
||||
"subIncyRoutingRules": "",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "/sub/",
|
||||
"subDomain": "",
|
||||
"subCertFile": "",
|
||||
"subKeyFile": "",
|
||||
"subUpdates": "12",
|
||||
"subEncrypt": "true",
|
||||
"subURI": "",
|
||||
"subJsonPath": "/json/",
|
||||
"subJsonURI": "",
|
||||
"subClashEnable": "false",
|
||||
"subClashPath": "/clash/",
|
||||
"subClashURI": "",
|
||||
"subClashEnableRouting": "false",
|
||||
"subClashRules": "",
|
||||
"subJsonMux": "",
|
||||
"subJsonRules": "",
|
||||
"subJsonFinalMask": "",
|
||||
"subJsonObservatory": "",
|
||||
"subThemeDir": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
"warpUpdateInterval": "0",
|
||||
"nord": "",
|
||||
"pia": "",
|
||||
"restartXrayOnClientDisable": "true",
|
||||
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
||||
"panelOutbound": "",
|
||||
"devChannelEnable": "false",
|
||||
|
||||
// LDAP defaults
|
||||
"ldapEnable": "false",
|
||||
"ldapHost": "",
|
||||
"ldapPort": "389",
|
||||
"ldapUseTLS": "false",
|
||||
"ldapInsecureSkipVerify": "false",
|
||||
"ldapBindDN": "",
|
||||
"ldapPassword": "",
|
||||
"ldapBaseDN": "",
|
||||
"ldapUserFilter": "(objectClass=person)",
|
||||
"ldapUserAttr": "mail",
|
||||
"ldapVlessField": "vless_enabled",
|
||||
"ldapSyncCron": "@every 1m",
|
||||
"ldapFlagField": "",
|
||||
"ldapTruthyValues": "true,1,yes,on",
|
||||
"ldapInvertFlag": "false",
|
||||
"ldapInboundTags": "",
|
||||
"ldapAutoCreate": "false",
|
||||
"ldapAutoDelete": "false",
|
||||
"ldapDefaultTotalGB": "0",
|
||||
"ldapDefaultExpiryDays": "0",
|
||||
"ldapDefaultLimitIP": "0",
|
||||
"nodeMtlsCaCertPem": "",
|
||||
"nodeMtlsCaKeyPem": "",
|
||||
"nodeMtlsClientCertPem": "",
|
||||
"nodeMtlsClientKeyPem": "",
|
||||
"nodeMtlsClientCertSha256": "",
|
||||
"nodeMtlsClientCAPem": "",
|
||||
"webBasePath": normalizeBasePath(getEnv("XUI_INIT_WEB_BASE_PATH", "/")),
|
||||
"sessionMaxAge": "360",
|
||||
"trustedProxyCIDRs": DefaultTrustedProxyCIDRs,
|
||||
"ipLimitAllowlist": "",
|
||||
"pageSize": "25",
|
||||
"expireDiff": "0",
|
||||
"trafficDiff": "0",
|
||||
"remarkTemplate": DefaultRemarkTemplate,
|
||||
"subShowIdentityOnAllLinks": "false",
|
||||
"timeLocation": "Local",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
"tgBotProxy": "",
|
||||
"tgBotAPIServer": "",
|
||||
"tgBotChatId": "",
|
||||
"tgRunTime": "@daily",
|
||||
"tgBotBackup": "false",
|
||||
"tgCpu": "80",
|
||||
"tgMemory": "80",
|
||||
"tgLang": "en-US",
|
||||
"twoFactorEnable": "false",
|
||||
"twoFactorToken": "",
|
||||
"subEnable": "true",
|
||||
"subJsonEnable": "false",
|
||||
"subJsonAutoDetect": "false",
|
||||
"subJsonAlwaysArray": "false",
|
||||
"subJsonUserAgentRegex": "",
|
||||
"subClashAutoDetect": "false",
|
||||
"subClashUserAgentRegex": "",
|
||||
"subTitle": "",
|
||||
"subSupportUrl": "",
|
||||
"subProfileUrl": "",
|
||||
"subAnnounce": "",
|
||||
"subEnableRouting": "false",
|
||||
"subRoutingRules": "",
|
||||
"subHideSettings": "false",
|
||||
"subIncyEnableRouting": "false",
|
||||
"subIncyRoutingRules": "",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "/sub/",
|
||||
"subDomain": "",
|
||||
"subCertFile": "",
|
||||
"subKeyFile": "",
|
||||
"subUpdates": "12",
|
||||
"subEncrypt": "true",
|
||||
"subURI": "",
|
||||
"subJsonPath": "/json/",
|
||||
"subJsonURI": "",
|
||||
"subClashEnable": "false",
|
||||
"subClashPath": "/clash/",
|
||||
"subClashURI": "",
|
||||
"subClashEnableRouting": "false",
|
||||
"subClashRules": "",
|
||||
"subJsonMux": "",
|
||||
"subJsonRules": "",
|
||||
"subJsonFinalMask": "",
|
||||
"subJsonObservatory": "",
|
||||
"subThemeDir": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
"warpUpdateInterval": "0",
|
||||
"nord": "",
|
||||
"pia": "",
|
||||
"restartXrayOnClientDisable": "true",
|
||||
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
||||
"panelOutbound": "",
|
||||
"devChannelEnable": "false",
|
||||
|
||||
// Event bus — per-subscriber event filtering (empty = all disabled)
|
||||
"tgEnabledEvents": "login.attempt,cpu.high",
|
||||
@@ -270,7 +247,6 @@ func (s *SettingService) GetAllSettingView() (*entity.AllSettingView, error) {
|
||||
view := &entity.AllSettingView{AllSetting: *allSetting}
|
||||
view.HasTgBotToken = secretConfigured(allSetting.TgBotToken)
|
||||
view.HasTwoFactorToken = secretConfigured(allSetting.TwoFactorToken)
|
||||
view.HasLdapPassword = secretConfigured(allSetting.LdapPassword)
|
||||
view.HasWarpSecret = secretConfigured(mustString(s.GetWarp()))
|
||||
view.HasNordSecret = secretConfigured(mustString(s.GetNord()))
|
||||
view.HasSmtpPassword = secretConfigured(allSetting.SmtpPassword)
|
||||
@@ -280,7 +256,6 @@ func (s *SettingService) GetAllSettingView() (*entity.AllSettingView, error) {
|
||||
}
|
||||
view.TgBotToken = ""
|
||||
view.TwoFactorToken = ""
|
||||
view.LdapPassword = ""
|
||||
view.SmtpPassword = ""
|
||||
return view, nil
|
||||
}
|
||||
@@ -965,91 +940,6 @@ func (s *SettingService) GetAccessLogEnable() (bool, error) {
|
||||
return (accessLogPath != "none" && accessLogPath != ""), nil
|
||||
}
|
||||
|
||||
// GetLdapEnable returns whether LDAP is enabled.
|
||||
func (s *SettingService) GetLdapEnable() (bool, error) {
|
||||
return s.getBool("ldapEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapHost() (string, error) {
|
||||
return s.getString("ldapHost")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapPort() (int, error) {
|
||||
return s.getInt("ldapPort")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapUseTLS() (bool, error) {
|
||||
return s.getBool("ldapUseTLS")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapInsecureSkipVerify() (bool, error) {
|
||||
return s.getBool("ldapInsecureSkipVerify")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapBindDN() (string, error) {
|
||||
return s.getString("ldapBindDN")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapPassword() (string, error) {
|
||||
return s.getString("ldapPassword")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapBaseDN() (string, error) {
|
||||
return s.getString("ldapBaseDN")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapUserFilter() (string, error) {
|
||||
return s.getString("ldapUserFilter")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapUserAttr() (string, error) {
|
||||
return s.getString("ldapUserAttr")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapVlessField() (string, error) {
|
||||
return s.getString("ldapVlessField")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapSyncCron() (string, error) {
|
||||
return s.getString("ldapSyncCron")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapFlagField() (string, error) {
|
||||
return s.getString("ldapFlagField")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapTruthyValues() (string, error) {
|
||||
return s.getString("ldapTruthyValues")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapInvertFlag() (bool, error) {
|
||||
return s.getBool("ldapInvertFlag")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapInboundTags() (string, error) {
|
||||
return s.getString("ldapInboundTags")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapAutoCreate() (bool, error) {
|
||||
return s.getBool("ldapAutoCreate")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapAutoDelete() (bool, error) {
|
||||
return s.getBool("ldapAutoDelete")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapDefaultTotalGB() (int, error) {
|
||||
return s.getInt("ldapDefaultTotalGB")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {
|
||||
return s.getInt("ldapDefaultExpiryDays")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
|
||||
return s.getInt("ldapDefaultLimitIP")
|
||||
}
|
||||
|
||||
// Event bus — per-subscriber event filtering
|
||||
|
||||
func (s *SettingService) GetTgEnabledEvents() (string, error) {
|
||||
@@ -1174,7 +1064,6 @@ func (s *SettingService) SetOutboundDownThreshold(value int) error {
|
||||
// blank to the browser) and the stored value is preserved.
|
||||
type SecretClears struct {
|
||||
TgBotToken bool
|
||||
LdapPassword bool
|
||||
SmtpPassword bool
|
||||
}
|
||||
|
||||
@@ -1278,13 +1167,6 @@ func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting,
|
||||
}
|
||||
allSetting.TgBotToken = value
|
||||
}
|
||||
if !clears.LdapPassword && strings.TrimSpace(allSetting.LdapPassword) == "" {
|
||||
value, err := s.GetLdapPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allSetting.LdapPassword = value
|
||||
}
|
||||
if allSetting.TwoFactorEnable && strings.TrimSpace(allSetting.TwoFactorToken) == "" {
|
||||
value, err := s.GetTwoFactorToken()
|
||||
if err != nil {
|
||||
@@ -1341,7 +1223,7 @@ func validateRemoteRoutingURLSetting(name string, value *string) error {
|
||||
|
||||
func (s *SettingService) UpdateSecret(key string, value string) error {
|
||||
switch key {
|
||||
case "tgBotToken", "ldapPassword", "twoFactorToken":
|
||||
case "tgBotToken", "twoFactorToken":
|
||||
return s.saveSetting(key, strings.TrimSpace(value))
|
||||
default:
|
||||
return common.NewError("secret key is not replaceable:", key)
|
||||
@@ -1476,7 +1358,6 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||
var factoryDefaultSecretKeys = map[string]bool{
|
||||
"tgBotToken": true,
|
||||
"twoFactorToken": true,
|
||||
"ldapPassword": true,
|
||||
"smtpPassword": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ func TestGetFactoryDefaultsOmitsSensitiveMaterial(t *testing.T) {
|
||||
"xrayTemplateConfig",
|
||||
"tgBotToken",
|
||||
"twoFactorToken",
|
||||
"ldapPassword",
|
||||
"smtpPassword",
|
||||
} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
@@ -372,17 +372,6 @@ func (s *Server) startTask(restartXray bool, loc *time.Location) {
|
||||
// Check monthly reset days at midnight
|
||||
_, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("monthly", loc))
|
||||
|
||||
// LDAP sync scheduling
|
||||
if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {
|
||||
runtime, err := s.settingService.GetLdapSyncCron()
|
||||
if err != nil || runtime == "" {
|
||||
runtime = "@every 1m"
|
||||
}
|
||||
j := job.NewLdapSyncJob()
|
||||
// job has zero-value services with method receivers that read settings on demand
|
||||
_, _ = s.cron.AddJob(runtime, j)
|
||||
}
|
||||
|
||||
// Telegram-bot–dependent jobs: periodic stats report + callback-hash cleanup.
|
||||
isTgbotenabled, err := s.settingService.GetTgbotEnabled()
|
||||
if (err == nil) && isTgbotenabled {
|
||||
|
||||
Reference in New Issue
Block a user