From 8153e0ac05d8ad54c3d98c47bb7c4673951b6197 Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Tue, 7 Oct 2025 13:46:30 +0200 Subject: [PATCH 1/2] fragment : MaxSplit --- web/html/settings.html | 40 +++++++++++++------ .../settings/panel/subscription/json.html | 16 +++++--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/web/html/settings.html b/web/html/settings.html index 26b936fa..e664e6ec 100644 --- a/web/html/settings.html +++ b/web/html/settings.html @@ -9,19 +9,20 @@ + message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable> + + + + @@ -74,7 +77,8 @@ - + [[ p ]] From b578a33518fdd883a33aef71c315ce46d3237aec Mon Sep 17 00:00:00 2001 From: mhsanaei Date: Tue, 7 Oct 2025 13:49:08 +0200 Subject: [PATCH 2/2] update dependencies --- go.mod | 19 +- go.sum | 40 +-- web/job/ldap_sync_job.go | 674 +++++++++++++++++++-------------------- 3 files changed, 366 insertions(+), 367 deletions(-) diff --git a/go.mod b/go.mod index d77c23cc..4040c87a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gin-contrib/gzip v1.2.3 github.com/gin-contrib/sessions v1.0.4 github.com/gin-gonic/gin v1.11.0 - github.com/go-ldap/ldap/v3 v3.4.11 + github.com/go-ldap/ldap/v3 v3.4.12 github.com/goccy/go-json v0.10.5 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 @@ -15,7 +15,7 @@ require ( github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pelletier/go-toml/v2 v2.2.4 github.com/robfig/cron/v3 v3.0.1 - github.com/shirou/gopsutil/v4 v4.25.8 + github.com/shirou/gopsutil/v4 v4.25.9 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/valyala/fasthttp v1.66.0 github.com/xlzd/gotp v0.1.0 @@ -24,7 +24,7 @@ require ( golang.org/x/crypto v0.42.0 golang.org/x/sys v0.36.0 golang.org/x/text v0.29.0 - google.golang.org/grpc v1.75.1 + google.golang.org/grpc v1.76.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.0 ) @@ -45,7 +45,7 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/gorilla/context v1.1.2 // indirect @@ -70,11 +70,11 @@ require ( github.com/pires/go-proxyproto v0.8.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect github.com/refraction-networking/utls v1.8.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sagernet/sing v0.7.10 // indirect + github.com/sagernet/sing v0.7.12 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect @@ -86,9 +86,8 @@ require ( github.com/valyala/fastjson v1.6.4 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect - github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect + github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.uber.org/mock v0.6.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/arch v0.21.0 // indirect golang.org/x/mod v0.28.0 // indirect @@ -98,8 +97,8 @@ require ( golang.org/x/tools v0.37.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/protobuf v1.36.9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 // indirect + google.golang.org/protobuf v1.36.10 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 1cae2aae..05f5cc41 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= @@ -39,8 +39,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= -github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -54,8 +54,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -148,8 +148,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= @@ -158,14 +158,14 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw= -github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA= +github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= -github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970= -github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= +github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU= +github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -200,8 +200,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= -github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU= -github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= +github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 h1:jb1y+Rm6UBW/CEV0FehsKlQ/2dnLsQjyUjn3UfWwbic= +github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno= github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -256,12 +256,12 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 h1:WgGZrMngVRRve7T3P5gbXdmedSmUpkf8uIUu1fg+biY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/web/job/ldap_sync_job.go b/web/job/ldap_sync_job.go index cb954932..6642bbcf 100644 --- a/web/job/ldap_sync_job.go +++ b/web/job/ldap_sync_job.go @@ -1,421 +1,421 @@ package job import ( - "time" + "time" - "github.com/mhsanaei/3x-ui/v2/database/model" - "github.com/mhsanaei/3x-ui/v2/logger" - ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" - "github.com/mhsanaei/3x-ui/v2/web/service" - "strings" + "strings" - "github.com/google/uuid" - "strconv" + "github.com/mhsanaei/3x-ui/v2/database/model" + "github.com/mhsanaei/3x-ui/v2/logger" + ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" + "github.com/mhsanaei/3x-ui/v2/web/service" + + "strconv" + + "github.com/google/uuid" ) var DefaultTruthyValues = []string{"true", "1", "yes", "on"} type LdapSyncJob struct { - settingService service.SettingService - inboundService service.InboundService - xrayService service.XrayService + settingService service.SettingService + inboundService service.InboundService + xrayService service.XrayService } // --- Helper functions for mustGet --- func mustGetString(fn func() (string, error)) string { - v, err := fn() - if err != nil { - panic(err) - } - return v + 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 + 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 + 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 + v, err := fn() + if err != nil || v == "" { + return fallback + } + return v } - func NewLdapSyncJob() *LdapSyncJob { - return new(LdapSyncJob) + return new(LdapSyncJob) } func (j *LdapSyncJob) Run() { - logger.Info("LDAP sync job started") + 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 - } + 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), - 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: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)), - Invert: mustGetBool(j.settingService.GetLdapInvertFlag), - } + // --- LDAP fetch --- + cfg := ldaputil.Config{ + Host: mustGetString(j.settingService.GetLdapHost), + Port: mustGetInt(j.settingService.GetLdapPort), + UseTLS: mustGetBool(j.settingService.GetLdapUseTLS), + 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: splitCsv(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)) + 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 - } + // --- 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] - } - } + 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) + // --- Prepare batch operations --- + autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate) + defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB) + 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 + 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 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) + } + } + } + } - // --- 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() - } - } + // --- 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() + } + } - // --- 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) - } + // --- 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 { - ldapEmailSet := map[string]struct{}{} - for e := range flags { - ldapEmailSet[e] = struct{}{} - } - for _, tag := range inboundTags { - j.deleteClientsNotInLDAP(tag, ldapEmailSet) - } - } + // --- Auto delete clients not in LDAP --- + autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete) + if autoDelete { + ldapEmailSet := map[string]struct{}{} + for e := range flags { + ldapEmailSet[e] = struct{}{} + } + for _, tag := range inboundTags { + j.deleteClientsNotInLDAP(tag, ldapEmailSet) + } + } } - - func splitCsv(s string) []string { - if s == "" { - return DefaultTruthyValues - } - 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 + if s == "" { + return DefaultTruthyValues + } + 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 func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client { - c := model.Client{ - Email: email, - Enable: true, - LimitIP: defLimitIP, - TotalGB: int64(defGB), - } - 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 + c := model.Client{ + Email: email, + Enable: true, + LimitIP: defLimitIP, + TotalGB: int64(defGB), + } + 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 } // 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 - } + if len(emails) == 0 { + return + } - // Prepare JSON for mass update - clients := make([]model.Client, 0, len(emails)) - for _, email := range emails { - clients = append(clients, model.Client{ - Email: email, - Enable: enable, - }) - } + // Prepare JSON for mass update + clients := make([]model.Client, 0, len(emails)) + for _, email := range emails { + clients = append(clients, model.Client{ + Email: email, + Enable: enable, + }) + } - payload := &model.Inbound{ - Id: ib.Id, - Settings: j.clientsToJSON(clients), - } + payload := &model.Inbound{ + Id: ib.Id, + Settings: j.clientsToJSON(clients), + } - // 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 - } + // 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 + } - logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag) - 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 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 - } + 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 + 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 - } + 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) - } - } + // 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 - } + if len(toDelete) == 0 { + continue + } - // Delete in batches - for i := 0; i < len(toDelete); i += batchSize { - end := i + batchSize - if end > len(toDelete) { - end = len(toDelete) - } - batch := toDelete[i:end] + // Delete in batches + for i := 0; i < len(toDelete); i += batchSize { + end := i + batchSize + if end > len(toDelete) { + end = 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 - } + 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 { - 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 - restartNeeded = true - } - } - } - } + if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); 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 + restartNeeded = true + } + } + } + } - // One time after all batches - if restartNeeded { - j.xrayService.SetToNeedRestart() - logger.Info("Xray restart scheduled after batch deletion") - } + // 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() + 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() } - // ensureClientExists adds client with defaults to inbound tag if not present func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) { - inbounds, err := j.inboundService.GetAllInbounds() - if err != nil { - logger.Warning("ensureClientExists: get inbounds failed:", err) - return - } - var target *model.Inbound - for _, ib := range inbounds { - if ib.Tag == inboundTag { - target = ib - break - } - } - if target == nil { - logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag) - return - } - // check if email already exists in this inbound - clients, err := j.inboundService.GetClients(target) - if err == nil { - for _, c := range clients { - if c.Email == email { - return - } - } - } + inbounds, err := j.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("ensureClientExists: get inbounds failed:", err) + return + } + var target *model.Inbound + for _, ib := range inbounds { + if ib.Tag == inboundTag { + target = ib + break + } + } + if target == nil { + logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag) + return + } + // check if email already exists in this inbound + clients, err := j.inboundService.GetClients(target) + if err == nil { + for _, c := range clients { + if c.Email == email { + return + } + } + } - // build new client according to protocol - newClient := model.Client{ - Email: email, - Enable: true, - LimitIP: defLimitIP, - TotalGB: int64(defGB), - } - if defExpiryDays > 0 { - newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() - } + // build new client according to protocol + newClient := model.Client{ + Email: email, + Enable: true, + LimitIP: defLimitIP, + TotalGB: int64(defGB), + } + if defExpiryDays > 0 { + newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() + } - switch target.Protocol { - case model.Trojan: - newClient.Password = uuid.NewString() - case model.Shadowsocks: - newClient.Password = uuid.NewString() - default: // VMESS/VLESS and others using ID - newClient.ID = uuid.NewString() - } + switch target.Protocol { + case model.Trojan: + newClient.Password = uuid.NewString() + case model.Shadowsocks: + newClient.Password = uuid.NewString() + default: // VMESS/VLESS and others using ID + newClient.ID = uuid.NewString() + } - // prepare inbound payload with only the new client - payload := &model.Inbound{Id: target.Id} - payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}` + // prepare inbound payload with only the new client + payload := &model.Inbound{Id: target.Id} + payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}` - if _, err := j.inboundService.AddInboundClient(payload); err != nil { - logger.Warning("ensureClientExists: add client failed:", err) - } else { - j.xrayService.SetToNeedRestart() - logger.Infof("LDAP auto-create: %s in %s", email, inboundTag) - } + if _, err := j.inboundService.AddInboundClient(payload); err != nil { + logger.Warning("ensureClientExists: add client failed:", err) + } else { + j.xrayService.SetToNeedRestart() + logger.Infof("LDAP auto-create: %s in %s", email, inboundTag) + } } // 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() + // 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() } - -