diff --git a/backend/internal/api/identity.go b/backend/internal/api/identity.go
index 20234cb..e4be81b 100644
--- a/backend/internal/api/identity.go
+++ b/backend/internal/api/identity.go
@@ -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) {
diff --git a/backend/internal/ldaputil/address.go b/backend/internal/ldaputil/address.go
new file mode 100644
index 0000000..0d56d96
--- /dev/null
+++ b/backend/internal/ldaputil/address.go
@@ -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()
+}
diff --git a/backend/internal/ldaputil/address_test.go b/backend/internal/ldaputil/address_test.go
new file mode 100644
index 0000000..c515c52
--- /dev/null
+++ b/backend/internal/ldaputil/address_test.go
@@ -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)
+ }
+ }
+}
diff --git a/backend/internal/service/openvpn/openvpn.go b/backend/internal/service/openvpn/openvpn.go
index acdd189..3cc8a27 100644
--- a/backend/internal/service/openvpn/openvpn.go
+++ b/backend/internal/service/openvpn/openvpn.go
@@ -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
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 43c2071..7639977 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index d9e80b3..a1bee67 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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"
}
diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json
index 4301cc7..c7b1e59 100644
--- a/frontend/src/locales/en-US.json
+++ b/frontend/src/locales/en-US.json
@@ -73,7 +73,7 @@
"passwordLabel": "Password",
"passwordPlaceholder": "Enter password",
"loginButton": "Sign In",
- "defaultAccount": "Default account: admin / admin123 — 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 command hooks: the platform pipes tunnel config JSON to hook commands via stdin.",
- "pluginContract": "Contract: Create / Start / Stop / Status / Config; Status hook outputs running or stopped 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 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json
index eb9a87f..53283c6 100644
--- a/frontend/src/locales/zh-CN.json
+++ b/frontend/src/locales/zh-CN.json
@@ -73,7 +73,7 @@
"passwordLabel": "密码",
"passwordPlaceholder": "请输入密码",
"loginButton": "登 录",
- "defaultAccount": "默认账号 admin / admin123,登录后请立即修改",
+ "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": "插件通过命令钩子实现统一协议契约:平台将专线配置 JSON 通过 stdin 传给钩子命令。",
- "pluginContract": "契约:Create / Start / Stop / Status / Config;Status 钩子 stdout 输出 running 或 stopped;Config 钩子 stdout 输出客户端配置文本。",
+ "commandHooks": "命令钩子",
+ "pluginHint": "插件通过 {hooks} 实现统一协议契约:平台将专线配置 JSON 通过 stdin 传给钩子命令。",
+ "pluginContract": "契约:{contract};Status 钩子 stdout 输出 {running} 或 {stopped};Config 钩子 stdout 输出客户端配置文本。",
"protocolId": "协议标识",
"enabled": "启用",
"disabled": "停用",
@@ -1024,4 +1034,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
index 53dfca5..48c56af 100644
--- a/frontend/src/views/Login.vue
+++ b/frontend/src/views/Login.vue
@@ -72,7 +72,9 @@
Create / Start / Stop / Status / Config
+ running
+ stopped
+