fix: 修复CSP白屏并优化LDAP配置

This commit is contained in:
Logan.Li committed 2026-07-25 22:28:16 +08:00
1 parent a14d9e62e6
commit c7f59f2cbf
12 files changed
+930 -70

No files matched your search

+48 -26
View File
@@ -6,7 +6,6 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
@@ -19,6 +18,7 @@ import (
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
"vpn-platform/backend/internal/ldaputil"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/service/ikev2"
"vpn-platform/backend/internal/service/l2tp"
@@ -85,27 +85,20 @@ func ldapSettings() (*model.LDAPConfig, error) {
}
func ldapConnect(cfg *model.LDAPConfig) (*ldap.Conn, error) {
url := cfg.URL
if !strings.HasPrefix(url, "ldap://") && !strings.HasPrefix(url, "ldaps://") {
// 安全默认:未显式指定协议时按 ldaps,避免口令明文过网
url = "ldaps://" + url
}
conn, err := ldap.DialURL(url)
dialURL, err := ldaputil.DialURL(cfg.URL)
if err != nil {
return nil, err
}
if cfg.StartTLS && strings.HasPrefix(url, "ldap://") {
host := strings.TrimPrefix(url, "ldap://")
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
conn, err := ldap.DialURL(dialURL)
if err != nil {
return nil, err
}
if cfg.StartTLS && strings.HasPrefix(dialURL, "ldap://") {
host := ldaputil.Hostname(dialURL)
if err := conn.StartTLS(&tls.Config{ServerName: host}); err != nil {
conn.Close()
return nil, fmt.Errorf("StartTLS 升级失败: %w", err)
}
} else if strings.HasPrefix(url, "ldap://") {
conn.Close()
return nil, fmt.Errorf("明文 ldap:// 存在口令泄露风险,请改用 ldaps:// 或开启 StartTLS")
}
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil {
@@ -126,7 +119,12 @@ func (s *Server) authenticateLDAP(username, password string) (*model.User, error
return nil, err
}
defer conn.Close()
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UsernameAttribute, ldap.EscapeFilter(username))
filter := cfg.UserFilter
if strings.Contains(filter, "%s") {
filter = fmt.Sprintf(filter, ldap.EscapeFilter(username))
} else {
filter = fmt.Sprintf("(&%s(%s=%s))", filter, cfg.UsernameAttribute, ldap.EscapeFilter(username))
}
result, err := conn.Search(ldap.NewSearchRequest(cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 5, false, filter, []string{"dn", cfg.UsernameAttribute, "cn", "mail"}, nil))
if err != nil || len(result.Entries) != 1 {
return nil, fmt.Errorf("LDAP 用户不存在")
@@ -361,6 +359,12 @@ func (s *Server) saveLDAPConfig(c *gin.Context) {
c.JSON(400, gin.H{"error": "LDAP 地址与 Base DN 必填"})
return
}
if req.Enabled {
if _, err := ldaputil.DialURL(req.URL); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
}
var cfg model.LDAPConfig
store.DB.First(&cfg)
cfg.URL, cfg.BindDN, cfg.BaseDN, cfg.UserFilter, cfg.UsernameAttribute, cfg.StartTLS, cfg.Enabled =
@@ -373,17 +377,35 @@ func (s *Server) saveLDAPConfig(c *gin.Context) {
}
func (s *Server) testLDAP(c *gin.Context) {
cfg, err := ldapSettings()
if err == nil {
conn, connErr := ldapConnect(cfg)
if connErr == nil {
conn.Close()
c.JSON(200, gin.H{"ok": true})
return
}
err = connErr
var req struct {
URL string `json:"url"`
BindDN string `json:"bind_dn"`
BindPassword string `json:"bind_password"`
StartTLS bool `json:"starttls"`
}
c.JSON(400, gin.H{"error": "LDAP 连接失败: " + err.Error()})
if c.ShouldBindJSON(&req) != nil || strings.TrimSpace(req.URL) == "" {
c.JSON(400, gin.H{"error": "LDAP 服务器地址必填"})
return
}
cfg := &model.LDAPConfig{URL: strings.TrimSpace(req.URL), BindDN: strings.TrimSpace(req.BindDN), BindPassword: req.BindPassword, StartTLS: req.StartTLS}
if cfg.BindPassword == "" {
var saved model.LDAPConfig
if store.DB.First(&saved).Error == nil && saved.BindPassword != "" {
plain, err := store.DecryptSecret(saved.BindPassword)
if err != nil {
c.JSON(400, gin.H{"error": "LDAP 配置密钥无效"})
return
}
cfg.BindPassword = plain
}
}
conn, err := ldapConnect(cfg)
if err != nil {
c.JSON(400, gin.H{"error": "LDAP 连接失败: " + err.Error()})
return
}
conn.Close()
c.JSON(200, gin.H{"ok": true})
}
func (s *Server) listVPNUsers(c *gin.Context) {
+69
View File
@@ -0,0 +1,69 @@
// Package ldaputil normalizes LDAP server addresses supplied by administrators.
package ldaputil
import (
"fmt"
"net"
"net/url"
"strings"
)
// DialURL accepts an IP address or domain name, with an optional port. Bare
// addresses use LDAPS on port 636. Existing ldap:// and ldaps:// values remain
// supported for backward compatibility.
func DialURL(address string) (string, error) {
address = strings.TrimSpace(address)
if address == "" {
return "", fmt.Errorf("LDAP 服务器地址不能为空")
}
if strings.Contains(address, "://") {
u, err := url.Parse(address)
if err != nil || (u.Scheme != "ldap" && u.Scheme != "ldaps") || u.Host == "" || u.Path != "" || u.RawQuery != "" || u.Fragment != "" {
return "", fmt.Errorf("LDAP 服务器地址格式无效")
}
port := "389"
if u.Scheme == "ldaps" {
port = "636"
}
host, err := withDefaultPort(u.Host, port)
if err != nil {
return "", err
}
u.Host = host
return u.String(), nil
}
host, err := withDefaultPort(address, "636")
if err != nil {
return "", err
}
return "ldaps://" + host, nil
}
func withDefaultPort(address, defaultPort string) (string, error) {
if strings.ContainsAny(address, " \t\r\n/?#@") {
return "", fmt.Errorf("LDAP 服务器地址格式无效")
}
if host, port, err := net.SplitHostPort(address); err == nil {
if host == "" || port == "" {
return "", fmt.Errorf("LDAP 服务器地址格式无效")
}
return address, nil
}
host := strings.TrimSuffix(strings.TrimPrefix(address, "["), "]")
if host == "" || (strings.Contains(host, ":") && net.ParseIP(host) == nil) {
return "", fmt.Errorf("LDAP 服务器地址格式无效")
}
return net.JoinHostPort(host, defaultPort), nil
}
// Hostname returns the hostname or IP for TLS ServerName selection.
func Hostname(dialURL string) string {
u, err := url.Parse(dialURL)
if err != nil {
return ""
}
return u.Hostname()
}
+32
View File
@@ -0,0 +1,32 @@
package ldaputil
import "testing"
func TestDialURL(t *testing.T) {
tests := []struct {
address string
want string
}{
{"8.217.205.92", "ldaps://8.217.205.92:636"},
{"ldap.opsx.vip", "ldaps://ldap.opsx.vip:636"},
{"ldap.opsx.vip:1636", "ldaps://ldap.opsx.vip:1636"},
{"2001:db8::1", "ldaps://[2001:db8::1]:636"},
{"ldap://ldap.opsx.vip", "ldap://ldap.opsx.vip:389"},
{"ldaps://ldap.opsx.vip", "ldaps://ldap.opsx.vip:636"},
{" ldaps://8.217.205.92 ", "ldaps://8.217.205.92:636"},
}
for _, tt := range tests {
got, err := DialURL(tt.address)
if err != nil || got != tt.want {
t.Errorf("DialURL(%q) = %q, %v; want %q", tt.address, got, err, tt.want)
}
}
}
func TestDialURLRejectsInvalidAddress(t *testing.T) {
for _, address := range []string{"", "ldap.opsx.vip/path", "ldap.opsx.vip:", "https://ldap.opsx.vip"} {
if _, err := DialURL(address); err == nil {
t.Errorf("DialURL(%q) succeeded; want error", address)
}
}
}
+5 -4
View File
@@ -28,6 +28,7 @@ import (
"gorm.io/gorm"
"vpn-platform/backend/internal/core"
"vpn-platform/backend/internal/ldaputil"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/service"
"vpn-platform/backend/internal/sessionmgr"
@@ -903,11 +904,11 @@ func verifyDialPassword(user *model.VPNUser, password string) error {
if err := store.DB.Where("enabled = ?", true).First(&cfg).Error; err != nil {
return errors.New("LDAP 未启用")
}
url := cfg.URL
if !strings.HasPrefix(url, "ldap://") && !strings.HasPrefix(url, "ldaps://") {
url = "ldap://" + url
dialURL, err := ldaputil.DialURL(cfg.URL)
if err != nil {
return err
}
conn, err := ldap.DialURL(url)
conn, err := ldap.DialURL(dialURL)
if err != nil {
return err
}
+659
View File
@@ -18,6 +18,7 @@
"vue-router": "^4.4.5"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "4.0.0",
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.10"
}
@@ -502,6 +503,35 @@
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
"node_modules/@intlify/bundle-utils": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/@intlify/bundle-utils/-/bundle-utils-8.0.0.tgz",
"integrity": "sha512-1B++zykRnMwQ+20SpsZI1JCnV/YJt9Oq7AGlEurzkWJOFtFAVqaGc/oV36PBRYeiKnTbY9VYfjBimr2Vt42wLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "^9.4.0",
"@intlify/shared": "^9.4.0",
"acorn": "^8.8.2",
"escodegen": "^2.1.0",
"estree-walker": "^2.0.2",
"jsonc-eslint-parser": "^2.3.0",
"mlly": "^1.2.0",
"source-map-js": "^1.0.1",
"yaml-eslint-parser": "^1.2.2"
},
"engines": {
"node": ">= 14.16"
},
"peerDependenciesMeta": {
"petite-vue-i18n": {
"optional": true
},
"vue-i18n": {
"optional": true
}
}
},
"node_modules/@intlify/core-base": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
@@ -546,12 +576,90 @@
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/unplugin-vue-i18n": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-4.0.0.tgz",
"integrity": "sha512-q2Mhqa/mLi0tulfLFO4fMXXvEbkSZpI5yGhNNsLTNJJ41icEGUuyDe+j5zRZIKSkOJRgX6YbCyibTDJdRsukmw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/bundle-utils": "^8.0.0",
"@intlify/shared": "^9.4.0",
"@rollup/pluginutils": "^5.1.0",
"@vue/compiler-sfc": "^3.2.47",
"debug": "^4.3.3",
"fast-glob": "^3.2.12",
"js-yaml": "^4.1.0",
"json5": "^2.2.3",
"pathe": "^1.0.0",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2",
"unplugin": "^1.1.0"
},
"engines": {
"node": ">= 14.16"
},
"peerDependencies": {
"petite-vue-i18n": "*",
"vue-i18n": "*",
"vue-i18n-bridge": "*"
},
"peerDependenciesMeta": {
"petite-vue-i18n": {
"optional": true
},
"vue-i18n": {
"optional": true
},
"vue-i18n-bridge": {
"optional": true
}
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.8",
@@ -563,6 +671,29 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.4.0",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
@@ -1097,6 +1228,29 @@
"vue": "^3.5.0"
}
},
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
@@ -1133,6 +1287,13 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@@ -1157,6 +1318,19 @@
"proxy-from-env": "^2.1.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1220,6 +1394,13 @@
"node": ">= 0.8"
}
},
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"dev": true,
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
@@ -1425,12 +1606,152 @@
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
@@ -1614,6 +1935,16 @@
"node": ">= 6"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -1623,6 +1954,84 @@
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/js-yaml": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/jsonc-eslint-parser": {
"version": "2.4.2",
"resolved": "https://registry.npmmirror.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.2.tgz",
"integrity": "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.5.0",
"eslint-visitor-keys": "^3.0.0",
"espree": "^9.0.0",
"semver": "^7.3.5"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
@@ -1682,6 +2091,43 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
@@ -1703,6 +2149,26 @@
"node": ">= 0.6"
}
},
"node_modules/mlly": {
"version": "1.8.2",
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz",
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.16.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.3"
}
},
"node_modules/mlly/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -1778,12 +2244,51 @@
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/pkg-types/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
@@ -1847,6 +2352,27 @@
"node": ">=10.13.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
@@ -1862,6 +2388,17 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rollup": {
"version": "4.62.2",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz",
@@ -1907,12 +2444,60 @@
"fsevents": "~2.3.2"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1948,12 +2533,46 @@
"node": ">=8"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"dev": true,
"license": "MIT"
},
"node_modules/unplugin": {
"version": "1.16.1",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz",
"integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
@@ -2077,6 +2696,13 @@
"vue": "^3.5.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
@@ -2103,6 +2729,39 @@
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yaml-eslint-parser": {
"version": "1.3.2",
"resolved": "https://registry.npmmirror.com/yaml-eslint-parser/-/yaml-eslint-parser-1.3.2.tgz",
"integrity": "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.0.0",
"yaml": "^2.0.0"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
+1
View File
@@ -18,6 +18,7 @@
"vue-router": "^4.4.5"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "4.0.0",
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.10"
}
+20 -10
View File
@@ -73,7 +73,7 @@
"passwordLabel": "Password",
"passwordPlaceholder": "Enter password",
"loginButton": "Sign In",
"defaultAccount": "Default account: <strong>admin / admin123</strong> — please change password after first login",
"defaultAccount": "Default account: {account} — please change password after first login",
"errorEmpty": "Please enter username and password",
"loginSuccess": "Login successful",
"changePasswordFirst": "Please change your password first",
@@ -279,14 +279,22 @@
"roleOperator": "Operator",
"roleViewer": "Viewer",
"noUsers": "No users yet",
"ldapTitle": "LDAP Identity Source",
"ldapTitle": "LDAP / Active Directory",
"ldapHint": "LDAP users can be auto-synced on first login, or pre-imported using the \"LDAP Import\" on the left.",
"ldapUrl": "LDAP URL",
"bindDn": "Bind DN",
"bindPassword": "Bind Password",
"ldapConfigHint": "Configure an LDAP server for centralized identity authentication",
"ldapEnabled": "Enable LDAP",
"ldapServerAddress": "LDAP Server Address",
"ldapServerAddressPlaceholder": "e.g. 8.217.205.92 or ldap.example.com",
"ldapPort": "Port",
"ldapProtocol": "Protocol",
"ldapProtocolLDAPS": "LDAPS (Recommended)",
"ldapProtocolLDAP": "LDAP",
"bindDn": "Administrator Bind DN",
"bindPassword": "Administrator Password",
"keepOriginal": "Leave empty to keep original",
"baseDn": "Base DN",
"userFilter": "User Filter",
"baseDn": "Base DN (User Search Base)",
"userFilter": "Search Filter (optional; %s is the username)",
"enableLdaps": "Enable LDAPS (SSL encryption)",
"usernameAttr": "Username Attribute",
"testConnection": "Test Connection",
"editUser": "Edit User",
@@ -320,6 +328,7 @@
"ldapLoadFailed": "LDAP load failed",
"ldapConfigSaved": "LDAP configuration saved",
"ldapConnected": "LDAP connection successful",
"ldapTestRequired": "Test the LDAP connection successfully before saving",
"ldapConnectFailed": "Connection failed",
"emptyUsername": "(empty)",
"errorDetails": "Error details:"
@@ -502,8 +511,9 @@
"plugins": {
"title": "Custom Protocol Plugins",
"registerPlugin": "Register Plugin",
"pluginHint": "Plugins implement the unified protocol contract via <b>command hooks</b>: the platform pipes tunnel config JSON to hook commands via stdin.",
"pluginContract": "Contract: <code>Create / Start / Stop / Status / Config</code>; Status hook outputs <code>running</code> or <code>stopped</code> on stdout; Config hook outputs client config text.",
"commandHooks": "command hooks",
"pluginHint": "Plugins implement the unified protocol contract via {hooks}: the platform pipes tunnel config JSON to hook commands via stdin.",
"pluginContract": "Contract: {contract}; Status hook outputs {running} or {stopped} on stdout; Config hook outputs client config text.",
"protocolId": "Protocol ID",
"enabled": "Enabled",
"disabled": "Disabled",
@@ -1024,4 +1034,4 @@
}
}
}
}
}
+20 -10
View File
@@ -73,7 +73,7 @@
"passwordLabel": "密码",
"passwordPlaceholder": "请输入密码",
"loginButton": "登 录",
"defaultAccount": "默认账号 <strong>admin / admin123</strong>,登录后请立即修改",
"defaultAccount": "默认账号 {account},登录后请立即修改",
"errorEmpty": "请输入用户名和密码",
"loginSuccess": "登录成功",
"changePasswordFirst": "首次登录请先修改密码",
@@ -279,14 +279,22 @@
"roleOperator": "运维",
"roleViewer": "只读",
"noUsers": "还没有用户",
"ldapTitle": "LDAP 身份源",
"ldapTitle": "LDAP / Active Directory",
"ldapHint": "LDAP 用户可首次登录后自动同步,也可使用左侧「LDAP 导入」预先添加。",
"ldapUrl": "LDAP URL",
"bindDn": "Bind DN",
"bindPassword": "Bind 密码",
"ldapConfigHint": "配置 LDAP 服务器,实现统一身份认证",
"ldapEnabled": "启用 LDAP",
"ldapServerAddress": "LDAP 服务器地址",
"ldapServerAddressPlaceholder": "例如 8.217.205.92 或 ldap.example.com",
"ldapPort": "端口",
"ldapProtocol": "连接协议",
"ldapProtocolLDAPS": "LDAPS(推荐)",
"ldapProtocolLDAP": "LDAP",
"bindDn": "管理员 Bind DN",
"bindPassword": "管理员密码",
"keepOriginal": "留空保持原值",
"baseDn": "Base DN",
"userFilter": "用户过滤",
"baseDn": "Base DN(用户搜索基础)",
"userFilter": "搜索过滤器(可选,%s 表示用户名)",
"enableLdaps": "启用 LDAPS(SSL 加密)",
"usernameAttr": "用户名属性",
"testConnection": "测试连接",
"editUser": "编辑用户",
@@ -320,6 +328,7 @@
"ldapLoadFailed": "LDAP 加载失败",
"ldapConfigSaved": "LDAP 配置已保存",
"ldapConnected": "LDAP 连接成功",
"ldapTestRequired": "请先测试 LDAP 连接,通过后才能保存",
"ldapConnectFailed": "连接失败",
"emptyUsername": "(空)",
"errorDetails": "失败详情:"
@@ -502,8 +511,9 @@
"plugins": {
"title": "自定义协议插件",
"registerPlugin": "注册插件",
"pluginHint": "插件通过<b>命令钩子</b>实现统一协议契约:平台将专线配置 JSON 通过 stdin 传给钩子命令。",
"pluginContract": "契约:<code>Create / Start / Stop / Status / Config</code>;Status 钩子 stdout 输出 <code>running</code> 或 <code>stopped</code>;Config 钩子 stdout 输出客户端配置文本。",
"commandHooks": "命令钩子",
"pluginHint": "插件通过 {hooks} 实现统一协议契约:平台将专线配置 JSON 通过 stdin 传给钩子命令。",
"pluginContract": "契约:{contract};Status 钩子 stdout 输出 {running} 或 {stopped};Config 钩子 stdout 输出客户端配置文本。",
"protocolId": "协议标识",
"enabled": "启用",
"disabled": "停用",
@@ -1024,4 +1034,4 @@
}
}
}
}
}
+3 -1
View File
@@ -72,7 +72,9 @@
</form>
<div class="login-footer">
<p v-html="$t('login.defaultAccount')"></p>
<i18n-t keypath="login.defaultAccount" tag="p">
<template #account><strong>admin / admin123</strong></template>
</i18n-t>
</div>
</div>
</div>
+8 -2
View File
@@ -8,8 +8,14 @@
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom:14px">
<p style="margin:0" v-html="$t('plugins.pluginHint')"></p>
<p style="margin:4px 0 0;font-size:12px" v-html="$t('plugins.pluginContract')"></p>
<i18n-t keypath="plugins.pluginHint" tag="p" style="margin:0">
<template #hooks><b>{{ $t('plugins.commandHooks') }}</b></template>
</i18n-t>
<i18n-t keypath="plugins.pluginContract" tag="p" style="margin:4px 0 0;font-size:12px">
<template #contract><code>Create / Start / Stop / Status / Config</code></template>
<template #running><code>running</code></template>
<template #stopped><code>stopped</code></template>
</i18n-t>
</el-alert>
<el-table :data="plugins" size="default" v-loading="loading">
<el-table-column prop="display_name" :label="$t('common.name')" width="160" />
+55 -16
View File
@@ -46,20 +46,28 @@
<el-col :xs="24" :lg="9">
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('users.ldapTitle') }}</b></template>
<el-alert type="info" :closable="false"
:title="$t('users.ldapHint')" style="margin-bottom:12px" />
<el-form label-width="90px" size="small">
<el-form-item :label="$t('common.enabled')"><el-switch v-model="ldap.enabled" /></el-form-item>
<el-form-item :label="$t('users.ldapUrl')"><el-input v-model="ldap.url" placeholder="ldaps://ldap.example.com:636" /></el-form-item>
<el-form-item :label="$t('users.bindDn')"><el-input v-model="ldap.bind_dn" /></el-form-item>
<template #header>
<div>
<b>{{ $t('users.ldapTitle') }}</b>
<div style="margin-top:4px;font-size:12px;color:var(--text-secondary)">{{ $t('users.ldapConfigHint') }}</div>
</div>
</template>
<el-form label-width="112px" size="small">
<el-form-item :label="$t('users.ldapEnabled')"><el-switch v-model="ldap.enabled" /></el-form-item>
<el-form-item :label="$t('users.ldapServerAddress')">
<el-input v-model.trim="ldap.url" :placeholder="$t('users.ldapServerAddressPlaceholder')" />
</el-form-item>
<el-form-item :label="$t('users.ldapPort')">
<el-input-number v-model="ldapPort" :min="1" :max="65535" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item :label="$t('users.baseDn')"><el-input v-model.trim="ldap.base_dn" placeholder="dc=opsx,dc=vip" /></el-form-item>
<el-form-item :label="$t('users.bindDn')"><el-input v-model.trim="ldap.bind_dn" placeholder="cn=admin,dc=opsx,dc=vip" /></el-form-item>
<el-form-item :label="$t('users.bindPassword')"><el-input v-model="ldap.bind_password" type="password" show-password :placeholder="$t('users.keepOriginal')" /></el-form-item>
<el-form-item :label="$t('users.baseDn')"><el-input v-model="ldap.base_dn" placeholder="dc=example,dc=com" /></el-form-item>
<el-form-item :label="$t('users.userFilter')"><el-input v-model="ldap.user_filter" placeholder="(objectClass=inetOrgPerson)" /></el-form-item>
<el-form-item :label="$t('users.usernameAttr')"><el-input v-model="ldap.username_attribute" placeholder="uid" /></el-form-item>
<el-form-item :label="$t('users.userFilter')"><el-input v-model.trim="ldap.user_filter" placeholder="(uid=%s)" /></el-form-item>
<el-form-item :label="$t('users.enableLdaps')"><el-switch v-model="ldapUseLDAPS" /></el-form-item>
<div style="text-align:right">
<el-button @click="testLDAP">{{ $t('users.testConnection') }}</el-button>
<el-button type="primary" @click="saveLDAP">{{ $t('common.save') }}</el-button>
<el-button :loading="ldapTesting" @click="testLDAP">{{ $t('users.testConnection') }}</el-button>
<el-button type="primary" :disabled="ldap.enabled && !ldapTested" :title="ldap.enabled && !ldapTested ? $t('users.ldapTestRequired') : ''" @click="saveLDAP">{{ $t('common.save') }}</el-button>
</div>
</el-form>
</el-card>
@@ -147,7 +155,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import api, { auth } from '../api'
@@ -163,6 +171,10 @@ const ldap = ref({
base_dn: '', user_filter: '(objectClass=inetOrgPerson)', username_attribute: 'uid',
start_tls: false,
})
const ldapUseLDAPS = ref(false)
const ldapPort = ref(389)
const ldapTested = ref(false)
const ldapTesting = ref(false)
// 批量导入
const batchVisible = ref(false)
@@ -183,6 +195,13 @@ async function load() {
const [u, l] = await Promise.all([api.get('/users'), api.get('/ldap')])
users.value = u.data || []
if (l.data) {
const savedAddress = String(l.data.url || '').trim()
const protocolMatch = savedAddress.match(/^(ldap|ldaps):\/\/(\[[^\]]+\]|[^/:]+)(?::(\d+))?$/i)
if (protocolMatch) {
ldapUseLDAPS.value = protocolMatch[1].toLowerCase() === 'ldaps'
l.data.url = protocolMatch[2].replace(/^\[|\]$/g, '')
ldapPort.value = Number(protocolMatch[3]) || (ldapUseLDAPS.value ? 636 : 389)
}
ldap.value = { ...ldap.value, ...l.data, bind_password: '' }
}
} catch (e) {
@@ -190,6 +209,20 @@ async function load() {
} finally { loading.value = false }
}
function ldapPayload() {
const address = ldap.value.url.trim()
const host = address.includes(':') && !address.startsWith('[') ? `[${address}]` : address
const port = Number(ldapPort.value)
const protocol = ldapUseLDAPS.value ? 'ldaps' : 'ldap'
return { ...ldap.value, url: address && port >= 1 && port <= 65535 ? `${protocol}://${host}:${port}` : '' }
}
watch(ldapUseLDAPS, (useLDAPS, previousUseLDAPS) => {
const previousPort = previousUseLDAPS ? 636 : 389
if (ldapPort.value === previousPort) ldapPort.value = useLDAPS ? 636 : 389
})
watch([ldap, ldapUseLDAPS, ldapPort], () => { ldapTested.value = false }, { deep: true })
// ---- 单用户 CRUD ----
function openCreate() {
editing.value = null
@@ -290,16 +323,22 @@ async function doLDAPImport() {
// ---- LDAP 配置 ----
async function saveLDAP() {
try {
await api.put('/ldap', ldap.value)
if (ldap.value.enabled && !ldapTested.value) return ElMessage.warning(t('users.ldapTestRequired'))
await api.put('/ldap', ldapPayload())
ldap.value.bind_password = ''
ElMessage.success(t('users.ldapConfigSaved'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('common.saveFailed')) }
}
async function testLDAP() {
ldapTesting.value = true
try {
await api.post('/ldap/test')
await api.post('/ldap/test', ldapPayload())
ldapTested.value = true
ElMessage.success(t('users.ldapConnected'))
} catch (e) { ElMessage.error(e.response?.data?.error || t('users.ldapConnectFailed')) }
} catch (e) {
ldapTested.value = false
ElMessage.error(e.response?.data?.error || t('users.ldapConnectFailed'))
} finally { ldapTesting.value = false }
}
onMounted(load)
+10 -1
View File
@@ -1,8 +1,17 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
plugins: [
vue(),
VueI18nPlugin({
include: [fileURLToPath(new URL('./src/locales/**', import.meta.url))],
runtimeOnly: true,
jitCompilation: false,
}),
],
server: {
port: 5173,
proxy: { '/api': 'http://localhost:8080' }