diff --git a/.env.example b/.env.example index 85ce6d2f..bdf204cf 100644 --- a/.env.example +++ b/.env.example @@ -15,23 +15,6 @@ ENCRYPTION_KEY=your-encryption-key-here # ADMIN_USERNAME=cr_admin_custom # 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_HOST=localhost REDIS_PORT=6379 @@ -62,10 +45,8 @@ LOG_MAX_FILES=5 CLEANUP_INTERVAL=3600000 TOKEN_USAGE_RETENTION=2592000000 HEALTH_CHECK_INTERVAL=60000 -SYSTEM_TIMEZONE=Asia/Shanghai -TIMEZONE_OFFSET=8 -# 实时指标统计窗口(分钟),可选1-60,默认5分钟 -METRICS_WINDOW=5 +TIMEZONE_OFFSET=8 # UTC偏移小时数,默认+8(中国时区) +METRICS_WINDOW=5 # 实时指标统计窗口(分钟),可选1-60,默认5分钟 # 🎨 Web 界面配置 WEB_TITLE=Claude Relay Service @@ -84,5 +65,4 @@ TRUST_PROXY=true WEBHOOK_ENABLED=true WEBHOOK_URLS=https://your-webhook-url.com/notify,https://backup-webhook.com/notify WEBHOOK_TIMEOUT=10000 -WEBHOOK_RETRIES=3 - +WEBHOOK_RETRIES=3 \ No newline at end of file diff --git a/README.md b/README.md index addadc27..8e6cfd5a 100644 --- a/README.md +++ b/README.md @@ -250,15 +250,6 @@ REDIS_HOST=localhost REDIS_PORT=6379 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_ENABLED=true WEBHOOK_URLS=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key diff --git a/package-lock.json b/package-lock.json index c428539e..98b89998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,10 +24,7 @@ "https-proxy-agent": "^7.0.2", "inquirer": "^8.2.6", "ioredis": "^5.3.2", - "jsonwebtoken": "^9.0.2", - "ldapjs": "^3.0.7", "morgan": "^1.10.0", - "node-fetch": "^2.7.0", "ora": "^5.4.1", "rate-limiter-flexible": "^5.0.5", "socks-proxy-agent": "^8.0.2", @@ -2051,101 +2048,6 @@ "@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": { "version": "1.8.0", "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3019,12 +2921,6 @@ "dev": true, "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": { "version": "1.3.8", "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", @@ -3181,15 +3077,6 @@ "dev": true, "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": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3338,18 +3225,6 @@ "@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": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4037,12 +3912,6 @@ "dev": true, "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": { "version": "2.8.5", "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", @@ -4095,7 +3964,7 @@ }, "node_modules/data-uri-to-buffer": { "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==", "license": "MIT", "engines": { @@ -4764,15 +4633,6 @@ "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": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4869,7 +4729,7 @@ }, "node_modules/fetch-blob": { "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==", "funding": [ { @@ -5050,7 +4910,7 @@ }, "node_modules/formdata-polyfill": { "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==", "license": "MIT", "dependencies": { @@ -5139,24 +4999,6 @@ "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": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-7.0.1.tgz", @@ -6635,67 +6477,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": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", @@ -6743,29 +6524,6 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "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": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", @@ -6825,48 +6583,12 @@ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "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": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "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": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6874,12 +6596,6 @@ "dev": true, "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": { "version": "4.4.2", "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -7162,7 +6878,7 @@ }, "node_modules/node-domexception": { "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==", "deprecated": "Use your platform's native DOMException instead", "funding": [ @@ -7181,23 +6897,21 @@ } }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/node-int64": { @@ -7382,6 +7096,7 @@ "version": "1.4.0", "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7686,14 +7401,6 @@ "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": { "version": "1.2.1", "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" } }, - "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": { "version": "2.4.2", "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", @@ -8876,12 +8577,6 @@ "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": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz", @@ -9062,46 +8757,6 @@ "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": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz", @@ -9123,29 +8778,13 @@ }, "node_modules/web-streams-polyfill": { "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==", "license": "MIT", "engines": { "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": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -9272,6 +8911,7 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 3c08d7da..48d7c604 100644 --- a/package.json +++ b/package.json @@ -63,10 +63,7 @@ "https-proxy-agent": "^7.0.2", "inquirer": "^8.2.6", "ioredis": "^5.3.2", - "jsonwebtoken": "^9.0.2", - "ldapjs": "^3.0.7", "morgan": "^1.10.0", - "node-fetch": "^2.7.0", "ora": "^5.4.1", "rate-limiter-flexible": "^5.0.5", "socks-proxy-agent": "^8.0.2", diff --git a/src/app.js b/src/app.js index 00f4c8ae..a1f8020b 100644 --- a/src/app.js +++ b/src/app.js @@ -23,7 +23,6 @@ const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes') const openaiRoutes = require('./routes/openaiRoutes') const azureOpenaiRoutes = require('./routes/azureOpenaiRoutes') const webhookRoutes = require('./routes/webhook') -const ldapRoutes = require('./routes/ldapRoutes') // Import middleware const { @@ -245,7 +244,6 @@ class Application { this.app.use('/openai', openaiRoutes) this.app.use('/azure', azureOpenaiRoutes) this.app.use('/admin/webhook', webhookRoutes) - this.app.use('/admin/ldap', ldapRoutes) // 🏠 根路径重定向到新版管理界面 this.app.get('/', (req, res) => { diff --git a/src/routes/admin.js b/src/routes/admin.js index b356ff16..368d0a33 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -791,8 +791,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const { - name, - description, tokenLimit, concurrencyLimit, rateLimitWindow, @@ -816,30 +814,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { // 只允许更新指定字段 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 (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) { 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 } - logger.info(`🔧 Admin updating API key: ${keyId}`, { - updates: Object.keys(updates), - updatesData: updates - }) - await apiKeyService.updateApiKey(keyId, updates) logger.success(`📝 Admin updated API key: ${keyId}`) return res.json({ success: true, message: 'API key updated successfully' }) } catch (error) { - logger.error(`❌ Failed to update API key ${req.params.keyId}:`, { - error: error.message, - stack: error.stack - }) + logger.error('❌ Failed to update API key:', error) return res.status(500).json({ error: 'Failed to update API key', message: error.message }) } }) diff --git a/src/routes/ldapRoutes.js b/src/routes/ldapRoutes.js deleted file mode 100644 index 66a49cf6..00000000 --- a/src/routes/ldapRoutes.js +++ /dev/null @@ -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 diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 9986736b..46be6352 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -32,9 +32,7 @@ class ApiKeyService { enableClientRestriction = false, allowedClients = [], dailyCostLimit = 0, - tags = [], - owner = null, - ownerType = null + tags = [] } = options // 生成简单的API Key (64字符十六进制) @@ -68,9 +66,7 @@ class ApiKeyService { createdAt: new Date().toISOString(), lastUsedAt: '', expiresAt: expiresAt || '', - createdBy: 'admin', // 可以根据需要扩展用户系统 - owner: owner || '', - ownerType: ownerType || '' + createdBy: 'admin' // 可以根据需要扩展用户系统 } // 保存API Key数据并建立哈希映射 @@ -103,9 +99,7 @@ class ApiKeyService { tags: JSON.parse(keyData.tags || '[]'), createdAt: keyData.createdAt, expiresAt: keyData.expiresAt, - createdBy: keyData.createdBy, - owner: keyData.owner, - ownerType: keyData.ownerType + createdBy: keyData.createdBy } } @@ -300,21 +294,11 @@ class ApiKeyService { // 📝 更新API Key async updateApiKey(keyId, updates) { try { - logger.debug(`🔧 Updating API key ${keyId} with:`, updates) - const keyData = await redis.getApiKey(keyId) if (!keyData || Object.keys(keyData).length === 0) { - logger.error(`❌ API key not found: ${keyId}`) 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 = [ 'name', @@ -360,10 +344,7 @@ class ApiKeyService { // 更新时不需要重新建立哈希映射,因为API Key本身没有变化 await redis.setApiKey(keyId, updatedData) - logger.success(`📝 Updated API key: ${keyId}`, { - updatedFields: Object.keys(updates), - newName: updatedData.name - }) + logger.success(`📝 Updated API key: ${keyId}`) return { success: true } } catch (error) { diff --git a/src/services/ldapService.js b/src/services/ldapService.js deleted file mode 100644 index 5838f570..00000000 --- a/src/services/ldapService.js +++ /dev/null @@ -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() diff --git a/src/services/userMappingService.js b/src/services/userMappingService.js deleted file mode 100644 index b79c2b23..00000000 --- a/src/services/userMappingService.js +++ /dev/null @@ -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 diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index f72b32be..7d8069cf 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -33,31 +33,12 @@ >名称 -

最多100个字符

- - -
- - -

- 最多500个字符(可选) -

+

名称不可修改

@@ -651,7 +632,6 @@ const unselectedTags = computed(() => { // 表单数据 const form = reactive({ name: '', - description: '', tokenLimit: '', rateLimitWindow: '', rateLimitRequests: '', @@ -727,8 +707,6 @@ const updateApiKey = async () => { try { // 准备提交的数据 const data = { - name: form.name, - description: form.description, tokenLimit: form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0, rateLimitWindow: @@ -915,7 +893,6 @@ onMounted(async () => { } form.name = props.apiKey.name - form.description = props.apiKey.description || '' form.tokenLimit = props.apiKey.tokenLimit || '' form.rateLimitWindow = props.apiKey.rateLimitWindow || '' form.rateLimitRequests = props.apiKey.rateLimitRequests || '' diff --git a/web/admin-spa/src/components/user/UserApiKeysView.vue b/web/admin-spa/src/components/user/UserApiKeysView.vue deleted file mode 100644 index e4d6b90f..00000000 --- a/web/admin-spa/src/components/user/UserApiKeysView.vue +++ /dev/null @@ -1,481 +0,0 @@ - - - - - diff --git a/web/admin-spa/src/components/user/UserStatsView.vue b/web/admin-spa/src/components/user/UserStatsView.vue deleted file mode 100644 index 58e2467b..00000000 --- a/web/admin-spa/src/components/user/UserStatsView.vue +++ /dev/null @@ -1,268 +0,0 @@ - - - - - diff --git a/web/admin-spa/src/router/index.js b/web/admin-spa/src/router/index.js index 11a8cc46..47680c7d 100644 --- a/web/admin-spa/src/router/index.js +++ b/web/admin-spa/src/router/index.js @@ -11,7 +11,6 @@ const AccountsView = () => import('@/views/AccountsView.vue') const TutorialView = () => import('@/views/TutorialView.vue') const SettingsView = () => import('@/views/SettingsView.vue') const ApiStatsView = () => import('@/views/ApiStatsView.vue') -const UserDashboardView = () => import('@/views/UserDashboardView.vue') const routes = [ { @@ -42,12 +41,6 @@ const routes = [ component: ApiStatsView, meta: { requiresAuth: false } }, - { - path: '/user-dashboard', - name: 'UserDashboard', - component: UserDashboardView, - meta: { requiresAuth: false, userAuth: true } - }, { path: '/dashboard', component: MainLayout, @@ -140,18 +133,7 @@ router.beforeEach((to, from, next) => { // API Stats 页面不需要认证,直接放行 if (to.path === '/api-stats' || to.path.startsWith('/api-stats')) { next() - } - // 用户仪表盘需要用户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) { + } else if (to.meta.requiresAuth && !authStore.isAuthenticated) { next('/login') } else if (to.path === '/login' && authStore.isAuthenticated) { next('/dashboard') diff --git a/web/admin-spa/src/views/ApiStatsView.vue b/web/admin-spa/src/views/ApiStatsView.vue index fcd6bd51..f2af17b2 100644 --- a/web/admin-spa/src/views/ApiStatsView.vue +++ b/web/admin-spa/src/views/ApiStatsView.vue @@ -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" /> - - - - - -
-
-
-
-

AD域控登录

-

使用您的域账号登录

-
- -
-
- - -
- -
- - -
- -
- - -
-
- -
- {{ userLoginError }} -
-
-
@@ -234,15 +157,6 @@ const currentTab = ref('stats') // 主题相关 const isDarkMode = computed(() => themeStore.isDarkMode) -// 用户登录相关 -const showLoginModal = ref(false) -const userLoginLoading = ref(false) -const userLoginError = ref('') -const userLoginForm = ref({ - username: '', - password: '' -}) - const { apiKey, apiId, @@ -257,63 +171,6 @@ const { 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) => { // Ctrl/Cmd + Enter 查询 @@ -452,55 +309,6 @@ watch(apiKey, (newValue) => { 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 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); diff --git a/web/admin-spa/src/views/UserDashboardView.vue b/web/admin-spa/src/views/UserDashboardView.vue deleted file mode 100644 index 09937bd8..00000000 --- a/web/admin-spa/src/views/UserDashboardView.vue +++ /dev/null @@ -1,344 +0,0 @@ - - - - -