mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Revert "Merge pull request #292 from iRubbish/dev"
This reverts commit9e8e74ce6b, reversing changes made to222f4e44fe.
This commit is contained in:
26
.env.example
26
.env.example
@@ -15,23 +15,6 @@ ENCRYPTION_KEY=your-encryption-key-here
|
|||||||
# ADMIN_USERNAME=cr_admin_custom
|
# ADMIN_USERNAME=cr_admin_custom
|
||||||
# ADMIN_PASSWORD=your-secure-password
|
# ADMIN_PASSWORD=your-secure-password
|
||||||
|
|
||||||
|
|
||||||
# 🏢 LDAP/Windows AD 域控认证配置(可选,用于企业内部用户登录)
|
|
||||||
# 启用LDAP认证功能
|
|
||||||
# LDAP_ENABLED=true
|
|
||||||
# AD域控服务器地址
|
|
||||||
# LDAP_URL=ldap://your-domain-controller-ip:389
|
|
||||||
# 绑定用户
|
|
||||||
# LDAP_BIND_DN=your-bind-user
|
|
||||||
# 绑定用户密码
|
|
||||||
# LDAP_BIND_PASSWORD=your-bind-password
|
|
||||||
# 搜索基础DN
|
|
||||||
# LDAP_BASE_DN=OU=YourOU,DC=your,DC=domain,DC=com
|
|
||||||
# 用户搜索过滤器
|
|
||||||
# LDAP_SEARCH_FILTER=(&(objectClass=user)(|(cn={username})(sAMAccountName={username})))
|
|
||||||
# 连接超时设置
|
|
||||||
# LDAP_TIMEOUT=10000
|
|
||||||
|
|
||||||
# 📊 Redis 配置
|
# 📊 Redis 配置
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
@@ -62,10 +45,8 @@ LOG_MAX_FILES=5
|
|||||||
CLEANUP_INTERVAL=3600000
|
CLEANUP_INTERVAL=3600000
|
||||||
TOKEN_USAGE_RETENTION=2592000000
|
TOKEN_USAGE_RETENTION=2592000000
|
||||||
HEALTH_CHECK_INTERVAL=60000
|
HEALTH_CHECK_INTERVAL=60000
|
||||||
SYSTEM_TIMEZONE=Asia/Shanghai
|
TIMEZONE_OFFSET=8 # UTC偏移小时数,默认+8(中国时区)
|
||||||
TIMEZONE_OFFSET=8
|
METRICS_WINDOW=5 # 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
||||||
# 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
|
||||||
METRICS_WINDOW=5
|
|
||||||
|
|
||||||
# 🎨 Web 界面配置
|
# 🎨 Web 界面配置
|
||||||
WEB_TITLE=Claude Relay Service
|
WEB_TITLE=Claude Relay Service
|
||||||
@@ -84,5 +65,4 @@ TRUST_PROXY=true
|
|||||||
WEBHOOK_ENABLED=true
|
WEBHOOK_ENABLED=true
|
||||||
WEBHOOK_URLS=https://your-webhook-url.com/notify,https://backup-webhook.com/notify
|
WEBHOOK_URLS=https://your-webhook-url.com/notify,https://backup-webhook.com/notify
|
||||||
WEBHOOK_TIMEOUT=10000
|
WEBHOOK_TIMEOUT=10000
|
||||||
WEBHOOK_RETRIES=3
|
WEBHOOK_RETRIES=3
|
||||||
|
|
||||||
@@ -250,15 +250,6 @@ REDIS_HOST=localhost
|
|||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
# AD域控配置(可选,用于企业内部用户登录)
|
|
||||||
LDAP_ENABLED=true
|
|
||||||
LDAP_URL=ldap://your-domain-controller-ip:389
|
|
||||||
LDAP_BIND_DN=your-bind-user
|
|
||||||
LDAP_BIND_PASSWORD=your-bind-password
|
|
||||||
LDAP_BASE_DN=DC=your-domain,DC=com
|
|
||||||
LDAP_SEARCH_FILTER=(&(objectClass=user)(|(cn={username})(sAMAccountName={username})))
|
|
||||||
LDAP_TIMEOUT=10000
|
|
||||||
|
|
||||||
# Webhook通知配置(可选)
|
# Webhook通知配置(可选)
|
||||||
WEBHOOK_ENABLED=true
|
WEBHOOK_ENABLED=true
|
||||||
WEBHOOK_URLS=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
|
WEBHOOK_URLS=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
|
||||||
|
|||||||
394
package-lock.json
generated
394
package-lock.json
generated
@@ -24,10 +24,7 @@
|
|||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"inquirer": "^8.2.6",
|
"inquirer": "^8.2.6",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"ldapjs": "^3.0.7",
|
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-fetch": "^2.7.0",
|
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"rate-limiter-flexible": "^5.0.5",
|
"rate-limiter-flexible": "^5.0.5",
|
||||||
"socks-proxy-agent": "^8.0.2",
|
"socks-proxy-agent": "^8.0.2",
|
||||||
@@ -2051,101 +2048,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ldapjs/asn1": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/attribute": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"process-warning": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/change": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"@ldapjs/attribute": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/controls": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "^1.2.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/dn": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"process-warning": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/filter": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"process-warning": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/messages": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "^2.0.0",
|
|
||||||
"@ldapjs/attribute": "^1.0.0",
|
|
||||||
"@ldapjs/change": "^1.0.0",
|
|
||||||
"@ldapjs/controls": "^2.1.0",
|
|
||||||
"@ldapjs/dn": "^1.1.0",
|
|
||||||
"@ldapjs/filter": "^2.1.1",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"process-warning": "^2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/protocol": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
@@ -3019,12 +2921,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/abstract-logging": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -3181,15 +3077,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/assert-plus": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/astral-regex": {
|
"node_modules/astral-regex": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||||
@@ -3338,18 +3225,6 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/backoff": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"precond": "0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -4037,12 +3912,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-util-is": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cors": {
|
"node_modules/cors": {
|
||||||
"version": "2.8.5",
|
"version": "2.8.5",
|
||||||
"resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
|
"resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
|
||||||
@@ -4095,7 +3964,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4764,15 +4633,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/extsprintf": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -4869,7 +4729,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/fetch-blob": {
|
"node_modules/fetch-blob": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -5050,7 +4910,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/formdata-polyfill": {
|
"node_modules/formdata-polyfill": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5139,24 +4999,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gaxios/node_modules/node-fetch": {
|
|
||||||
"version": "3.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
|
||||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"data-uri-to-buffer": "^4.0.0",
|
|
||||||
"fetch-blob": "^3.1.4",
|
|
||||||
"formdata-polyfill": "^4.0.10"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/node-fetch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gcp-metadata": {
|
"node_modules/gcp-metadata": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
|
||||||
@@ -6635,67 +6477,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsonwebtoken": {
|
|
||||||
"version": "9.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
|
||||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"jws": "^3.2.2",
|
|
||||||
"lodash.includes": "^4.3.0",
|
|
||||||
"lodash.isboolean": "^3.0.3",
|
|
||||||
"lodash.isinteger": "^4.0.4",
|
|
||||||
"lodash.isnumber": "^3.0.3",
|
|
||||||
"lodash.isplainobject": "^4.0.6",
|
|
||||||
"lodash.isstring": "^4.0.1",
|
|
||||||
"lodash.once": "^4.0.0",
|
|
||||||
"ms": "^2.1.1",
|
|
||||||
"semver": "^7.5.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12",
|
|
||||||
"npm": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsonwebtoken/node_modules/jwa": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"buffer-equal-constant-time": "^1.0.1",
|
|
||||||
"ecdsa-sig-formatter": "1.0.11",
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsonwebtoken/node_modules/jws": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
|
||||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"jwa": "^1.4.1",
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsonwebtoken/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/jsonwebtoken/node_modules/semver": {
|
|
||||||
"version": "7.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jwa": {
|
"node_modules/jwa": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
|
||||||
@@ -6743,29 +6524,6 @@
|
|||||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ldapjs": {
|
|
||||||
"version": "3.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz",
|
|
||||||
"integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "^2.0.0",
|
|
||||||
"@ldapjs/attribute": "^1.0.0",
|
|
||||||
"@ldapjs/change": "^1.0.0",
|
|
||||||
"@ldapjs/controls": "^2.1.0",
|
|
||||||
"@ldapjs/dn": "^1.1.0",
|
|
||||||
"@ldapjs/filter": "^2.1.1",
|
|
||||||
"@ldapjs/messages": "^1.3.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"abstract-logging": "^2.0.1",
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"backoff": "^2.5.0",
|
|
||||||
"once": "^1.4.0",
|
|
||||||
"vasync": "^2.2.1",
|
|
||||||
"verror": "^1.10.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
||||||
@@ -6825,48 +6583,12 @@
|
|||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.includes": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isarguments": {
|
"node_modules/lodash.isarguments": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isboolean": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isinteger": {
|
|
||||||
"version": "4.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
|
||||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isnumber": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isplainobject": {
|
|
||||||
"version": "4.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isstring": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -6874,12 +6596,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.once": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.truncate": {
|
"node_modules/lodash.truncate": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
||||||
@@ -7162,7 +6878,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/node-domexception": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
"deprecated": "Use your platform's native DOMException instead",
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
"funding": [
|
"funding": [
|
||||||
@@ -7181,23 +6897,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "4.x || >=6.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"funding": {
|
||||||
"encoding": "^0.1.0"
|
"type": "opencollective",
|
||||||
},
|
"url": "https://opencollective.com/node-fetch"
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
@@ -7382,6 +7096,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@@ -7686,14 +7401,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/precond": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -7761,12 +7468,6 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/process-warning": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/prompts": {
|
"node_modules/prompts": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz",
|
"resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz",
|
||||||
@@ -8876,12 +8577,6 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tr46": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/triple-beam": {
|
"node_modules/triple-beam": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz",
|
"resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||||
@@ -9062,46 +8757,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vasync": {
|
|
||||||
"version": "2.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"verror": "1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vasync/node_modules/verror": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"core-util-is": "1.0.2",
|
|
||||||
"extsprintf": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/verror": {
|
|
||||||
"version": "1.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
|
||||||
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"core-util-is": "1.0.2",
|
|
||||||
"extsprintf": "^1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/walker": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz",
|
||||||
@@ -9123,29 +8778,13 @@
|
|||||||
},
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
|
||||||
"license": "BSD-2-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "~0.0.3",
|
|
||||||
"webidl-conversions": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||||
@@ -9272,6 +8911,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
|
|||||||
@@ -63,10 +63,7 @@
|
|||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"inquirer": "^8.2.6",
|
"inquirer": "^8.2.6",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"ldapjs": "^3.0.7",
|
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-fetch": "^2.7.0",
|
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"rate-limiter-flexible": "^5.0.5",
|
"rate-limiter-flexible": "^5.0.5",
|
||||||
"socks-proxy-agent": "^8.0.2",
|
"socks-proxy-agent": "^8.0.2",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes')
|
|||||||
const openaiRoutes = require('./routes/openaiRoutes')
|
const openaiRoutes = require('./routes/openaiRoutes')
|
||||||
const azureOpenaiRoutes = require('./routes/azureOpenaiRoutes')
|
const azureOpenaiRoutes = require('./routes/azureOpenaiRoutes')
|
||||||
const webhookRoutes = require('./routes/webhook')
|
const webhookRoutes = require('./routes/webhook')
|
||||||
const ldapRoutes = require('./routes/ldapRoutes')
|
|
||||||
|
|
||||||
// Import middleware
|
// Import middleware
|
||||||
const {
|
const {
|
||||||
@@ -245,7 +244,6 @@ class Application {
|
|||||||
this.app.use('/openai', openaiRoutes)
|
this.app.use('/openai', openaiRoutes)
|
||||||
this.app.use('/azure', azureOpenaiRoutes)
|
this.app.use('/azure', azureOpenaiRoutes)
|
||||||
this.app.use('/admin/webhook', webhookRoutes)
|
this.app.use('/admin/webhook', webhookRoutes)
|
||||||
this.app.use('/admin/ldap', ldapRoutes)
|
|
||||||
|
|
||||||
// 🏠 根路径重定向到新版管理界面
|
// 🏠 根路径重定向到新版管理界面
|
||||||
this.app.get('/', (req, res) => {
|
this.app.get('/', (req, res) => {
|
||||||
|
|||||||
@@ -791,8 +791,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { keyId } = req.params
|
const { keyId } = req.params
|
||||||
const {
|
const {
|
||||||
name,
|
|
||||||
description,
|
|
||||||
tokenLimit,
|
tokenLimit,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
@@ -816,30 +814,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
// 只允许更新指定字段
|
// 只允许更新指定字段
|
||||||
const updates = {}
|
const updates = {}
|
||||||
|
|
||||||
// 处理name字段
|
|
||||||
if (name !== undefined) {
|
|
||||||
if (name === null || name === '') {
|
|
||||||
return res.status(400).json({ error: 'Name cannot be empty' })
|
|
||||||
}
|
|
||||||
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
||||||
return res.status(400).json({ error: 'Name must be a non-empty string' })
|
|
||||||
}
|
|
||||||
if (name.length > 100) {
|
|
||||||
return res.status(400).json({ error: 'Name must be less than 100 characters' })
|
|
||||||
}
|
|
||||||
updates.name = name.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理description字段
|
|
||||||
if (description !== undefined) {
|
|
||||||
if (description && (typeof description !== 'string' || description.length > 500)) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ error: 'Description must be a string with less than 500 characters' })
|
|
||||||
}
|
|
||||||
updates.description = description || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
|
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
|
||||||
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
|
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
|
||||||
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
|
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
|
||||||
@@ -980,20 +954,12 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.isActive = isActive
|
updates.isActive = isActive
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`🔧 Admin updating API key: ${keyId}`, {
|
|
||||||
updates: Object.keys(updates),
|
|
||||||
updatesData: updates
|
|
||||||
})
|
|
||||||
|
|
||||||
await apiKeyService.updateApiKey(keyId, updates)
|
await apiKeyService.updateApiKey(keyId, updates)
|
||||||
|
|
||||||
logger.success(`📝 Admin updated API key: ${keyId}`)
|
logger.success(`📝 Admin updated API key: ${keyId}`)
|
||||||
return res.json({ success: true, message: 'API key updated successfully' })
|
return res.json({ success: true, message: 'API key updated successfully' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to update API key ${req.params.keyId}:`, {
|
logger.error('❌ Failed to update API key:', error)
|
||||||
error: error.message,
|
|
||||||
stack: error.stack
|
|
||||||
})
|
|
||||||
return res.status(500).json({ error: 'Failed to update API key', message: error.message })
|
return res.status(500).json({ error: 'Failed to update API key', message: error.message })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,689 +0,0 @@
|
|||||||
const express = require('express')
|
|
||||||
const ldapService = require('../services/ldapService')
|
|
||||||
const logger = require('../utils/logger')
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试LDAP/AD连接
|
|
||||||
*/
|
|
||||||
router.get('/test-connection', async (req, res) => {
|
|
||||||
try {
|
|
||||||
logger.info('LDAP connection test requested')
|
|
||||||
const result = await ldapService.testConnection()
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'LDAP/AD connection successful',
|
|
||||||
data: result
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'LDAP/AD connection failed',
|
|
||||||
error: result.error,
|
|
||||||
config: result.config
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('LDAP connection test error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'LDAP connection test failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取LDAP配置信息
|
|
||||||
*/
|
|
||||||
router.get('/config', (req, res) => {
|
|
||||||
try {
|
|
||||||
const config = ldapService.getConfig()
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
config
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Get LDAP config error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Failed to get LDAP config',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索用户
|
|
||||||
*/
|
|
||||||
router.post('/search-user', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username } = req.body
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Username is required'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Searching for user: ${username}`)
|
|
||||||
|
|
||||||
await ldapService.createConnection()
|
|
||||||
await ldapService.bind()
|
|
||||||
|
|
||||||
const users = await ldapService.searchUser(username)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: `Found ${users.length} users`,
|
|
||||||
users
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('User search error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'User search failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
ldapService.disconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列出所有用户(模拟Python代码的describe_ou功能)
|
|
||||||
*/
|
|
||||||
router.get('/list-users', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { limit = 20, type = 'human' } = req.query
|
|
||||||
const limitNum = parseInt(limit)
|
|
||||||
|
|
||||||
logger.info(`Listing users with limit: ${limitNum}, type: ${type}`)
|
|
||||||
|
|
||||||
await ldapService.createConnection()
|
|
||||||
await ldapService.bind()
|
|
||||||
|
|
||||||
const users = await ldapService.listAllUsers(limitNum, type)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: `Found ${users.length} users`,
|
|
||||||
users,
|
|
||||||
total: users.length,
|
|
||||||
limit: limitNum,
|
|
||||||
type
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('List users error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'List users failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
ldapService.disconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试用户认证
|
|
||||||
*/
|
|
||||||
router.post('/test-auth', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username, password } = req.body
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Username and password are required'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Testing authentication for user: ${username}`)
|
|
||||||
|
|
||||||
const result = await ldapService.authenticateUser(username, password)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'Authentication successful',
|
|
||||||
user: result.user
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('User authentication test error:', error)
|
|
||||||
res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Authentication failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列出所有OU
|
|
||||||
*/
|
|
||||||
router.get('/list-ous', async (req, res) => {
|
|
||||||
try {
|
|
||||||
logger.info('Listing all OUs in domain')
|
|
||||||
|
|
||||||
await ldapService.createConnection()
|
|
||||||
await ldapService.bind()
|
|
||||||
|
|
||||||
const ous = await ldapService.listOUs()
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: `Found ${ous.length} OUs`,
|
|
||||||
ous
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('List OUs error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'List OUs failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
ldapService.disconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证OU是否存在
|
|
||||||
*/
|
|
||||||
router.get('/verify-ou', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const defaultOU = process.env.LDAP_DEFAULT_OU || 'YourOU'
|
|
||||||
const { ou = defaultOU } = req.query
|
|
||||||
// 使用配置的baseDN来构建测试DN,而不是硬编码域名
|
|
||||||
const config = ldapService.getConfig()
|
|
||||||
// 从baseDN中提取域部分,替换OU部分
|
|
||||||
const baseDNParts = config.baseDN.split(',')
|
|
||||||
const domainParts = baseDNParts.filter((part) => part.trim().startsWith('DC='))
|
|
||||||
const testDN = `OU=${ou},${domainParts.join(',')}`
|
|
||||||
|
|
||||||
logger.info(`Verifying OU exists: ${testDN}`)
|
|
||||||
|
|
||||||
await ldapService.createConnection()
|
|
||||||
await ldapService.bind()
|
|
||||||
|
|
||||||
const result = await ldapService.verifyOU(testDN)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'OU verification completed',
|
|
||||||
testDN,
|
|
||||||
result
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('OU verification error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'OU verification failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
ldapService.disconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LDAP服务状态检查
|
|
||||||
*/
|
|
||||||
router.get('/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const config = ldapService.getConfig()
|
|
||||||
|
|
||||||
// 简单的连接测试
|
|
||||||
const connectionTest = await ldapService.testConnection()
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
status: connectionTest.success ? 'connected' : 'disconnected',
|
|
||||||
config,
|
|
||||||
lastTest: new Date().toISOString(),
|
|
||||||
testResult: connectionTest
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('LDAP status check error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
status: 'error',
|
|
||||||
message: 'Status check failed',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AD用户登录认证
|
|
||||||
*/
|
|
||||||
router.post('/login', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username, password } = req.body
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: '用户名和密码不能为空'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`AD用户登录尝试: ${username}`)
|
|
||||||
|
|
||||||
// 使用AD认证用户
|
|
||||||
const authResult = await ldapService.authenticateUser(username, password)
|
|
||||||
|
|
||||||
// 生成用户会话token
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const config = require('../../config/config')
|
|
||||||
|
|
||||||
const userInfo = {
|
|
||||||
type: 'ad_user',
|
|
||||||
username: authResult.user.username || authResult.user.cn,
|
|
||||||
displayName: authResult.user.displayName,
|
|
||||||
email: authResult.user.email,
|
|
||||||
groups: authResult.user.groups,
|
|
||||||
loginTime: new Date().toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = jwt.sign(userInfo, config.security.jwtSecret, {
|
|
||||||
expiresIn: '8h' // 8小时过期
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`AD用户登录成功: ${username}`)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '登录成功',
|
|
||||||
token,
|
|
||||||
user: userInfo
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('AD用户登录失败:', error)
|
|
||||||
res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
message: '用户名或密码错误',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AD用户token验证
|
|
||||||
*/
|
|
||||||
router.get('/verify-token', (req, res) => {
|
|
||||||
try {
|
|
||||||
const authHeader = req.headers.authorization
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
message: '未提供有效的认证token'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7)
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const config = require('../../config/config')
|
|
||||||
|
|
||||||
const decoded = jwt.verify(token, config.security.jwtSecret)
|
|
||||||
|
|
||||||
if (decoded.type !== 'ad_user') {
|
|
||||||
return res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: '无效的用户类型'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
user: decoded
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Token验证失败:', error)
|
|
||||||
res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Token无效或已过期'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AD用户认证中间件
|
|
||||||
*/
|
|
||||||
const authenticateUser = (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const authHeader = req.headers.authorization
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
message: '未提供有效的认证token'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7)
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const config = require('../../config/config')
|
|
||||||
|
|
||||||
const decoded = jwt.verify(token, config.security.jwtSecret)
|
|
||||||
|
|
||||||
if (decoded.type !== 'ad_user') {
|
|
||||||
return res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: '无效的用户类型'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = decoded
|
|
||||||
next()
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('用户认证失败:', error)
|
|
||||||
res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Token无效或已过期'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户的API Keys
|
|
||||||
*
|
|
||||||
* 自动关联逻辑说明:
|
|
||||||
* 系统迁移过程中存在历史API Key,这些Key是在AD集成前手动创建的
|
|
||||||
* 创建时使用的name字段恰好与AD用户的displayName一致
|
|
||||||
* 例如: AD用户displayName为"测试用户",对应的API Key name也是"测试用户"
|
|
||||||
* 为了避免用户重复创建Key,系统会自动关联这些历史Key
|
|
||||||
* 关联规则:
|
|
||||||
* 1. 优先匹配owner字段(新建的Key)
|
|
||||||
* 2. 如果没有owner匹配,则尝试匹配name字段与displayName
|
|
||||||
* 3. 找到匹配的历史Key后,自动将owner设置为当前用户,完成关联
|
|
||||||
*/
|
|
||||||
router.get('/user/api-keys', authenticateUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const apiKeyService = require('../services/apiKeyService')
|
|
||||||
const redis = require('../models/redis')
|
|
||||||
const { username, displayName } = req.user
|
|
||||||
|
|
||||||
logger.info(`获取用户API Keys: ${username}, displayName: ${displayName}`)
|
|
||||||
|
|
||||||
// 使用与admin相同的API Key服务,获取所有API Keys的完整信息
|
|
||||||
const allApiKeys = await apiKeyService.getAllApiKeys()
|
|
||||||
|
|
||||||
const userKeys = []
|
|
||||||
let foundHistoricalKey = false
|
|
||||||
|
|
||||||
// 筛选属于该用户的API Keys,并处理自动关联
|
|
||||||
for (const apiKey of allApiKeys) {
|
|
||||||
logger.debug(
|
|
||||||
`检查API Key: ${apiKey.id}, name: "${apiKey.name}", owner: "${apiKey.owner || '无'}", displayName: "${displayName}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// 规则1: 直接owner匹配(已关联的Key)
|
|
||||||
if (apiKey.owner === username) {
|
|
||||||
logger.info(`找到已关联的API Key: ${apiKey.id}`)
|
|
||||||
userKeys.push(apiKey)
|
|
||||||
}
|
|
||||||
// 规则2: 历史Key自动关联(name字段匹配displayName且无owner)
|
|
||||||
else if (displayName && apiKey.name === displayName && !apiKey.owner) {
|
|
||||||
logger.info(
|
|
||||||
`🔗 发现历史API Key需要关联: id=${apiKey.id}, name="${apiKey.name}", displayName="${displayName}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// 自动关联: 设置owner为当前用户
|
|
||||||
await redis.getClient().hset(`apikey:${apiKey.id}`, 'owner', username)
|
|
||||||
foundHistoricalKey = true
|
|
||||||
|
|
||||||
// 更新本地数据并添加到用户Key列表
|
|
||||||
apiKey.owner = username
|
|
||||||
userKeys.push(apiKey)
|
|
||||||
|
|
||||||
logger.info(`✅ 历史API Key关联成功: ${apiKey.id} -> ${username}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundHistoricalKey) {
|
|
||||||
logger.info(`用户 ${username} 自动关联了历史API Key`)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
apiKeys: userKeys
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('获取用户API Keys失败:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '获取API Keys失败'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建用户API Key
|
|
||||||
*/
|
|
||||||
router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username } = req.user
|
|
||||||
// 用户创建的API Key不需要任何输入参数,都使用默认值
|
|
||||||
// const { limit } = req.body // 不再从请求体获取limit
|
|
||||||
|
|
||||||
// 检查用户是否已有API Key
|
|
||||||
const redis = require('../models/redis')
|
|
||||||
const allKeysPattern = 'apikey:*'
|
|
||||||
const keys = await redis.getClient().keys(allKeysPattern)
|
|
||||||
|
|
||||||
let userKeyCount = 0
|
|
||||||
for (const key of keys) {
|
|
||||||
const apiKeyData = await redis.getClient().hgetall(key)
|
|
||||||
if (apiKeyData && apiKeyData.owner === username) {
|
|
||||||
userKeyCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userKeyCount >= 1) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: '每个用户只能创建一个API Key'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用与admin相同的API Key生成服务,确保数据结构一致性
|
|
||||||
const apiKeyService = require('../services/apiKeyService')
|
|
||||||
|
|
||||||
// 获取用户的显示名称
|
|
||||||
const { displayName } = req.user
|
|
||||||
// 用户创建的API Key名称固定为displayName,不允许自定义
|
|
||||||
const defaultName = displayName || username
|
|
||||||
|
|
||||||
const keyParams = {
|
|
||||||
name: defaultName, // 使用displayName作为API Key名称
|
|
||||||
tokenLimit: 0, // 固定为无限制
|
|
||||||
description: `AD用户${username}创建的API Key`,
|
|
||||||
// AD用户创建的Key添加owner信息以区分用户归属
|
|
||||||
owner: username,
|
|
||||||
ownerType: 'ad_user',
|
|
||||||
// 确保用户创建的Key默认激活
|
|
||||||
isActive: true,
|
|
||||||
// 设置基本权限(与admin创建保持一致)
|
|
||||||
permissions: 'all',
|
|
||||||
// 设置合理的并发和速率限制(与admin创建保持一致)
|
|
||||||
concurrencyLimit: 0,
|
|
||||||
rateLimitWindow: 0,
|
|
||||||
rateLimitRequests: 0,
|
|
||||||
// 添加标签标识AD用户创建
|
|
||||||
tags: ['ad-user', 'user-created']
|
|
||||||
}
|
|
||||||
|
|
||||||
const newKey = await apiKeyService.generateApiKey(keyParams)
|
|
||||||
|
|
||||||
logger.info(`用户${username}创建API Key成功: ${newKey.id}`)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'API Key创建成功',
|
|
||||||
apiKey: {
|
|
||||||
id: newKey.id,
|
|
||||||
key: newKey.apiKey, // 返回完整的API Key
|
|
||||||
name: newKey.name,
|
|
||||||
tokenLimit: newKey.tokenLimit || 0,
|
|
||||||
used: 0,
|
|
||||||
createdAt: newKey.createdAt,
|
|
||||||
isActive: true,
|
|
||||||
usage: {
|
|
||||||
daily: { requests: 0, tokens: 0 },
|
|
||||||
total: { requests: 0, tokens: 0 }
|
|
||||||
},
|
|
||||||
dailyCost: 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('创建用户API Key失败:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '创建API Key失败'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户API Key使用统计
|
|
||||||
*/
|
|
||||||
router.get('/user/usage-stats', authenticateUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username } = req.user
|
|
||||||
const redis = require('../models/redis')
|
|
||||||
|
|
||||||
// 获取用户的API Keys
|
|
||||||
const allKeysPattern = 'apikey:*'
|
|
||||||
const keys = await redis.getClient().keys(allKeysPattern)
|
|
||||||
|
|
||||||
let totalUsage = 0
|
|
||||||
let totalLimit = 0
|
|
||||||
const userKeys = []
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const apiKeyData = await redis.getClient().hgetall(key)
|
|
||||||
if (apiKeyData && apiKeyData.owner === username) {
|
|
||||||
const used = parseInt(apiKeyData.used) || 0
|
|
||||||
const limit = parseInt(apiKeyData.limit) || 0
|
|
||||||
|
|
||||||
totalUsage += used
|
|
||||||
totalLimit += limit
|
|
||||||
|
|
||||||
userKeys.push({
|
|
||||||
id: apiKeyData.id,
|
|
||||||
name: apiKeyData.name,
|
|
||||||
used,
|
|
||||||
limit,
|
|
||||||
percentage: limit > 0 ? Math.round((used / limit) * 100) : 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
stats: {
|
|
||||||
totalUsage,
|
|
||||||
totalLimit,
|
|
||||||
percentage: totalLimit > 0 ? Math.round((totalUsage / totalLimit) * 100) : 0,
|
|
||||||
keyCount: userKeys.length,
|
|
||||||
keys: userKeys
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('获取用户使用统计失败:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '获取使用统计失败'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新用户API Key
|
|
||||||
*/
|
|
||||||
router.put('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username } = req.user
|
|
||||||
const { keyId } = req.params
|
|
||||||
const updates = req.body
|
|
||||||
|
|
||||||
// 验证用户只能编辑自己的API Key
|
|
||||||
const apiKeyService = require('../services/apiKeyService')
|
|
||||||
const allApiKeys = await apiKeyService.getAllApiKeys()
|
|
||||||
const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username)
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: 'API Key 不存在或无权限'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制用户只能修改特定字段(不允许修改name)
|
|
||||||
const allowedFields = ['description', 'isActive']
|
|
||||||
const filteredUpdates = {}
|
|
||||||
for (const [key, value] of Object.entries(updates)) {
|
|
||||||
if (allowedFields.includes(key)) {
|
|
||||||
filteredUpdates[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await apiKeyService.updateApiKey(keyId, filteredUpdates)
|
|
||||||
|
|
||||||
logger.info(`用户 ${username} 更新了 API Key: ${keyId}`)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'API Key 更新成功'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('更新用户API Key失败:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '更新 API Key 失败'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除用户API Key
|
|
||||||
*/
|
|
||||||
router.delete('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username } = req.user
|
|
||||||
const { keyId } = req.params
|
|
||||||
|
|
||||||
// 验证用户只能删除自己的API Key
|
|
||||||
const apiKeyService = require('../services/apiKeyService')
|
|
||||||
const allApiKeys = await apiKeyService.getAllApiKeys()
|
|
||||||
const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username)
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: 'API Key 不存在或无权限'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await apiKeyService.deleteApiKey(keyId)
|
|
||||||
|
|
||||||
logger.info(`用户 ${username} 删除了 API Key: ${keyId}`)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'API Key 删除成功'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('删除用户API Key失败:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '删除 API Key 失败'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
@@ -32,9 +32,7 @@ class ApiKeyService {
|
|||||||
enableClientRestriction = false,
|
enableClientRestriction = false,
|
||||||
allowedClients = [],
|
allowedClients = [],
|
||||||
dailyCostLimit = 0,
|
dailyCostLimit = 0,
|
||||||
tags = [],
|
tags = []
|
||||||
owner = null,
|
|
||||||
ownerType = null
|
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 生成简单的API Key (64字符十六进制)
|
// 生成简单的API Key (64字符十六进制)
|
||||||
@@ -68,9 +66,7 @@ class ApiKeyService {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastUsedAt: '',
|
lastUsedAt: '',
|
||||||
expiresAt: expiresAt || '',
|
expiresAt: expiresAt || '',
|
||||||
createdBy: 'admin', // 可以根据需要扩展用户系统
|
createdBy: 'admin' // 可以根据需要扩展用户系统
|
||||||
owner: owner || '',
|
|
||||||
ownerType: ownerType || ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存API Key数据并建立哈希映射
|
// 保存API Key数据并建立哈希映射
|
||||||
@@ -103,9 +99,7 @@ class ApiKeyService {
|
|||||||
tags: JSON.parse(keyData.tags || '[]'),
|
tags: JSON.parse(keyData.tags || '[]'),
|
||||||
createdAt: keyData.createdAt,
|
createdAt: keyData.createdAt,
|
||||||
expiresAt: keyData.expiresAt,
|
expiresAt: keyData.expiresAt,
|
||||||
createdBy: keyData.createdBy,
|
createdBy: keyData.createdBy
|
||||||
owner: keyData.owner,
|
|
||||||
ownerType: keyData.ownerType
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,21 +294,11 @@ class ApiKeyService {
|
|||||||
// 📝 更新API Key
|
// 📝 更新API Key
|
||||||
async updateApiKey(keyId, updates) {
|
async updateApiKey(keyId, updates) {
|
||||||
try {
|
try {
|
||||||
logger.debug(`🔧 Updating API key ${keyId} with:`, updates)
|
|
||||||
|
|
||||||
const keyData = await redis.getApiKey(keyId)
|
const keyData = await redis.getApiKey(keyId)
|
||||||
if (!keyData || Object.keys(keyData).length === 0) {
|
if (!keyData || Object.keys(keyData).length === 0) {
|
||||||
logger.error(`❌ API key not found: ${keyId}`)
|
|
||||||
throw new Error('API key not found')
|
throw new Error('API key not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`📋 Current API key data:`, {
|
|
||||||
id: keyData.id,
|
|
||||||
name: keyData.name,
|
|
||||||
owner: keyData.owner,
|
|
||||||
ownerType: keyData.ownerType
|
|
||||||
})
|
|
||||||
|
|
||||||
// 允许更新的字段
|
// 允许更新的字段
|
||||||
const allowedUpdates = [
|
const allowedUpdates = [
|
||||||
'name',
|
'name',
|
||||||
@@ -360,10 +344,7 @@ class ApiKeyService {
|
|||||||
// 更新时不需要重新建立哈希映射,因为API Key本身没有变化
|
// 更新时不需要重新建立哈希映射,因为API Key本身没有变化
|
||||||
await redis.setApiKey(keyId, updatedData)
|
await redis.setApiKey(keyId, updatedData)
|
||||||
|
|
||||||
logger.success(`📝 Updated API key: ${keyId}`, {
|
logger.success(`📝 Updated API key: ${keyId}`)
|
||||||
updatedFields: Object.keys(updates),
|
|
||||||
newName: updatedData.name
|
|
||||||
})
|
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,761 +0,0 @@
|
|||||||
const ldap = require('ldapjs')
|
|
||||||
const logger = require('../utils/logger')
|
|
||||||
|
|
||||||
class LDAPService {
|
|
||||||
constructor() {
|
|
||||||
this.client = null
|
|
||||||
|
|
||||||
// 检查必需的LDAP配置
|
|
||||||
if (
|
|
||||||
!process.env.LDAP_URL ||
|
|
||||||
!process.env.LDAP_BIND_DN ||
|
|
||||||
!process.env.LDAP_BIND_PASSWORD ||
|
|
||||||
!process.env.LDAP_BASE_DN
|
|
||||||
) {
|
|
||||||
logger.warn('⚠️ LDAP配置不完整,请检查.env文件中的LDAP配置项')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.config = {
|
|
||||||
url: process.env.LDAP_URL || '',
|
|
||||||
bindDN: process.env.LDAP_BIND_DN || '',
|
|
||||||
bindPassword: process.env.LDAP_BIND_PASSWORD || '',
|
|
||||||
baseDN: process.env.LDAP_BASE_DN || '',
|
|
||||||
searchFilter: process.env.LDAP_SEARCH_FILTER || '(&(objectClass=user)(cn={username}))',
|
|
||||||
timeout: parseInt(process.env.LDAP_TIMEOUT) || 10000,
|
|
||||||
connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建LDAP连接
|
|
||||||
*/
|
|
||||||
createConnection() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const options = {
|
|
||||||
url: this.config.url,
|
|
||||||
timeout: this.config.timeout,
|
|
||||||
connectTimeout: this.config.connectTimeout,
|
|
||||||
reconnect: false,
|
|
||||||
// 匹配Python代码中的设置:禁用referrals
|
|
||||||
followReferrals: false,
|
|
||||||
// LDAP协议版本3
|
|
||||||
version: 3,
|
|
||||||
// 增加兼容性选项
|
|
||||||
strictDN: false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client = ldap.createClient(options)
|
|
||||||
|
|
||||||
// 连接超时处理
|
|
||||||
const timeoutTimer = setTimeout(() => {
|
|
||||||
this.client.destroy()
|
|
||||||
reject(new Error(`LDAP connection timeout after ${this.config.connectTimeout}ms`))
|
|
||||||
}, this.config.connectTimeout)
|
|
||||||
|
|
||||||
// 连接成功
|
|
||||||
this.client.on('connect', () => {
|
|
||||||
clearTimeout(timeoutTimer)
|
|
||||||
logger.info('LDAP connection established successfully')
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 连接错误
|
|
||||||
this.client.on('error', (err) => {
|
|
||||||
clearTimeout(timeoutTimer)
|
|
||||||
logger.error('LDAP connection error:', err)
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 连接关闭
|
|
||||||
this.client.on('close', () => {
|
|
||||||
logger.info('LDAP connection closed')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定LDAP连接(认证)
|
|
||||||
*/
|
|
||||||
bind() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.client) {
|
|
||||||
return reject(new Error('LDAP client not initialized'))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.bind(this.config.bindDN, this.config.bindPassword, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error('LDAP bind failed:', err)
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
logger.info('LDAP bind successful')
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试AD域控连接
|
|
||||||
*/
|
|
||||||
async testConnection() {
|
|
||||||
try {
|
|
||||||
logger.info('Testing LDAP/AD connection...')
|
|
||||||
logger.info(`Connecting to: ${this.config.url}`)
|
|
||||||
logger.info(`Bind DN: ${this.config.bindDN}`)
|
|
||||||
logger.info(`Base DN: ${this.config.baseDN}`)
|
|
||||||
|
|
||||||
await this.createConnection()
|
|
||||||
await this.bind()
|
|
||||||
|
|
||||||
// 先测试连接和绑定是否真的成功
|
|
||||||
logger.info('LDAP connection and bind successful')
|
|
||||||
|
|
||||||
// 尝试简单的根 DSE 查询来验证连接
|
|
||||||
let searchResult = null
|
|
||||||
try {
|
|
||||||
searchResult = await this.testRootDSE()
|
|
||||||
logger.info('Root DSE query successful')
|
|
||||||
} catch (searchError) {
|
|
||||||
logger.warn('Root DSE query failed, trying base search:', searchError.message)
|
|
||||||
try {
|
|
||||||
searchResult = await this.testSearch()
|
|
||||||
} catch (baseSearchError) {
|
|
||||||
logger.warn('Base search also failed:', baseSearchError.message)
|
|
||||||
// 连接成功但搜索失败,仍然返回部分成功
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message:
|
|
||||||
'LDAP connection and authentication successful, but search requires DN adjustment',
|
|
||||||
connectionTest: 'SUCCESS',
|
|
||||||
authTest: 'SUCCESS',
|
|
||||||
searchTest: `FAILED - ${baseSearchError.message}`,
|
|
||||||
config: {
|
|
||||||
url: this.config.url,
|
|
||||||
bindDN: this.config.bindDN,
|
|
||||||
baseDN: this.config.baseDN,
|
|
||||||
searchFilter: this.config.searchFilter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('LDAP/AD full connection test successful')
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'LDAP/AD connection test successful',
|
|
||||||
connectionTest: 'SUCCESS',
|
|
||||||
authTest: 'SUCCESS',
|
|
||||||
searchTest: 'SUCCESS',
|
|
||||||
config: {
|
|
||||||
url: this.config.url,
|
|
||||||
bindDN: this.config.bindDN,
|
|
||||||
baseDN: this.config.baseDN,
|
|
||||||
searchFilter: this.config.searchFilter
|
|
||||||
},
|
|
||||||
searchResult
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('LDAP/AD connection test failed:', error)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `LDAP/AD connection test failed: ${error.message}`,
|
|
||||||
error: error.message,
|
|
||||||
connectionTest: error.message.includes('connect') ? 'FAILED' : 'UNKNOWN',
|
|
||||||
authTest:
|
|
||||||
error.message.includes('bind') || error.message.includes('authentication')
|
|
||||||
? 'FAILED'
|
|
||||||
: 'UNKNOWN',
|
|
||||||
config: {
|
|
||||||
url: this.config.url,
|
|
||||||
bindDN: this.config.bindDN,
|
|
||||||
baseDN: this.config.baseDN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试根DSE查询(最基本的LDAP查询)
|
|
||||||
*/
|
|
||||||
testRootDSE() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const searchOptions = {
|
|
||||||
filter: '(objectClass=*)',
|
|
||||||
scope: 'base',
|
|
||||||
attributes: ['*']
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.search('', searchOptions, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootDSE = null
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
rootDSE = {
|
|
||||||
dn: entry.dn,
|
|
||||||
namingContexts: entry.object?.namingContexts || entry.attributes?.namingContexts,
|
|
||||||
supportedLDAPVersion:
|
|
||||||
entry.object?.supportedLDAPVersion || entry.attributes?.supportedLDAPVersion,
|
|
||||||
defaultNamingContext:
|
|
||||||
entry.object?.defaultNamingContext || entry.attributes?.defaultNamingContext,
|
|
||||||
raw: entry.object || entry.attributes
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('referral', (referral) => {
|
|
||||||
logger.info(`Root DSE referral: ${referral}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('error', (error) => {
|
|
||||||
if (error.message && error.message.toLowerCase().includes('referral')) {
|
|
||||||
logger.warn(`Root DSE referral error (ignored): ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
if (rootDSE) {
|
|
||||||
logger.info('Root DSE query completed successfully')
|
|
||||||
resolve(rootDSE)
|
|
||||||
} else {
|
|
||||||
resolve({ message: 'No Root DSE data returned' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行测试搜索
|
|
||||||
*/
|
|
||||||
testSearch() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// 匹配Python代码的搜索:查找用户对象,获取CN和userAccountControl属性
|
|
||||||
const searchOptions = {
|
|
||||||
filter: '(objectClass=user)',
|
|
||||||
scope: 'sub', // SCOPE_SUBTREE in Python
|
|
||||||
attributes: ['CN', 'userAccountControl'],
|
|
||||||
sizeLimit: 10 // 限制结果数量
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.search(this.config.baseDN, searchOptions, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let entryCount = 0
|
|
||||||
const entries = []
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
entryCount++
|
|
||||||
entries.push({
|
|
||||||
dn: entry.dn,
|
|
||||||
cn: entry.object.CN || entry.object.cn,
|
|
||||||
userAccountControl: entry.object.userAccountControl
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('referral', (referral) => {
|
|
||||||
// 记录referral但不作为错误处理
|
|
||||||
logger.info(`LDAP referral received: ${referral}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('error', (error) => {
|
|
||||||
// 如果是referral相关错误,不视为失败
|
|
||||||
if (error.message && error.message.toLowerCase().includes('referral')) {
|
|
||||||
logger.warn(`LDAP referral error (ignored): ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('end', (result) => {
|
|
||||||
logger.info(
|
|
||||||
`Search test completed. Found ${entryCount} entries, status: ${result.status}`
|
|
||||||
)
|
|
||||||
resolve({
|
|
||||||
entryCount,
|
|
||||||
status: result.status,
|
|
||||||
entries: entries.slice(0, 5)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据用户名搜索用户
|
|
||||||
*/
|
|
||||||
searchUser(username) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.client) {
|
|
||||||
return reject(new Error('LDAP client not initialized'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const filter = this.config.searchFilter.replace(/{username}/g, username)
|
|
||||||
const searchOptions = {
|
|
||||||
filter,
|
|
||||||
scope: 'sub',
|
|
||||||
attributes: [
|
|
||||||
'dn',
|
|
||||||
'sAMAccountName',
|
|
||||||
'displayName',
|
|
||||||
'mail',
|
|
||||||
'memberOf',
|
|
||||||
'cn',
|
|
||||||
'userAccountControl'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Searching for user: ${username}, Filter: ${filter}`)
|
|
||||||
|
|
||||||
this.client.search(this.config.baseDN, searchOptions, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = []
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
const obj = entry.object || {}
|
|
||||||
const attrs = entry.attributes || []
|
|
||||||
|
|
||||||
// 创建属性查找函数
|
|
||||||
const getAttr = (name) => {
|
|
||||||
if (obj[name]) {
|
|
||||||
return obj[name]
|
|
||||||
}
|
|
||||||
const attr = attrs.find((a) => a.type === name)
|
|
||||||
return attr ? (Array.isArray(attr.values) ? attr.values[0] : attr.values) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
dn: entry.dn,
|
|
||||||
username: getAttr('sAMAccountName'),
|
|
||||||
displayName: getAttr('displayName'),
|
|
||||||
email: getAttr('mail'),
|
|
||||||
cn: getAttr('cn'),
|
|
||||||
userAccountControl: getAttr('userAccountControl'),
|
|
||||||
groups: (() => {
|
|
||||||
const memberOf = getAttr('memberOf')
|
|
||||||
return Array.isArray(memberOf) ? memberOf : memberOf ? [memberOf] : []
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
users.push(user)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('referral', (referral) => {
|
|
||||||
logger.info(`LDAP referral received during user search: ${referral}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('error', (error) => {
|
|
||||||
if (error.message && error.message.toLowerCase().includes('referral')) {
|
|
||||||
logger.warn(`LDAP referral error during user search (ignored): ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
logger.info(`Found ${users.length} users for username: ${username}`)
|
|
||||||
resolve(users)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列出所有用户(模拟Python代码的describe_ou功能)
|
|
||||||
*/
|
|
||||||
listAllUsers(limit = 20, type = 'human') {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.client) {
|
|
||||||
return reject(new Error('LDAP client not initialized'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据类型选择不同的搜索过滤器
|
|
||||||
let filter
|
|
||||||
if (type === 'computer') {
|
|
||||||
// 只显示计算机账户
|
|
||||||
filter = '(&(objectClass=user)(sAMAccountName=*$))'
|
|
||||||
} else if (type === 'human') {
|
|
||||||
// 只显示人员账户(排除计算机账户)
|
|
||||||
filter = '(&(objectClass=user)(!(sAMAccountName=*$)))'
|
|
||||||
} else {
|
|
||||||
// 显示所有用户
|
|
||||||
filter = '(objectClass=user)'
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchOptions = {
|
|
||||||
filter,
|
|
||||||
scope: 'sub', // SCOPE_SUBTREE
|
|
||||||
attributes: ['CN', 'userAccountControl', 'sAMAccountName', 'displayName', 'mail', 'dn']
|
|
||||||
// 不使用 sizeLimit,而是在客户端限制结果数量
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Listing all users with filter: ${searchOptions.filter}, limit: ${limit}`)
|
|
||||||
|
|
||||||
this.client.search(this.config.baseDN, searchOptions, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = []
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
// 如果已经达到限制,停止处理
|
|
||||||
if (users.length >= limit) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = entry.object || {}
|
|
||||||
const attrs = entry.attributes || []
|
|
||||||
|
|
||||||
// 创建属性查找函数
|
|
||||||
const getAttr = (name) => {
|
|
||||||
if (obj[name]) {
|
|
||||||
return obj[name]
|
|
||||||
}
|
|
||||||
const attr = attrs.find((a) => a.type === name)
|
|
||||||
return attr ? (Array.isArray(attr.values) ? attr.values[0] : attr.values) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
dn: entry.dn,
|
|
||||||
cn: getAttr('CN') || getAttr('cn'),
|
|
||||||
sAMAccountName: getAttr('sAMAccountName'),
|
|
||||||
displayName: getAttr('displayName'),
|
|
||||||
email: getAttr('mail'),
|
|
||||||
userAccountControl: getAttr('userAccountControl'),
|
|
||||||
// 为了兼容Python代码的数据结构
|
|
||||||
org: entry.dn,
|
|
||||||
// 调试信息 (限制原始数据大小)
|
|
||||||
raw: users.length < 3 ? { object: entry.object, attributes: entry.attributes } : null
|
|
||||||
}
|
|
||||||
users.push(user)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('referral', (referral) => {
|
|
||||||
logger.info(`LDAP referral received during user listing: ${referral}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('error', (error) => {
|
|
||||||
if (error.message && error.message.toLowerCase().includes('referral')) {
|
|
||||||
logger.warn(`LDAP referral error during user listing (ignored): ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
logger.info(`Found ${users.length} users total`)
|
|
||||||
resolve(users)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证用户凭据
|
|
||||||
*/
|
|
||||||
async authenticateUser(username, password) {
|
|
||||||
try {
|
|
||||||
// 先搜索用户获取DN
|
|
||||||
await this.createConnection()
|
|
||||||
await this.bind()
|
|
||||||
|
|
||||||
const users = await this.searchUser(username)
|
|
||||||
if (users.length === 0) {
|
|
||||||
throw new Error('User not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修复DN提取逻辑,处理ldapjs的DN对象
|
|
||||||
let userDN = users[0].dn
|
|
||||||
if (userDN && typeof userDN === 'object') {
|
|
||||||
// ldapjs返回的是DN对象,需要正确转换为字符串
|
|
||||||
if (userDN.toString && typeof userDN.toString === 'function') {
|
|
||||||
userDN = userDN.toString()
|
|
||||||
} else if (userDN.format && typeof userDN.format === 'function') {
|
|
||||||
userDN = userDN.format()
|
|
||||||
} else {
|
|
||||||
// 从dn对象中提取rdns信息手动构建DN字符串
|
|
||||||
logger.info('User DN object structure:', JSON.stringify(userDN, null, 2))
|
|
||||||
throw new Error('Unable to extract user DN from object')
|
|
||||||
}
|
|
||||||
} else if (typeof userDN !== 'string') {
|
|
||||||
throw new Error('Invalid DN format')
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Attempting to authenticate with DN: ${userDN}`)
|
|
||||||
logger.info(`User sAMAccountName: ${users[0].sAMAccountName || users[0].username}`)
|
|
||||||
logger.info(`User Account Control: ${users[0].userAccountControl}`)
|
|
||||||
|
|
||||||
// 检查账户状态
|
|
||||||
const userAccountControl = parseInt(users[0].userAccountControl) || 0
|
|
||||||
if (userAccountControl & 2) {
|
|
||||||
// UF_ACCOUNTDISABLE = 2
|
|
||||||
throw new Error('User account is disabled')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 断开管理员连接
|
|
||||||
this.disconnect()
|
|
||||||
|
|
||||||
// 尝试多种认证格式
|
|
||||||
const sAMAccountName = users[0].sAMAccountName || users[0].username
|
|
||||||
const authFormats = [
|
|
||||||
sAMAccountName, // 直接使用sAMAccountName
|
|
||||||
`${sAMAccountName}@corp.weidian-inc.com`, // UPN格式
|
|
||||||
`${sAMAccountName}@weidian-inc.com`, // 简化UPN格式
|
|
||||||
`corp\\${sAMAccountName}`, // 域\\用户名格式
|
|
||||||
`CORP\\${sAMAccountName}`, // 大写域\\用户名格式
|
|
||||||
`weidian-inc\\${sAMAccountName}`, // 完整域名\\用户名格式
|
|
||||||
userDN // 完整DN(最后尝试)
|
|
||||||
].filter(Boolean)
|
|
||||||
|
|
||||||
logger.info(`Trying authentication with formats: ${JSON.stringify(authFormats)}`)
|
|
||||||
|
|
||||||
for (const authFormat of authFormats) {
|
|
||||||
try {
|
|
||||||
logger.info(`Attempting authentication with: ${authFormat}`)
|
|
||||||
|
|
||||||
const userClient = ldap.createClient({
|
|
||||||
url: this.config.url,
|
|
||||||
timeout: 10000,
|
|
||||||
connectTimeout: 10000,
|
|
||||||
idleTimeout: 30000
|
|
||||||
})
|
|
||||||
|
|
||||||
const authResult = await new Promise((resolve, reject) => {
|
|
||||||
let resolved = false
|
|
||||||
|
|
||||||
// 设置错误处理
|
|
||||||
userClient.on('error', (err) => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true
|
|
||||||
logger.warn(`Connection error with ${authFormat}:`, err.message)
|
|
||||||
userClient.destroy()
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
userClient.on('connect', () => {
|
|
||||||
logger.info(`Connected for authentication with: ${authFormat}`)
|
|
||||||
|
|
||||||
// 尝试使用用户凭据绑定
|
|
||||||
userClient.bind(authFormat, password, (err) => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true
|
|
||||||
if (err) {
|
|
||||||
logger.warn(
|
|
||||||
`Bind failed with ${authFormat}: ${err.name} - ${err.message} (Code: ${err.code})`
|
|
||||||
)
|
|
||||||
userClient.destroy()
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
logger.info(`Bind successful with ${authFormat}`)
|
|
||||||
userClient.unbind()
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 超时处理
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true
|
|
||||||
userClient.destroy()
|
|
||||||
reject(new Error('Authentication timeout'))
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (authResult) {
|
|
||||||
logger.info(`User ${username} authenticated successfully with format: ${authFormat}`)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
user: users[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(
|
|
||||||
`Authentication failed with format ${authFormat}: ${err.name} - ${err.message}`
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有格式都失败
|
|
||||||
throw new Error('Invalid username or password')
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('User authentication error:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
this.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭连接
|
|
||||||
*/
|
|
||||||
disconnect() {
|
|
||||||
if (this.client) {
|
|
||||||
this.client.destroy()
|
|
||||||
this.client = null
|
|
||||||
logger.info('LDAP connection closed')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列出所有OU
|
|
||||||
*/
|
|
||||||
listOUs() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.client) {
|
|
||||||
return reject(new Error('LDAP client not initialized'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchOptions = {
|
|
||||||
filter: '(objectClass=organizationalUnit)',
|
|
||||||
scope: 'sub',
|
|
||||||
attributes: ['ou', 'dn', 'objectClass', 'description']
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从域根开始搜索所有OU
|
|
||||||
const baseDN = 'DC=corp,DC=weidian-inc,DC=com'
|
|
||||||
logger.info(`Searching for all OUs in: ${baseDN}`)
|
|
||||||
|
|
||||||
this.client.search(baseDN, searchOptions, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const ous = []
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
const obj = entry.object || {}
|
|
||||||
const attrs = entry.attributes || []
|
|
||||||
|
|
||||||
const getAttr = (name) => {
|
|
||||||
if (obj[name]) {
|
|
||||||
return obj[name]
|
|
||||||
}
|
|
||||||
const attr = attrs.find((a) => a.type === name)
|
|
||||||
return attr ? (Array.isArray(attr.values) ? attr.values[0] : attr.values) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const ou = {
|
|
||||||
dn: entry.dn,
|
|
||||||
ou: getAttr('ou'),
|
|
||||||
description: getAttr('description'),
|
|
||||||
objectClass: getAttr('objectClass')
|
|
||||||
}
|
|
||||||
ous.push(ou)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('referral', (referral) => {
|
|
||||||
logger.info(`OUs search referral: ${referral}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('error', (error) => {
|
|
||||||
if (error.message && error.message.toLowerCase().includes('referral')) {
|
|
||||||
logger.warn(`OUs search referral error (ignored): ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
logger.info(`Found ${ous.length} OUs total`)
|
|
||||||
resolve(ous)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证OU是否存在
|
|
||||||
*/
|
|
||||||
verifyOU(ouDN) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.client) {
|
|
||||||
return reject(new Error('LDAP client not initialized'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchOptions = {
|
|
||||||
filter: '(objectClass=organizationalUnit)',
|
|
||||||
scope: 'base',
|
|
||||||
attributes: ['ou', 'dn', 'objectClass']
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Searching for OU: ${ouDN}`)
|
|
||||||
|
|
||||||
this.client.search(ouDN, searchOptions, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let found = false
|
|
||||||
let ouInfo = null
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
found = true
|
|
||||||
ouInfo = {
|
|
||||||
dn: entry.dn,
|
|
||||||
ou: entry.object?.ou || entry.attributes?.find((a) => a.type === 'ou')?.values,
|
|
||||||
objectClass:
|
|
||||||
entry.object?.objectClass ||
|
|
||||||
entry.attributes?.find((a) => a.type === 'objectClass')?.values
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('referral', (referral) => {
|
|
||||||
logger.info(`OU search referral: ${referral}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('error', (error) => {
|
|
||||||
if (error.message && error.message.toLowerCase().includes('referral')) {
|
|
||||||
logger.warn(`OU search referral error (ignored): ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
resolve({
|
|
||||||
exists: found,
|
|
||||||
dn: ouDN,
|
|
||||||
info: ouInfo
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取配置信息(不包含密码)
|
|
||||||
*/
|
|
||||||
getConfig() {
|
|
||||||
return {
|
|
||||||
url: this.config.url,
|
|
||||||
bindDN: this.config.bindDN,
|
|
||||||
baseDN: this.config.baseDN,
|
|
||||||
searchFilter: this.config.searchFilter,
|
|
||||||
timeout: this.config.timeout,
|
|
||||||
connectTimeout: this.config.connectTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new LDAPService()
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
const logger = require('../utils/logger')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户映射服务 - 处理AD用户数据转换和过滤
|
|
||||||
*/
|
|
||||||
class UserMappingService {
|
|
||||||
/**
|
|
||||||
* 解析AD用户账户控制状态
|
|
||||||
*/
|
|
||||||
static parseUserAccountControl(uac) {
|
|
||||||
if (!uac) {
|
|
||||||
return { disabled: true, description: 'Unknown' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const uacValue = parseInt(uac)
|
|
||||||
const flags = {
|
|
||||||
SCRIPT: 0x00000001,
|
|
||||||
ACCOUNTDISABLE: 0x00000002,
|
|
||||||
HOMEDIR_REQUIRED: 0x00000008,
|
|
||||||
LOCKOUT: 0x00000010,
|
|
||||||
PASSWD_NOTREQD: 0x00000020,
|
|
||||||
PASSWD_CANT_CHANGE: 0x00000040,
|
|
||||||
ENCRYPTED_TEXT_PASSWORD_ALLOWED: 0x00000080,
|
|
||||||
TEMP_DUPLICATE_ACCOUNT: 0x00000100,
|
|
||||||
NORMAL_ACCOUNT: 0x00000200,
|
|
||||||
INTERDOMAIN_TRUST_ACCOUNT: 0x00000800,
|
|
||||||
WORKSTATION_TRUST_ACCOUNT: 0x00001000,
|
|
||||||
SERVER_TRUST_ACCOUNT: 0x00002000,
|
|
||||||
DONT_EXPIRE_PASSWD: 0x00010000,
|
|
||||||
MNS_LOGON_ACCOUNT: 0x00020000,
|
|
||||||
SMARTCARD_REQUIRED: 0x00040000,
|
|
||||||
TRUSTED_FOR_DELEGATION: 0x00080000,
|
|
||||||
NOT_DELEGATED: 0x00100000,
|
|
||||||
USE_DES_KEY_ONLY: 0x00200000,
|
|
||||||
DONT_REQUIRE_PREAUTH: 0x00400000,
|
|
||||||
PASSWORD_EXPIRED: 0x00800000,
|
|
||||||
TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: 0x01000000,
|
|
||||||
PARTIAL_SECRETS_ACCOUNT: 0x04000000
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = {
|
|
||||||
disabled: !!(uacValue & flags.ACCOUNTDISABLE),
|
|
||||||
locked: !!(uacValue & flags.LOCKOUT),
|
|
||||||
passwordExpired: !!(uacValue & flags.PASSWORD_EXPIRED),
|
|
||||||
normalAccount: !!(uacValue & flags.NORMAL_ACCOUNT),
|
|
||||||
passwordNotRequired: !!(uacValue & flags.PASSWD_NOTREQD),
|
|
||||||
dontExpirePassword: !!(uacValue & flags.DONT_EXPIRE_PASSWD),
|
|
||||||
description: this.getUserAccountControlDescription(uacValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户账户控制的描述
|
|
||||||
*/
|
|
||||||
static getUserAccountControlDescription(uac) {
|
|
||||||
const uacValue = parseInt(uac)
|
|
||||||
|
|
||||||
if (uacValue & 0x00000002) {
|
|
||||||
return 'Account Disabled'
|
|
||||||
}
|
|
||||||
if (uacValue & 0x00000010) {
|
|
||||||
return 'Account Locked'
|
|
||||||
}
|
|
||||||
if (uacValue & 0x00800000) {
|
|
||||||
return 'Password Expired'
|
|
||||||
}
|
|
||||||
if (uacValue & 0x00000200) {
|
|
||||||
return 'Normal User Account'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `UAC: ${uacValue}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 过滤和映射AD用户数据
|
|
||||||
* 模拟Python代码中的get_ad()函数逻辑
|
|
||||||
*/
|
|
||||||
static mapAdUsers(searchResults) {
|
|
||||||
if (!Array.isArray(searchResults)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除第一个元素(Python代码中的slist.pop(0))
|
|
||||||
const userList = searchResults.slice(1)
|
|
||||||
const mappedUsers = []
|
|
||||||
|
|
||||||
for (const user of userList) {
|
|
||||||
try {
|
|
||||||
const userObj = {
|
|
||||||
org: user.dn || user.distinguishedName,
|
|
||||||
cn: null,
|
|
||||||
userAccountControl: null,
|
|
||||||
accountStatus: null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取CN
|
|
||||||
if (user.cn || user.CN) {
|
|
||||||
userObj.cn = user.cn || user.CN
|
|
||||||
} else {
|
|
||||||
// 如果没有CN属性,跳过此用户
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取userAccountControl
|
|
||||||
if (user.userAccountControl) {
|
|
||||||
userObj.userAccountControl = user.userAccountControl
|
|
||||||
userObj.accountStatus = this.parseUserAccountControl(user.userAccountControl)
|
|
||||||
} else {
|
|
||||||
// 如果没有userAccountControl,跳过此用户
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedUsers.push(userObj)
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Error processing user entry: ${error.message}`, { user })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappedUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 过滤活跃用户(未禁用的账户)
|
|
||||||
*/
|
|
||||||
static filterActiveUsers(users) {
|
|
||||||
return users.filter((user) => user.accountStatus && !user.accountStatus.disabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据用户名搜索(支持模糊匹配)
|
|
||||||
*/
|
|
||||||
static searchUsersByName(users, searchTerm) {
|
|
||||||
if (!searchTerm) {
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
const term = searchTerm.toLowerCase()
|
|
||||||
return users.filter((user) => user.cn && user.cn.toLowerCase().includes(term))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化用户信息用于显示
|
|
||||||
*/
|
|
||||||
static formatUserInfo(user) {
|
|
||||||
return {
|
|
||||||
name: user.cn,
|
|
||||||
distinguishedName: user.org,
|
|
||||||
accountControl: user.userAccountControl,
|
|
||||||
status: user.accountStatus
|
|
||||||
? {
|
|
||||||
enabled: !user.accountStatus.disabled,
|
|
||||||
locked: user.accountStatus.locked,
|
|
||||||
description: user.accountStatus.description
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户统计信息
|
|
||||||
*/
|
|
||||||
static getUserStats(users) {
|
|
||||||
const stats = {
|
|
||||||
total: users.length,
|
|
||||||
active: 0,
|
|
||||||
disabled: 0,
|
|
||||||
locked: 0,
|
|
||||||
passwordExpired: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
users.forEach((user) => {
|
|
||||||
if (user.accountStatus) {
|
|
||||||
if (!user.accountStatus.disabled) {
|
|
||||||
stats.active++
|
|
||||||
}
|
|
||||||
if (user.accountStatus.disabled) {
|
|
||||||
stats.disabled++
|
|
||||||
}
|
|
||||||
if (user.accountStatus.locked) {
|
|
||||||
stats.locked++
|
|
||||||
}
|
|
||||||
if (user.accountStatus.passwordExpired) {
|
|
||||||
stats.passwordExpired++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = UserMappingService
|
|
||||||
@@ -33,31 +33,12 @@
|
|||||||
>名称</label
|
>名称</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.name"
|
class="form-input w-full cursor-not-allowed bg-gray-100 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||||
class="form-input w-full text-sm"
|
disabled
|
||||||
maxlength="100"
|
|
||||||
placeholder="请输入API Key名称"
|
|
||||||
required
|
|
||||||
type="text"
|
type="text"
|
||||||
|
:value="form.name"
|
||||||
/>
|
/>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">最多100个字符</p>
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">名称不可修改</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
|
||||||
>描述</label
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
v-model="form.description"
|
|
||||||
class="form-input w-full text-sm"
|
|
||||||
maxlength="500"
|
|
||||||
placeholder="请输入API Key描述(可选)"
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
|
||||||
最多500个字符(可选)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
@@ -651,7 +632,6 @@ const unselectedTags = computed(() => {
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
|
||||||
tokenLimit: '',
|
tokenLimit: '',
|
||||||
rateLimitWindow: '',
|
rateLimitWindow: '',
|
||||||
rateLimitRequests: '',
|
rateLimitRequests: '',
|
||||||
@@ -727,8 +707,6 @@ const updateApiKey = async () => {
|
|||||||
try {
|
try {
|
||||||
// 准备提交的数据
|
// 准备提交的数据
|
||||||
const data = {
|
const data = {
|
||||||
name: form.name,
|
|
||||||
description: form.description,
|
|
||||||
tokenLimit:
|
tokenLimit:
|
||||||
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
||||||
rateLimitWindow:
|
rateLimitWindow:
|
||||||
@@ -915,7 +893,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.name = props.apiKey.name
|
form.name = props.apiKey.name
|
||||||
form.description = props.apiKey.description || ''
|
|
||||||
form.tokenLimit = props.apiKey.tokenLimit || ''
|
form.tokenLimit = props.apiKey.tokenLimit || ''
|
||||||
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
||||||
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
||||||
|
|||||||
@@ -1,481 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- API Key 创建区域 -->
|
|
||||||
<div class="glass-strong rounded-3xl p-6 shadow-xl">
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fas fa-key text-2xl text-blue-500" />
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-100">API Keys 管理</h3>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">每个用户只能创建一个 API Key</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ apiKeys.length }}/1 个 Key</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 创建新 API Key -->
|
|
||||||
<div v-if="apiKeys.length === 0" class="space-y-4">
|
|
||||||
<div
|
|
||||||
class="rounded-xl border-2 border-dashed border-gray-300 bg-gray-50/50 p-6 text-center dark:border-gray-600 dark:bg-gray-800/50"
|
|
||||||
>
|
|
||||||
<i class="fas fa-plus-circle mb-3 text-3xl text-gray-400" />
|
|
||||||
<h4 class="mb-2 text-lg font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
创建您的第一个 API Key
|
|
||||||
</h4>
|
|
||||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
API Key 将用于访问 Claude Relay Service
|
|
||||||
</p>
|
|
||||||
<form class="mx-auto max-w-md space-y-4" @submit.prevent="createApiKey">
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
API Key 名称将自动设置为您的用户名
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-500">使用额度:无限制</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="w-full rounded-xl bg-gradient-to-r from-blue-500 to-purple-600 px-6 py-3 font-medium text-white transition-all hover:from-blue-600 hover:to-purple-700 disabled:opacity-50"
|
|
||||||
:disabled="createLoading"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<div v-if="createLoading" class="flex items-center justify-center gap-2">
|
|
||||||
<div
|
|
||||||
class="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
|
|
||||||
></div>
|
|
||||||
创建中...
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center justify-center gap-2">
|
|
||||||
<i class="fas fa-plus" />
|
|
||||||
创建 API Key
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 现有 API Keys 显示 -->
|
|
||||||
<div v-if="apiKeys.length > 0" class="space-y-4">
|
|
||||||
<div
|
|
||||||
v-for="apiKey in apiKeys"
|
|
||||||
:key="apiKey.id"
|
|
||||||
class="glass-strong rounded-3xl p-6 shadow-xl"
|
|
||||||
>
|
|
||||||
<div class="mb-4 flex items-start justify-between">
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<div
|
|
||||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500/20 to-purple-500/20"
|
|
||||||
>
|
|
||||||
<i class="fas fa-key text-lg text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-100">
|
|
||||||
{{ apiKey.name || '未命名 API Key' }}
|
|
||||||
</h4>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
创建时间:{{ formatDate(apiKey.createdAt) }}
|
|
||||||
</p>
|
|
||||||
<div class="mt-2 flex items-center justify-between">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium"
|
|
||||||
:class="
|
|
||||||
apiKey.isActive
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<i :class="apiKey.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'" />
|
|
||||||
{{ apiKey.isActive ? '活跃' : '已禁用' }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
:class="[
|
|
||||||
apiKey.isActive
|
|
||||||
? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-900/20'
|
|
||||||
: 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/20',
|
|
||||||
'rounded-lg px-3 py-1 text-xs font-medium transition-colors'
|
|
||||||
]"
|
|
||||||
@click="toggleApiKeyStatus(apiKey)"
|
|
||||||
>
|
|
||||||
<i :class="['fas mr-1', apiKey.isActive ? 'fa-ban' : 'fa-check-circle']" />
|
|
||||||
{{ apiKey.isActive ? '禁用' : '激活' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded-lg px-3 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
|
||||||
@click="deleteApiKey(apiKey)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-trash mr-1" />删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Key 显示 - 历史Key无法显示原始内容 -->
|
|
||||||
<div class="mb-4 space-y-3">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
API Key
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="rounded-xl border border-amber-300 bg-amber-50 p-4 dark:border-amber-600 dark:bg-amber-900/20"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
|
|
||||||
<i class="fas fa-info-circle" />
|
|
||||||
<span class="text-sm">
|
|
||||||
已关联的历史API
|
|
||||||
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请删除原key创建新Key。
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
||||||
Key ID: {{ apiKey.id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 使用统计 -->
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
||||||
<div class="rounded-xl bg-blue-50 p-4 dark:bg-blue-900/20">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fas fa-chart-bar text-blue-500" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">今日请求</p>
|
|
||||||
<p class="text-xl font-bold text-blue-900 dark:text-blue-100">
|
|
||||||
{{ apiKey.usage?.daily?.requests?.toLocaleString() || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-xl bg-green-50 p-4 dark:bg-green-900/20">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fas fa-coins text-green-500" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">今日Token</p>
|
|
||||||
<p class="text-xl font-bold text-green-900 dark:text-green-100">
|
|
||||||
{{ apiKey.usage?.daily?.tokens?.toLocaleString() || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-xl bg-purple-50 p-4 dark:bg-purple-900/20">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fas fa-dollar-sign text-purple-500" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-purple-800 dark:text-purple-300">今日费用</p>
|
|
||||||
<p class="text-xl font-bold text-purple-900 dark:text-purple-100">
|
|
||||||
${{ (apiKey.dailyCost || 0).toFixed(4) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Token 额度进度条 -->
|
|
||||||
<div v-if="apiKey.tokenLimit > 0" class="mt-4">
|
|
||||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<span>Token 使用进度</span>
|
|
||||||
<span>
|
|
||||||
{{ apiKey.usage?.total?.tokens?.toLocaleString() || 0 }} /
|
|
||||||
{{ apiKey.tokenLimit?.toLocaleString() || 0 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-600 transition-all duration-500"
|
|
||||||
:style="{
|
|
||||||
width: `${Math.min(calculateTokenUsagePercentage(apiKey.usage?.total?.tokens || 0, apiKey.tokenLimit || 0), 100)}%`
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 费用限制进度条 -->
|
|
||||||
<div v-if="apiKey.dailyCostLimit > 0" class="mt-4">
|
|
||||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<span>每日费用限制</span>
|
|
||||||
<span>
|
|
||||||
${{ (apiKey.dailyCost || 0).toFixed(4) }} / ${{
|
|
||||||
(apiKey.dailyCostLimit || 0).toFixed(2)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-gradient-to-r from-green-500 to-red-500 transition-all duration-500"
|
|
||||||
:style="{
|
|
||||||
width: `${Math.min(calculateCostUsagePercentage(apiKey.dailyCost || 0, apiKey.dailyCostLimit || 0), 100)}%`
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 查看详细统计按钮 -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<button
|
|
||||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-indigo-500 to-purple-600 px-4 py-2.5 text-sm font-medium text-white transition-all duration-200 hover:from-indigo-600 hover:to-purple-700 hover:shadow-lg"
|
|
||||||
@click="showUsageDetails(apiKey)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chart-line" />
|
|
||||||
查看详细统计
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
class="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"
|
|
||||||
></div>
|
|
||||||
<span class="text-gray-600 dark:text-gray-400">加载中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
|
||||||
<div
|
|
||||||
v-if="error"
|
|
||||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-4 text-center text-red-800 dark:text-red-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-triangle mr-2" />{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 成功提示 -->
|
|
||||||
<div
|
|
||||||
v-if="successMessage"
|
|
||||||
class="rounded-xl border border-green-500/30 bg-green-500/20 p-4 text-center text-green-800 dark:text-green-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check-circle mr-2" />{{ successMessage }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 使用详情模态框 -->
|
|
||||||
<UsageDetailModal
|
|
||||||
:api-key="selectedApiKeyForDetail || {}"
|
|
||||||
:show="showUsageDetailModal"
|
|
||||||
@close="showUsageDetailModal = false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 新API Key模态框 -->
|
|
||||||
<NewApiKeyModal
|
|
||||||
v-if="showNewApiKeyModal"
|
|
||||||
:api-key="newApiKeyData"
|
|
||||||
@close="showNewApiKeyModal = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
|
||||||
import NewApiKeyModal from '@/components/apikeys/NewApiKeyModal.vue'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
userInfo: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const loading = ref(false)
|
|
||||||
const createLoading = ref(false)
|
|
||||||
const error = ref('')
|
|
||||||
const successMessage = ref('')
|
|
||||||
const apiKeys = ref([])
|
|
||||||
|
|
||||||
// 使用详情模态框相关
|
|
||||||
const showUsageDetailModal = ref(false)
|
|
||||||
const selectedApiKeyForDetail = ref(null)
|
|
||||||
|
|
||||||
// 新API Key模态框相关
|
|
||||||
const showNewApiKeyModal = ref(false)
|
|
||||||
const newApiKeyData = ref(null)
|
|
||||||
|
|
||||||
const newKeyForm = ref({})
|
|
||||||
|
|
||||||
// 获取用户的 API Keys
|
|
||||||
const fetchApiKeys = async () => {
|
|
||||||
loading.value = true
|
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('user_token')
|
|
||||||
const response = await fetch('/admin/ldap/user/api-keys', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
if (result.success) {
|
|
||||||
apiKeys.value = result.apiKeys
|
|
||||||
} else {
|
|
||||||
error.value = result.message || '获取 API Keys 失败'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('获取 API Keys 错误:', err)
|
|
||||||
error.value = '网络错误,请重试'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的 API Key
|
|
||||||
const createApiKey = async () => {
|
|
||||||
createLoading.value = true
|
|
||||||
error.value = ''
|
|
||||||
successMessage.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('user_token')
|
|
||||||
const response = await fetch('/admin/ldap/user/api-keys', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
// name和limit字段都由后端自动生成/设置
|
|
||||||
// name: 用户displayName
|
|
||||||
// limit: 0 (无限制)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
if (result.success) {
|
|
||||||
// 显示新API Key模态框
|
|
||||||
newApiKeyData.value = result.apiKey
|
|
||||||
showNewApiKeyModal.value = true
|
|
||||||
|
|
||||||
// 更新API Keys列表
|
|
||||||
apiKeys.value = [result.apiKey]
|
|
||||||
newKeyForm.value = {}
|
|
||||||
|
|
||||||
// 清除错误信息
|
|
||||||
error.value = ''
|
|
||||||
} else {
|
|
||||||
error.value = result.message || 'API Key 创建失败'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('创建 API Key 错误:', err)
|
|
||||||
error.value = '网络错误,请重试'
|
|
||||||
} finally {
|
|
||||||
createLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算Token使用百分比
|
|
||||||
const calculateTokenUsagePercentage = (used, limit) => {
|
|
||||||
if (!limit || limit === 0) return 0
|
|
||||||
return Math.round((used / limit) * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算费用使用百分比
|
|
||||||
const calculateCostUsagePercentage = (used, limit) => {
|
|
||||||
if (!limit || limit === 0) return 0
|
|
||||||
return Math.round((used / limit) * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换API Key状态
|
|
||||||
const toggleApiKeyStatus = async (apiKey) => {
|
|
||||||
const action = apiKey.isActive ? '禁用' : '激活'
|
|
||||||
if (confirm(`确定要${action}这个API Key吗?`)) {
|
|
||||||
await updateApiKey(apiKey.id, { isActive: !apiKey.isActive })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除API Key
|
|
||||||
const deleteApiKey = async (apiKey) => {
|
|
||||||
if (confirm(`确定要删除API Key "${apiKey.name}" 吗?删除后将无法恢复!`)) {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('user_token')
|
|
||||||
const response = await fetch(`/admin/ldap/user/api-keys/${apiKey.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
if (result.success) {
|
|
||||||
successMessage.value = 'API Key 删除成功'
|
|
||||||
// 从本地数组中移除
|
|
||||||
const index = apiKeys.value.findIndex((k) => k.id === apiKey.id)
|
|
||||||
if (index > -1) {
|
|
||||||
apiKeys.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
successMessage.value = ''
|
|
||||||
}, 3000)
|
|
||||||
} else {
|
|
||||||
error.value = result.message || 'API Key 删除失败'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('删除 API Key 错误:', err)
|
|
||||||
error.value = '网络错误,请重试'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新API Key
|
|
||||||
const updateApiKey = async (keyId, updates) => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('user_token')
|
|
||||||
const response = await fetch(`/admin/ldap/user/api-keys/${keyId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updates)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
if (result.success) {
|
|
||||||
successMessage.value = 'API Key 更新成功'
|
|
||||||
// 更新本地数据
|
|
||||||
const apiKey = apiKeys.value.find((k) => k.id === keyId)
|
|
||||||
if (apiKey) {
|
|
||||||
Object.assign(apiKey, updates)
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
successMessage.value = ''
|
|
||||||
}, 3000)
|
|
||||||
} else {
|
|
||||||
error.value = result.message || 'API Key 更新失败'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('更新 API Key 错误:', err)
|
|
||||||
error.value = '网络错误,请重试'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化日期
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return ''
|
|
||||||
const date = new Date(dateString)
|
|
||||||
return date.toLocaleDateString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示使用详情
|
|
||||||
const showUsageDetails = (apiKey) => {
|
|
||||||
selectedApiKeyForDetail.value = apiKey
|
|
||||||
showUsageDetailModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
fetchApiKeys()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 使用全局样式 */
|
|
||||||
</style>
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- 统计概览卡片 -->
|
|
||||||
<div class="glass-strong rounded-3xl p-6 shadow-xl">
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fas fa-chart-line text-2xl text-blue-500" />
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-100">使用统计</h3>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">您的 API 使用情况概览</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="rounded-xl bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="refreshStats"
|
|
||||||
>
|
|
||||||
<i class="fas fa-sync-alt mr-1" :class="{ 'animate-spin': loading }" />
|
|
||||||
刷新
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 统计卡片网格 -->
|
|
||||||
<div v-if="stats" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<!-- API Key 数量 -->
|
|
||||||
<div
|
|
||||||
class="rounded-xl bg-gradient-to-br from-blue-500/10 to-blue-600/10 p-4 dark:from-blue-500/5 dark:to-blue-600/5"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">API Keys</p>
|
|
||||||
<p class="text-2xl font-bold text-blue-900 dark:text-blue-100">
|
|
||||||
{{ stats.keyCount }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-full bg-blue-500/20 p-2">
|
|
||||||
<i class="fas fa-key text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 总使用量 -->
|
|
||||||
<div
|
|
||||||
class="rounded-xl bg-gradient-to-br from-green-500/10 to-green-600/10 p-4 dark:from-green-500/5 dark:to-green-600/5"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">总使用量</p>
|
|
||||||
<p class="text-2xl font-bold text-green-900 dark:text-green-100">
|
|
||||||
{{ stats.totalUsage.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-full bg-green-500/20 p-2">
|
|
||||||
<i class="fas fa-chart-bar text-green-600 dark:text-green-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 总额度 -->
|
|
||||||
<div
|
|
||||||
class="rounded-xl bg-gradient-to-br from-purple-500/10 to-purple-600/10 p-4 dark:from-purple-500/5 dark:to-purple-600/5"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-purple-800 dark:text-purple-300">总额度</p>
|
|
||||||
<p class="text-2xl font-bold text-purple-900 dark:text-purple-100">
|
|
||||||
{{ stats.totalLimit.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-full bg-purple-500/20 p-2">
|
|
||||||
<i class="fas fa-battery-three-quarters text-purple-600 dark:text-purple-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 使用率 -->
|
|
||||||
<div
|
|
||||||
class="rounded-xl bg-gradient-to-br from-orange-500/10 to-orange-600/10 p-4 dark:from-orange-500/5 dark:to-orange-600/5"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-orange-800 dark:text-orange-300">使用率</p>
|
|
||||||
<p class="text-2xl font-bold text-orange-900 dark:text-orange-100">
|
|
||||||
{{ stats.percentage }}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-full bg-orange-500/20 p-2">
|
|
||||||
<i class="fas fa-percentage text-orange-600 dark:text-orange-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading && !stats" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<div
|
|
||||||
v-for="i in 4"
|
|
||||||
:key="i"
|
|
||||||
class="animate-pulse rounded-xl bg-gray-200/50 p-4 dark:bg-gray-700/50"
|
|
||||||
>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="h-4 w-20 rounded bg-gray-300/70 dark:bg-gray-600/70"></div>
|
|
||||||
<div class="h-8 w-16 rounded bg-gray-300/70 dark:bg-gray-600/70"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 各个 API Key 详细统计 -->
|
|
||||||
<div v-if="stats && stats.keys.length > 0" class="space-y-4">
|
|
||||||
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-100">API Key 详细统计</h4>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="keyStats in stats.keys"
|
|
||||||
:key="keyStats.id"
|
|
||||||
class="glass-strong rounded-2xl p-5 shadow-lg"
|
|
||||||
>
|
|
||||||
<div class="mb-4 flex items-start justify-between">
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<div
|
|
||||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-purple-500/20"
|
|
||||||
>
|
|
||||||
<i class="fas fa-key text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 class="font-semibold text-gray-800 dark:text-gray-100">
|
|
||||||
{{ keyStats.name || '未命名 API Key' }}
|
|
||||||
</h5>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">ID: {{ keyStats.id }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">使用率</p>
|
|
||||||
<p class="text-lg font-bold text-gray-800 dark:text-gray-100">
|
|
||||||
{{ keyStats.percentage }}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 使用统计条 -->
|
|
||||||
<div class="mb-3 grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-600 dark:text-gray-400">已使用:</span>
|
|
||||||
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
|
|
||||||
keyStats.used.toLocaleString()
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-600 dark:text-gray-400">总额度:</span>
|
|
||||||
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
|
|
||||||
keyStats.limit.toLocaleString()
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 进度条 -->
|
|
||||||
<div class="relative h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-gradient-to-r transition-all duration-500"
|
|
||||||
:class="getProgressColor(keyStats.percentage)"
|
|
||||||
:style="{ width: `${Math.min(keyStats.percentage, 100)}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 状态警告 -->
|
|
||||||
<div
|
|
||||||
v-if="keyStats.percentage >= 90"
|
|
||||||
class="mt-3 flex items-center gap-2 text-sm text-red-600 dark:text-red-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-triangle" />
|
|
||||||
<span>额度即将用尽,请注意使用</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="keyStats.percentage >= 75"
|
|
||||||
class="mt-3 flex items-center gap-2 text-sm text-orange-600 dark:text-orange-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-info-circle" />
|
|
||||||
<span>额度使用较多,建议关注使用情况</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无数据提示 -->
|
|
||||||
<div
|
|
||||||
v-if="!loading && stats && stats.keys.length === 0"
|
|
||||||
class="glass-strong rounded-2xl p-8 text-center shadow-lg"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chart-line mb-3 text-4xl text-gray-400" />
|
|
||||||
<h4 class="mb-2 text-lg font-medium text-gray-700 dark:text-gray-300">暂无使用数据</h4>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
创建 API Key 后开始使用即可查看详细统计
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
|
||||||
<div
|
|
||||||
v-if="error"
|
|
||||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-4 text-center text-red-800 dark:text-red-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-triangle mr-2" />{{ error }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
userInfo: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref('')
|
|
||||||
const stats = ref(null)
|
|
||||||
|
|
||||||
// 获取用户统计数据
|
|
||||||
const fetchStats = async () => {
|
|
||||||
loading.value = true
|
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('user_token')
|
|
||||||
const response = await fetch('/admin/ldap/user/usage-stats', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
if (result.success) {
|
|
||||||
stats.value = result.stats
|
|
||||||
} else {
|
|
||||||
error.value = result.message || '获取统计数据失败'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('获取统计数据错误:', err)
|
|
||||||
error.value = '网络错误,请重试'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新统计数据
|
|
||||||
const refreshStats = () => {
|
|
||||||
fetchStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取进度条颜色
|
|
||||||
const getProgressColor = (percentage) => {
|
|
||||||
if (percentage >= 90) return 'from-red-500 to-red-600'
|
|
||||||
if (percentage >= 75) return 'from-orange-500 to-orange-600'
|
|
||||||
if (percentage >= 50) return 'from-yellow-500 to-yellow-600'
|
|
||||||
return 'from-green-500 to-green-600'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
fetchStats()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 使用全局样式 */
|
|
||||||
</style>
|
|
||||||
@@ -11,7 +11,6 @@ const AccountsView = () => import('@/views/AccountsView.vue')
|
|||||||
const TutorialView = () => import('@/views/TutorialView.vue')
|
const TutorialView = () => import('@/views/TutorialView.vue')
|
||||||
const SettingsView = () => import('@/views/SettingsView.vue')
|
const SettingsView = () => import('@/views/SettingsView.vue')
|
||||||
const ApiStatsView = () => import('@/views/ApiStatsView.vue')
|
const ApiStatsView = () => import('@/views/ApiStatsView.vue')
|
||||||
const UserDashboardView = () => import('@/views/UserDashboardView.vue')
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -42,12 +41,6 @@ const routes = [
|
|||||||
component: ApiStatsView,
|
component: ApiStatsView,
|
||||||
meta: { requiresAuth: false }
|
meta: { requiresAuth: false }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/user-dashboard',
|
|
||||||
name: 'UserDashboard',
|
|
||||||
component: UserDashboardView,
|
|
||||||
meta: { requiresAuth: false, userAuth: true }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
component: MainLayout,
|
component: MainLayout,
|
||||||
@@ -140,18 +133,7 @@ router.beforeEach((to, from, next) => {
|
|||||||
// API Stats 页面不需要认证,直接放行
|
// API Stats 页面不需要认证,直接放行
|
||||||
if (to.path === '/api-stats' || to.path.startsWith('/api-stats')) {
|
if (to.path === '/api-stats' || to.path.startsWith('/api-stats')) {
|
||||||
next()
|
next()
|
||||||
}
|
} else if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||||
// 用户仪表盘需要用户token验证
|
|
||||||
else if (to.meta.userAuth) {
|
|
||||||
const userToken = localStorage.getItem('user_token')
|
|
||||||
if (!userToken) {
|
|
||||||
next('/api-stats')
|
|
||||||
} else {
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 管理员页面需要管理员认证
|
|
||||||
else if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
||||||
next('/login')
|
next('/login')
|
||||||
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
||||||
next('/dashboard')
|
next('/dashboard')
|
||||||
|
|||||||
@@ -20,15 +20,6 @@
|
|||||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 用户登录按钮 -->
|
|
||||||
<button
|
|
||||||
class="user-login-button flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
|
||||||
@click="showUserLogin"
|
|
||||||
>
|
|
||||||
<i class="fas fa-user-circle text-sm md:text-base" />
|
|
||||||
<span class="text-xs font-semibold tracking-wide md:text-sm">用户登录</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 管理后台按钮 -->
|
<!-- 管理后台按钮 -->
|
||||||
<router-link
|
<router-link
|
||||||
class="admin-button-refined flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
class="admin-button-refined flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
||||||
@@ -138,74 +129,6 @@
|
|||||||
<TutorialView />
|
<TutorialView />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户登录模态框 -->
|
|
||||||
<div v-if="showLoginModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" @click="hideUserLogin"></div>
|
|
||||||
<div class="glass-strong relative w-full max-w-md rounded-2xl p-6 shadow-2xl">
|
|
||||||
<div class="mb-6 text-center">
|
|
||||||
<h2 class="mb-2 text-2xl font-bold text-gray-800 dark:text-gray-100">AD域控登录</h2>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">使用您的域账号登录</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="space-y-4" @submit.prevent="handleUserLogin">
|
|
||||||
<div>
|
|
||||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
用户名
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="userLoginForm.username"
|
|
||||||
class="w-full rounded-xl border border-gray-300 bg-white/70 px-4 py-3 text-gray-800 placeholder-gray-500 backdrop-blur-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800/70 dark:text-gray-200 dark:placeholder-gray-400"
|
|
||||||
placeholder="请输入域用户名"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
密码
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="userLoginForm.password"
|
|
||||||
class="w-full rounded-xl border border-gray-300 bg-white/70 px-4 py-3 text-gray-800 placeholder-gray-500 backdrop-blur-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800/70 dark:text-gray-200 dark:placeholder-gray-400"
|
|
||||||
placeholder="请输入域密码"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
class="flex-1 rounded-xl border border-gray-300 bg-white/70 px-4 py-3 text-sm font-medium text-gray-700 backdrop-blur-sm transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800/70 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
||||||
type="button"
|
|
||||||
@click="hideUserLogin"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-blue-500 to-purple-600 px-4 py-3 text-sm font-medium text-white backdrop-blur-sm transition-all hover:from-blue-600 hover:to-purple-700 disabled:opacity-50"
|
|
||||||
:disabled="userLoginLoading"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="userLoginLoading"
|
|
||||||
class="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
|
|
||||||
></div>
|
|
||||||
<i v-else class="fas fa-sign-in-alt"></i>
|
|
||||||
{{ userLoginLoading ? '登录中...' : '登录' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="userLoginError"
|
|
||||||
class="mt-4 rounded-xl border border-red-500/30 bg-red-500/20 p-3 text-center text-sm text-red-800 dark:text-red-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-triangle mr-2"></i>{{ userLoginError }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -234,15 +157,6 @@ const currentTab = ref('stats')
|
|||||||
// 主题相关
|
// 主题相关
|
||||||
const isDarkMode = computed(() => themeStore.isDarkMode)
|
const isDarkMode = computed(() => themeStore.isDarkMode)
|
||||||
|
|
||||||
// 用户登录相关
|
|
||||||
const showLoginModal = ref(false)
|
|
||||||
const userLoginLoading = ref(false)
|
|
||||||
const userLoginError = ref('')
|
|
||||||
const userLoginForm = ref({
|
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
apiKey,
|
apiKey,
|
||||||
apiId,
|
apiId,
|
||||||
@@ -257,63 +171,6 @@ const {
|
|||||||
|
|
||||||
const { queryStats, switchPeriod, loadStatsWithApiId, loadOemSettings, reset } = apiStatsStore
|
const { queryStats, switchPeriod, loadStatsWithApiId, loadOemSettings, reset } = apiStatsStore
|
||||||
|
|
||||||
// 用户登录相关方法
|
|
||||||
const showUserLogin = () => {
|
|
||||||
showLoginModal.value = true
|
|
||||||
userLoginError.value = ''
|
|
||||||
userLoginForm.value = {
|
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideUserLogin = () => {
|
|
||||||
showLoginModal.value = false
|
|
||||||
userLoginError.value = ''
|
|
||||||
userLoginForm.value = {
|
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUserLogin = async () => {
|
|
||||||
if (!userLoginForm.value.username || !userLoginForm.value.password) {
|
|
||||||
userLoginError.value = '请输入用户名和密码'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userLoginLoading.value = true
|
|
||||||
userLoginError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/admin/ldap/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(userLoginForm.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// 保存token到localStorage
|
|
||||||
localStorage.setItem('user_token', result.token)
|
|
||||||
localStorage.setItem('user_info', JSON.stringify(result.user))
|
|
||||||
|
|
||||||
// 跳转到用户专用页面
|
|
||||||
window.location.href = '/admin-next/user-dashboard'
|
|
||||||
} else {
|
|
||||||
userLoginError.value = result.message || '登录失败'
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('用户登录错误:', error)
|
|
||||||
userLoginError.value = '网络错误,请重试'
|
|
||||||
} finally {
|
|
||||||
userLoginLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理键盘快捷键
|
// 处理键盘快捷键
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
// Ctrl/Cmd + Enter 查询
|
// Ctrl/Cmd + Enter 查询
|
||||||
@@ -452,55 +309,6 @@ watch(apiKey, (newValue) => {
|
|||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 用户登录按钮 */
|
|
||||||
.user-login-button {
|
|
||||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 12px rgba(16, 185, 129, 0.25),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 暗色模式下的用户登录按钮 */
|
|
||||||
:global(.dark) .user-login-button {
|
|
||||||
background: rgba(34, 197, 94, 0.8);
|
|
||||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
|
||||||
color: #f3f4f6;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-login-button:hover {
|
|
||||||
transform: translateY(-2px) scale(1.02);
|
|
||||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 20px rgba(5, 150, 105, 0.35),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .user-login-button:hover {
|
|
||||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
||||||
border-color: rgba(34, 197, 94, 0.4);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 20px rgba(16, 185, 129, 0.3),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-login-button:active {
|
|
||||||
transform: translateY(-1px) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 管理后台按钮 - 精致版本 */
|
/* 管理后台按钮 - 精致版本 */
|
||||||
.admin-button-refined {
|
.admin-button-refined {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
|||||||
@@ -1,344 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen p-4 md:p-6" :class="isDarkMode ? 'gradient-bg-dark' : 'gradient-bg'">
|
|
||||||
<!-- 顶部导航 -->
|
|
||||||
<div class="glass-strong mb-6 rounded-3xl p-4 shadow-xl md:mb-8 md:p-6">
|
|
||||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<LogoTitle
|
|
||||||
:loading="false"
|
|
||||||
logo-src="/assets/logo.png"
|
|
||||||
:subtitle="`欢迎,${userInfo.displayName || userInfo.username}`"
|
|
||||||
title="Claude Relay Service"
|
|
||||||
/>
|
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<i class="fas fa-building mr-1"></i
|
|
||||||
>{{
|
|
||||||
userInfo.groups && userInfo.groups.length > 0
|
|
||||||
? extractGroupName(userInfo.groups[0])
|
|
||||||
: '未知部门'
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 md:gap-4">
|
|
||||||
<!-- 主题切换按钮 -->
|
|
||||||
<ThemeToggle mode="dropdown" />
|
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
|
||||||
<div
|
|
||||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 退出登录按钮 -->
|
|
||||||
<button
|
|
||||||
class="logout-button flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
|
||||||
@click="handleLogout"
|
|
||||||
>
|
|
||||||
<i class="fas fa-sign-out-alt text-sm md:text-base" />
|
|
||||||
<span class="text-xs font-semibold tracking-wide md:text-sm">退出登录</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab 切换 -->
|
|
||||||
<div class="mb-6 md:mb-8">
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div
|
|
||||||
class="inline-flex w-full max-w-2xl rounded-full border border-white/20 bg-white/10 p-1 shadow-lg backdrop-blur-xl md:w-auto"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
:class="['tab-pill-button', currentTab === 'api-keys' ? 'active' : '']"
|
|
||||||
@click="currentTab = 'api-keys'"
|
|
||||||
>
|
|
||||||
<i class="fas fa-key mr-1 md:mr-2" />
|
|
||||||
<span class="text-sm md:text-base">API Keys 管理</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
:class="['tab-pill-button', currentTab === 'tutorial' ? 'active' : '']"
|
|
||||||
@click="currentTab = 'tutorial'"
|
|
||||||
>
|
|
||||||
<i class="fas fa-graduation-cap mr-1 md:mr-2" />
|
|
||||||
<span class="text-sm md:text-base">使用教程</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Keys 管理 -->
|
|
||||||
<div v-if="currentTab === 'api-keys'" class="tab-content">
|
|
||||||
<UserApiKeysView :user-info="userInfo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 使用教程 -->
|
|
||||||
<div v-if="currentTab === 'tutorial'" class="tab-content">
|
|
||||||
<div class="glass-strong rounded-3xl shadow-xl">
|
|
||||||
<TutorialView />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, computed } from 'vue'
|
|
||||||
import { useThemeStore } from '@/stores/theme'
|
|
||||||
import LogoTitle from '@/components/common/LogoTitle.vue'
|
|
||||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
|
||||||
import TutorialView from './TutorialView.vue'
|
|
||||||
import UserApiKeysView from '@/components/user/UserApiKeysView.vue'
|
|
||||||
const themeStore = useThemeStore()
|
|
||||||
|
|
||||||
// 当前标签页
|
|
||||||
const currentTab = ref('api-keys')
|
|
||||||
|
|
||||||
// 主题相关
|
|
||||||
const isDarkMode = computed(() => themeStore.isDarkMode)
|
|
||||||
|
|
||||||
// 用户信息
|
|
||||||
const userInfo = ref({})
|
|
||||||
|
|
||||||
// 从组名中提取部门名称
|
|
||||||
const extractGroupName = (group) => {
|
|
||||||
if (!group) return '未知部门'
|
|
||||||
// 从 "CN=总裁办,OU=微店,DC=corp,DC=weidian-inc,DC=com" 中提取 "总裁办"
|
|
||||||
const match = group.match(/CN=([^,]+)/)
|
|
||||||
return match ? match[1] : '未知部门'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
const handleLogout = () => {
|
|
||||||
localStorage.removeItem('user_token')
|
|
||||||
localStorage.removeItem('user_info')
|
|
||||||
window.location.href = '/admin-next/api-stats'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证用户token
|
|
||||||
const verifyToken = async () => {
|
|
||||||
const token = localStorage.getItem('user_token')
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = '/admin-next/api-stats'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/admin/ldap/verify-token', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
if (result.success) {
|
|
||||||
userInfo.value = result.user
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('user_token')
|
|
||||||
localStorage.removeItem('user_info')
|
|
||||||
window.location.href = '/admin-next/api-stats'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Token验证失败:', error)
|
|
||||||
localStorage.removeItem('user_token')
|
|
||||||
localStorage.removeItem('user_info')
|
|
||||||
window.location.href = '/admin-next/api-stats'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(async () => {
|
|
||||||
// 初始化主题
|
|
||||||
themeStore.initTheme()
|
|
||||||
|
|
||||||
// 验证token
|
|
||||||
const isValid = await verifyToken()
|
|
||||||
if (!isValid) return
|
|
||||||
|
|
||||||
// 从localStorage获取用户信息作为备份
|
|
||||||
const storedUserInfo = localStorage.getItem('user_info')
|
|
||||||
if (storedUserInfo && !userInfo.value.username) {
|
|
||||||
userInfo.value = JSON.parse(storedUserInfo)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 渐变背景 */
|
|
||||||
.gradient-bg {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-bg-dark {
|
|
||||||
background: linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-bg::before,
|
|
||||||
.gradient-bg-dark::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-bg::before {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(240, 147, 251, 0.2) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(102, 126, 234, 0.2) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 40% 40%, rgba(118, 75, 162, 0.1) 0%, transparent 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-bg-dark::before {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(100, 116, 139, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(71, 85, 105, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 40% 40%, rgba(30, 41, 59, 0.1) 0%, transparent 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 玻璃态效果 */
|
|
||||||
.glass-strong {
|
|
||||||
background: var(--glass-strong-color);
|
|
||||||
backdrop-filter: blur(25px);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
box-shadow:
|
|
||||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .glass-strong {
|
|
||||||
box-shadow:
|
|
||||||
0 25px 50px -12px rgba(0, 0, 0, 0.7),
|
|
||||||
0 0 0 1px rgba(55, 65, 81, 0.3),
|
|
||||||
inset 0 1px 0 rgba(75, 85, 99, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 退出登录按钮 */
|
|
||||||
.logout-button {
|
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 12px rgba(239, 68, 68, 0.25),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .logout-button {
|
|
||||||
background: rgba(239, 68, 68, 0.8);
|
|
||||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
|
||||||
color: #f3f4f6;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-button:hover {
|
|
||||||
transform: translateY(-2px) scale(1.02);
|
|
||||||
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 20px rgba(220, 38, 38, 0.35),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .logout-button:hover {
|
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
||||||
border-color: rgba(239, 68, 68, 0.4);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 20px rgba(239, 68, 68, 0.3),
|
|
||||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-button:active {
|
|
||||||
transform: translateY(-1px) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab 胶囊按钮样式 */
|
|
||||||
.tab-pill-button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.dark) .tab-pill-button {
|
|
||||||
color: rgba(209, 213, 219, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.tab-pill-button {
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-pill-button:hover {
|
|
||||||
color: white;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.dark) .tab-pill-button:hover {
|
|
||||||
color: #f3f4f6;
|
|
||||||
background: rgba(100, 116, 139, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-pill-button.active {
|
|
||||||
background: white;
|
|
||||||
color: #764ba2;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.dark) .tab-pill-button.active {
|
|
||||||
background: rgba(71, 85, 105, 0.9);
|
|
||||||
color: #f3f4f6;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab 内容切换动画 */
|
|
||||||
.tab-content {
|
|
||||||
animation: tabFadeIn 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tabFadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user