feat: 用户登录
1. 同意HTTP返回类型,写方法 2. 优化错误拦截,返回正确的错误类型 3. 优化auth中的返回类型
This commit is contained in:
parent
ad9bf3896b
commit
541dd50ea3
35
bun.lock
35
bun.lock
@ -4,13 +4,13 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "cursor-init",
|
"name": "cursor-init",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/jwt": "^1.3.1",
|
|
||||||
"@elysiajs/swagger": "^1.3.0",
|
"@elysiajs/swagger": "^1.3.0",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"canvas": "^3.1.2",
|
"canvas": "^3.1.2",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"nodemailer": "^7.0.4",
|
"nodemailer": "^7.0.4",
|
||||||
@ -24,6 +24,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/bun": "^1.0.25",
|
"@types/bun": "^1.0.25",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/redis": "^4.0.11",
|
"@types/redis": "^4.0.11",
|
||||||
"@types/winston": "^2.4.4",
|
"@types/winston": "^2.4.4",
|
||||||
@ -45,8 +46,6 @@
|
|||||||
|
|
||||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||||
|
|
||||||
"@elysiajs/jwt": ["@elysiajs/jwt@1.3.1", "https://registry.npmmirror.com/@elysiajs/jwt/-/jwt-1.3.1.tgz", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-BVLAp0ER4839bR82ElgTsI7OoPxvFWP5u02KMgqpNoAM6xirJBYTKqANzY9ghuMfQoVEuD4B1/8lZwdPKAVg9Q=="],
|
|
||||||
|
|
||||||
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.0", "https://registry.npmmirror.com/@elysiajs/swagger/-/swagger-1.3.0.tgz", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg=="],
|
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.0", "https://registry.npmmirror.com/@elysiajs/swagger/-/swagger-1.3.0.tgz", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "https://registry.npmmirror.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "https://registry.npmmirror.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||||
@ -211,6 +210,10 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
|
||||||
|
|
||||||
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.0.4", "https://registry.npmmirror.com/@types/node/-/node-24.0.4.tgz", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
|
"@types/node": ["@types/node@24.0.4", "https://registry.npmmirror.com/@types/node/-/node-24.0.4.tgz", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
|
||||||
|
|
||||||
"@types/node-fetch": ["@types/node-fetch@2.6.12", "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.12.tgz", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
|
"@types/node-fetch": ["@types/node-fetch@2.6.12", "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.12.tgz", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
|
||||||
@ -293,6 +296,8 @@
|
|||||||
|
|
||||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.17", "https://registry.npmmirror.com/bun-types/-/bun-types-1.2.17.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
"bun-types": ["bun-types@1.2.17", "https://registry.npmmirror.com/bun-types/-/bun-types-1.2.17.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||||
@ -357,6 +362,8 @@
|
|||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||||
|
|
||||||
"elysia": ["elysia@1.3.5", "https://registry.npmmirror.com/elysia/-/elysia-1.3.5.tgz", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="],
|
"elysia": ["elysia@1.3.5", "https://registry.npmmirror.com/elysia/-/elysia-1.3.5.tgz", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="],
|
||||||
|
|
||||||
"enabled": ["enabled@2.0.0", "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
"enabled": ["enabled@2.0.0", "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||||
@ -505,8 +512,6 @@
|
|||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"jose": ["jose@6.0.11", "https://registry.npmmirror.com/jose/-/jose-6.0.11.tgz", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
|
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@9.0.1", "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
"js-tokens": ["js-tokens@9.0.1", "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.0", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
"js-yaml": ["js-yaml@4.1.0", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
@ -517,6 +522,12 @@
|
|||||||
|
|
||||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
|
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "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" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||||
|
|
||||||
|
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||||
|
|
||||||
|
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
"kuler": ["kuler@2.0.0", "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
"kuler": ["kuler@2.0.0", "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||||
@ -525,8 +536,22 @@
|
|||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
|
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||||
|
|
||||||
|
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||||
|
|
||||||
|
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||||
|
|
||||||
|
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||||
|
|
||||||
|
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||||
|
|
||||||
|
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||||
|
|
||||||
"lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
"lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
|
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||||
|
|
||||||
"logform": ["logform@2.7.0", "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
"logform": ["logform@2.7.0", "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||||
|
|
||||||
"long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
"long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"canvas": "^3.1.2",
|
"canvas": "^3.1.2",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"nodemailer": "^7.0.4",
|
"nodemailer": "^7.0.4",
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
* @description 统一导出JWT密钥和过期时间,支持不同类型的token配置
|
* @description 统一导出JWT密钥和过期时间,支持不同类型的token配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TOKEN_TYPES, type TokenType } from '@/type/jwt.type';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT基础配置
|
* JWT基础配置
|
||||||
@ -22,72 +21,4 @@ export const jwtConfig = {
|
|||||||
issuer: process.env.JWT_ISSUER || 'elysia-api',
|
issuer: process.env.JWT_ISSUER || 'elysia-api',
|
||||||
/** JWT受众 */
|
/** JWT受众 */
|
||||||
audience: process.env.JWT_AUDIENCE || 'web-client',
|
audience: process.env.JWT_AUDIENCE || 'web-client',
|
||||||
/** Token有效期(向后兼容) */
|
|
||||||
exp: '7d',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 不同类型Token的配置
|
|
||||||
* @description 区分不同用途的token配置,包括过期时间和盐值
|
|
||||||
*/
|
|
||||||
export const tokenConfig = {
|
|
||||||
/** 访问令牌配置 */
|
|
||||||
accessToken: {
|
|
||||||
/** 过期时间 */
|
|
||||||
expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '2h',
|
|
||||||
/** 盐值(用于增强安全性) */
|
|
||||||
salt: process.env.JWT_ACCESS_SALT || 'access_token_salt_2024',
|
|
||||||
/** 令牌类型标识 */
|
|
||||||
type: 'access' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 刷新令牌配置 */
|
|
||||||
refreshToken: {
|
|
||||||
/** 过期时间 */
|
|
||||||
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d',
|
|
||||||
/** 盐值 */
|
|
||||||
salt: process.env.JWT_REFRESH_SALT || 'refresh_token_salt_2024',
|
|
||||||
/** 令牌类型标识 */
|
|
||||||
type: 'refresh' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 邮箱激活令牌配置 */
|
|
||||||
activationToken: {
|
|
||||||
/** 过期时间 */
|
|
||||||
expiresIn: process.env.JWT_ACTIVATION_EXPIRES_IN || '24h',
|
|
||||||
/** 盐值 */
|
|
||||||
salt: process.env.JWT_ACTIVATION_SALT || 'activation_token_salt_2024',
|
|
||||||
/** 令牌类型标识 */
|
|
||||||
type: 'activation' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 密码重置令牌配置 */
|
|
||||||
passwordResetToken: {
|
|
||||||
/** 过期时间 */
|
|
||||||
expiresIn: process.env.JWT_PASSWORD_RESET_EXPIRES_IN || '1h',
|
|
||||||
/** 盐值 */
|
|
||||||
salt: process.env.JWT_PASSWORD_RESET_SALT || 'password_reset_salt_2024',
|
|
||||||
/** 令牌类型标识 */
|
|
||||||
type: 'password_reset' as const,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定类型token的配置
|
|
||||||
* @param type Token类型
|
|
||||||
* @returns Token配置
|
|
||||||
*/
|
|
||||||
export function getTokenConfig(type: TokenType) {
|
|
||||||
switch (type) {
|
|
||||||
case TOKEN_TYPES.ACCESS:
|
|
||||||
return tokenConfig.accessToken;
|
|
||||||
case TOKEN_TYPES.REFRESH:
|
|
||||||
return tokenConfig.refreshToken;
|
|
||||||
case TOKEN_TYPES.ACTIVATION:
|
|
||||||
return tokenConfig.activationToken;
|
|
||||||
case TOKEN_TYPES.PASSWORD_RESET:
|
|
||||||
return tokenConfig.passwordResetToken;
|
|
||||||
default:
|
|
||||||
throw new Error(`未知的Token类型: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
* @file 统一错误码定义
|
* @file 统一错误码定义
|
||||||
* @author AI助手
|
* @author AI助手
|
||||||
* @date 2025-06-29
|
* @date 2025-06-29
|
||||||
|
* @lastEditor AI Assistant
|
||||||
|
* @lastEditTime 2025-01-07
|
||||||
* @description 定义整个应用的统一错误码,提供类型安全的错误处理
|
* @description 定义整个应用的统一错误码,提供类型安全的错误处理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -47,6 +49,8 @@ export const ERROR_CODES = {
|
|||||||
ACCOUNT_NOT_ACTIVATED: 'ACCOUNT_NOT_ACTIVATED', // 账号未激活
|
ACCOUNT_NOT_ACTIVATED: 'ACCOUNT_NOT_ACTIVATED', // 账号未激活
|
||||||
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED', // 账号被锁定
|
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED', // 账号被锁定
|
||||||
TOO_MANY_FAILED_ATTEMPTS: 'TOO_MANY_FAILED_ATTEMPTS', // 失败次数过多
|
TOO_MANY_FAILED_ATTEMPTS: 'TOO_MANY_FAILED_ATTEMPTS', // 失败次数过多
|
||||||
|
TOO_MANY_ATTEMPTS: 'TOO_MANY_ATTEMPTS', // 登录次数过多
|
||||||
|
CAPTCHA_REQUIRED: 'CAPTCHA_REQUIRED', // 需要验证码
|
||||||
|
|
||||||
// 密码重置相关错误
|
// 密码重置相关错误
|
||||||
INVALID_RESET_TOKEN: 'INVALID_RESET_TOKEN', // 重置令牌无效
|
INVALID_RESET_TOKEN: 'INVALID_RESET_TOKEN', // 重置令牌无效
|
||||||
@ -108,6 +112,8 @@ export const ERROR_CODE_TO_HTTP_STATUS: Record<ErrorCode, number> = {
|
|||||||
[ERROR_CODES.ACCOUNT_NOT_ACTIVATED]: 403,
|
[ERROR_CODES.ACCOUNT_NOT_ACTIVATED]: 403,
|
||||||
[ERROR_CODES.ACCOUNT_LOCKED]: 403,
|
[ERROR_CODES.ACCOUNT_LOCKED]: 403,
|
||||||
[ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: 429,
|
[ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: 429,
|
||||||
|
[ERROR_CODES.TOO_MANY_ATTEMPTS]: 429,
|
||||||
|
[ERROR_CODES.CAPTCHA_REQUIRED]: 400,
|
||||||
|
|
||||||
// 密码重置相关错误
|
// 密码重置相关错误
|
||||||
[ERROR_CODES.INVALID_RESET_TOKEN]: 400,
|
[ERROR_CODES.INVALID_RESET_TOKEN]: 400,
|
||||||
@ -164,6 +170,8 @@ export const ERROR_CODE_MESSAGES: Record<ErrorCode, string> = {
|
|||||||
[ERROR_CODES.ACCOUNT_NOT_ACTIVATED]: '账号未激活',
|
[ERROR_CODES.ACCOUNT_NOT_ACTIVATED]: '账号未激活',
|
||||||
[ERROR_CODES.ACCOUNT_LOCKED]: '账号已被锁定',
|
[ERROR_CODES.ACCOUNT_LOCKED]: '账号已被锁定',
|
||||||
[ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: '登录失败次数过多',
|
[ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: '登录失败次数过多',
|
||||||
|
[ERROR_CODES.TOO_MANY_ATTEMPTS]: '登录次数过多,请稍后再试',
|
||||||
|
[ERROR_CODES.CAPTCHA_REQUIRED]: '请输入验证码',
|
||||||
|
|
||||||
// 密码重置相关错误
|
// 密码重置相关错误
|
||||||
[ERROR_CODES.INVALID_RESET_TOKEN]: '重置令牌无效或已过期',
|
[ERROR_CODES.INVALID_RESET_TOKEN]: '重置令牌无效或已过期',
|
||||||
|
18
src/demo/jwt.ts
Normal file
18
src/demo/jwt.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { getTokenConfig } from '@/config';
|
||||||
|
import { TOKEN_TYPES } from '@/type/jwt.type';
|
||||||
|
import { jwt } from '@elysiajs/jwt';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const config = getTokenConfig(TOKEN_TYPES.ACTIVATION)
|
||||||
|
|
||||||
|
const token = jwt().sign()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2,17 +2,16 @@
|
|||||||
* @file 认证模块Controller层实现
|
* @file 认证模块Controller层实现
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @date 2024-12-19
|
* @date 2024-12-19
|
||||||
* @description 认证模块的路由控制器,处理用户注册和邮箱激活相关请求
|
* @lastEditor AI Assistant
|
||||||
|
* @lastEditTime 2025-01-07
|
||||||
|
* @description 认证模块的路由控制器,处理用户注册、邮箱激活、用户登录等请求
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { RegisterSchema, ActivateSchema } from './auth.schema';
|
import { RegisterSchema, ActivateSchema, LoginSchema } from './auth.schema';
|
||||||
import { RegisterResponses, ActivateResponses } from './auth.response';
|
import { RegisterResponses, ActivateResponses, LoginResponses } from './auth.response';
|
||||||
import { authService } from './auth.service';
|
import { authService } from './auth.service';
|
||||||
import { tags } from '@/modules/tags';
|
import { tags } from '@/modules/tags';
|
||||||
import { Logger } from '@/plugins/logger/logger.service';
|
|
||||||
import { ERROR_CODES } from '@/constants/error-codes';
|
|
||||||
import { BusinessError } from '@/utils/response.helper';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证控制器
|
* 认证控制器
|
||||||
@ -28,28 +27,7 @@ export const authController = new Elysia()
|
|||||||
*/
|
*/
|
||||||
.post(
|
.post(
|
||||||
'/register',
|
'/register',
|
||||||
async ({ body, set }) => {
|
({ body, set }) => authService.register(body),
|
||||||
try {
|
|
||||||
return await authService.register(body);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BusinessError) {
|
|
||||||
set.status = 400;
|
|
||||||
return {
|
|
||||||
code: error.code,
|
|
||||||
message: error.message,
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.error(error as Error);
|
|
||||||
set.status = 500;
|
|
||||||
return {
|
|
||||||
code: ERROR_CODES.INTERNAL_ERROR,
|
|
||||||
message: '服务器内部错误',
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
body: RegisterSchema,
|
body: RegisterSchema,
|
||||||
detail: {
|
detail: {
|
||||||
@ -71,43 +49,7 @@ export const authController = new Elysia()
|
|||||||
*/
|
*/
|
||||||
.post(
|
.post(
|
||||||
'/activate',
|
'/activate',
|
||||||
async ({ body, set }) => {
|
({ body, set }) => authService.activate(body),
|
||||||
try {
|
|
||||||
return await authService.activate(body);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BusinessError) {
|
|
||||||
// 根据错误码设置适当的HTTP状态码
|
|
||||||
switch (error.code) {
|
|
||||||
case ERROR_CODES.INVALID_ACTIVATION_TOKEN:
|
|
||||||
case ERROR_CODES.TOKEN_EXPIRED:
|
|
||||||
set.status = 400;
|
|
||||||
break;
|
|
||||||
case ERROR_CODES.USER_NOT_FOUND:
|
|
||||||
set.status = 404;
|
|
||||||
break;
|
|
||||||
case ERROR_CODES.ALREADY_ACTIVATED:
|
|
||||||
set.status = 409;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
set.status = 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: error.code,
|
|
||||||
message: error.message,
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.error(error as Error);
|
|
||||||
set.status = 500;
|
|
||||||
return {
|
|
||||||
code: ERROR_CODES.INTERNAL_ERROR,
|
|
||||||
message: '服务器内部错误',
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
body: ActivateSchema,
|
body: ActivateSchema,
|
||||||
detail: {
|
detail: {
|
||||||
@ -118,4 +60,26 @@ export const authController = new Elysia()
|
|||||||
},
|
},
|
||||||
response: ActivateResponses,
|
response: ActivateResponses,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录接口
|
||||||
|
* @route POST /api/auth/login
|
||||||
|
* @description 用户登录,支持用户名或邮箱登录,返回JWT令牌
|
||||||
|
* @param body LoginRequest 登录请求参数
|
||||||
|
* @returns LoginSuccessResponse | LoginErrorResponse
|
||||||
|
*/
|
||||||
|
.post(
|
||||||
|
'/login',
|
||||||
|
({ body, set }) => authService.login(body),
|
||||||
|
{
|
||||||
|
body: LoginSchema,
|
||||||
|
detail: {
|
||||||
|
summary: '用户登录',
|
||||||
|
description: '用户登录接口,支持用户名或邮箱登录,登录成功返回JWT访问令牌和刷新令牌',
|
||||||
|
tags: [tags.auth],
|
||||||
|
operationId: 'loginUser',
|
||||||
|
},
|
||||||
|
response: LoginResponses,
|
||||||
|
}
|
||||||
);
|
);
|
@ -2,7 +2,9 @@
|
|||||||
* @file 认证模块响应格式定义
|
* @file 认证模块响应格式定义
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @date 2024-12-19
|
* @date 2024-12-19
|
||||||
* @description 定义认证模块的用户注册响应格式
|
* @lastEditor AI Assistant
|
||||||
|
* @lastEditTime 2025-01-07
|
||||||
|
* @description 定义认证模块的响应格式,包括用户注册、邮箱激活、用户登录等
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { t, type Static } from 'elysia';
|
import { t, type Static } from 'elysia';
|
||||||
@ -224,3 +226,186 @@ export type ActivateSuccessResponse = Static<typeof ActivateSuccessResponseSchem
|
|||||||
|
|
||||||
/** 邮箱激活失败响应类型 */
|
/** 邮箱激活失败响应类型 */
|
||||||
export type ActivateErrorResponse = Static<typeof ActivateErrorResponseSchema>;
|
export type ActivateErrorResponse = Static<typeof ActivateErrorResponseSchema>;
|
||||||
|
|
||||||
|
// ========== 用户登录相关响应格式 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录成功响应数据Schema
|
||||||
|
* @description 用户登录成功后返回的用户信息和认证令牌
|
||||||
|
*/
|
||||||
|
export const LoginSuccessDataSchema = t.Object({
|
||||||
|
/** 用户基本信息 */
|
||||||
|
user: t.Object({
|
||||||
|
/** 用户ID */
|
||||||
|
id: t.String({
|
||||||
|
description: '用户ID(bigint类型以字符串形式返回防止精度丢失)',
|
||||||
|
examples: ['1', '2', '3']
|
||||||
|
}),
|
||||||
|
/** 用户名 */
|
||||||
|
username: t.String({
|
||||||
|
description: '用户名',
|
||||||
|
examples: ['admin', 'testuser']
|
||||||
|
}),
|
||||||
|
/** 邮箱地址 */
|
||||||
|
email: t.String({
|
||||||
|
description: '邮箱地址',
|
||||||
|
examples: ['user@example.com']
|
||||||
|
}),
|
||||||
|
/** 账号状态 */
|
||||||
|
status: t.String({
|
||||||
|
description: '账号状态',
|
||||||
|
examples: ['active']
|
||||||
|
}),
|
||||||
|
/** 最后登录时间 */
|
||||||
|
lastLoginAt: t.Union([t.String(), t.Null()], {
|
||||||
|
description: '最后登录时间',
|
||||||
|
examples: ['2024-12-19T10:30:00Z', null]
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
/** 认证令牌信息 */
|
||||||
|
tokens: t.Object({
|
||||||
|
/** 访问令牌 */
|
||||||
|
accessToken: t.String({
|
||||||
|
description: 'JWT访问令牌',
|
||||||
|
examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...']
|
||||||
|
}),
|
||||||
|
/** 刷新令牌 */
|
||||||
|
refreshToken: t.String({
|
||||||
|
description: 'JWT刷新令牌',
|
||||||
|
examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...']
|
||||||
|
}),
|
||||||
|
/** 令牌类型 */
|
||||||
|
tokenType: t.String({
|
||||||
|
description: '令牌类型',
|
||||||
|
examples: ['Bearer']
|
||||||
|
}),
|
||||||
|
/** 过期时间(秒) */
|
||||||
|
expiresIn: t.String({
|
||||||
|
description: '访问令牌过期时间(秒)',
|
||||||
|
examples: [7200, 86400]
|
||||||
|
}),
|
||||||
|
/** 刷新令牌过期时间(秒) */
|
||||||
|
refreshExpiresIn: t.String({
|
||||||
|
description: '刷新令牌过期时间(秒)',
|
||||||
|
examples: [2592000]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录成功响应Schema
|
||||||
|
* @description 用户登录成功的完整响应格式
|
||||||
|
*/
|
||||||
|
export const LoginSuccessResponseSchema = globalResponseWrapperSchema(LoginSuccessDataSchema);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录失败响应Schema
|
||||||
|
* @description 用户登录失败的错误响应格式
|
||||||
|
*/
|
||||||
|
export const LoginErrorResponseSchema = t.Object({
|
||||||
|
/** 错误代码 */
|
||||||
|
code: t.Union([
|
||||||
|
t.Literal('VALIDATION_ERROR'),
|
||||||
|
t.Literal('USER_NOT_FOUND'),
|
||||||
|
t.Literal('INVALID_PASSWORD'),
|
||||||
|
t.Literal('ACCOUNT_NOT_ACTIVATED'),
|
||||||
|
t.Literal('ACCOUNT_LOCKED'),
|
||||||
|
t.Literal('TOO_MANY_ATTEMPTS'),
|
||||||
|
t.Literal('CAPTCHA_REQUIRED'),
|
||||||
|
t.Literal('CAPTCHA_ERROR'),
|
||||||
|
t.Literal('INTERNAL_ERROR')
|
||||||
|
], {
|
||||||
|
description: '错误代码',
|
||||||
|
examples: ['USER_NOT_FOUND', 'INVALID_PASSWORD', 'ACCOUNT_NOT_ACTIVATED']
|
||||||
|
}),
|
||||||
|
/** 错误信息 */
|
||||||
|
message: t.String({
|
||||||
|
description: '错误信息',
|
||||||
|
examples: ['用户不存在', '密码错误', '账号未激活']
|
||||||
|
}),
|
||||||
|
/** 错误数据 */
|
||||||
|
data: t.Union([
|
||||||
|
t.Null(),
|
||||||
|
t.Object({
|
||||||
|
/** 登录失败次数 */
|
||||||
|
attempts: t.Optional(t.Number({
|
||||||
|
description: '登录失败次数',
|
||||||
|
examples: [3, 5]
|
||||||
|
})),
|
||||||
|
/** 账号锁定时间 */
|
||||||
|
lockUntil: t.Optional(t.String({
|
||||||
|
description: '账号锁定到期时间',
|
||||||
|
examples: ['2024-12-19T11:30:00Z']
|
||||||
|
})),
|
||||||
|
/** 是否需要验证码 */
|
||||||
|
captchaRequired: t.Optional(t.Boolean({
|
||||||
|
description: '是否需要验证码',
|
||||||
|
examples: [true]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
], {
|
||||||
|
description: '错误时的附加数据'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录接口响应组合
|
||||||
|
* @description 用于Controller中定义所有可能的响应格式
|
||||||
|
*/
|
||||||
|
export const LoginResponses = {
|
||||||
|
200: LoginSuccessResponseSchema,
|
||||||
|
400: LoginErrorResponseSchema,
|
||||||
|
401: t.Object({
|
||||||
|
code: t.Literal('UNAUTHORIZED'),
|
||||||
|
message: t.String({
|
||||||
|
description: '认证失败',
|
||||||
|
examples: ['用户名或密码错误', '账号未激活']
|
||||||
|
}),
|
||||||
|
data: t.Null()
|
||||||
|
}),
|
||||||
|
423: t.Object({
|
||||||
|
code: t.Literal('ACCOUNT_LOCKED'),
|
||||||
|
message: t.String({
|
||||||
|
description: '账号被锁定',
|
||||||
|
examples: ['账号已被锁定,请稍后再试']
|
||||||
|
}),
|
||||||
|
data: t.Object({
|
||||||
|
lockUntil: t.String({
|
||||||
|
description: '锁定到期时间',
|
||||||
|
examples: ['2024-12-19T11:30:00Z']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
429: t.Object({
|
||||||
|
code: t.Literal('TOO_MANY_ATTEMPTS'),
|
||||||
|
message: t.String({
|
||||||
|
description: '登录次数过多',
|
||||||
|
examples: ['登录次数过多,请稍后再试']
|
||||||
|
}),
|
||||||
|
data: t.Object({
|
||||||
|
retryAfter: t.Number({
|
||||||
|
description: '重试间隔(秒)',
|
||||||
|
examples: [300, 600]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
500: t.Object({
|
||||||
|
code: t.Literal('INTERNAL_ERROR'),
|
||||||
|
message: t.String({
|
||||||
|
description: '内部服务器错误',
|
||||||
|
examples: ['服务器内部错误']
|
||||||
|
}),
|
||||||
|
data: t.Null()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== TypeScript类型导出 ==========
|
||||||
|
|
||||||
|
/** 用户登录成功响应数据类型 */
|
||||||
|
export type LoginSuccessData = Static<typeof LoginSuccessDataSchema>;
|
||||||
|
|
||||||
|
/** 用户登录成功响应类型 */
|
||||||
|
export type LoginSuccessResponse = Static<typeof LoginSuccessResponseSchema>;
|
||||||
|
|
||||||
|
/** 用户登录失败响应类型 */
|
||||||
|
export type LoginErrorResponse = Static<typeof LoginErrorResponseSchema>;
|
@ -63,8 +63,50 @@ export const ActivateSchema = t.Object({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录Schema
|
||||||
|
* @description 用户登录请求参数验证规则
|
||||||
|
*/
|
||||||
|
export const LoginSchema = t.Object({
|
||||||
|
/** 登录标识符,支持用户名或邮箱 */
|
||||||
|
identifier: t.String({
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 100,
|
||||||
|
description: '登录标识符,支持用户名或邮箱',
|
||||||
|
examples: ['admin', 'user@example.com']
|
||||||
|
}),
|
||||||
|
/** 密码 */
|
||||||
|
password: t.String({
|
||||||
|
minLength: 6,
|
||||||
|
maxLength: 50,
|
||||||
|
description: '用户密码',
|
||||||
|
examples: ['password123']
|
||||||
|
}),
|
||||||
|
/** 图形验证码(可选) */
|
||||||
|
captcha: t.Optional(t.String({
|
||||||
|
minLength: 4,
|
||||||
|
maxLength: 6,
|
||||||
|
description: '图形验证码,登录失败次数过多时需要',
|
||||||
|
examples: ['a1b2']
|
||||||
|
})),
|
||||||
|
/** 验证码会话ID(可选) */
|
||||||
|
captchaId: t.Optional(t.String({
|
||||||
|
description: '验证码会话ID,与captcha配对使用',
|
||||||
|
examples: ['uuid-string-here']
|
||||||
|
})),
|
||||||
|
/** 是否记住登录状态 */
|
||||||
|
rememberMe: t.Optional(t.Boolean({
|
||||||
|
description: '是否记住登录状态,影响token过期时间',
|
||||||
|
examples: [true, false],
|
||||||
|
default: false
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
/** 用户注册请求类型 */
|
/** 用户注册请求类型 */
|
||||||
export type RegisterRequest = Static<typeof RegisterSchema>;
|
export type RegisterRequest = Static<typeof RegisterSchema>;
|
||||||
|
|
||||||
/** 邮箱激活请求类型 */
|
/** 邮箱激活请求类型 */
|
||||||
export type ActivateRequest = Static<typeof ActivateSchema>;
|
export type ActivateRequest = Static<typeof ActivateSchema>;
|
||||||
|
|
||||||
|
/** 用户登录请求类型 */
|
||||||
|
export type LoginRequest = Static<typeof LoginSchema>;
|
@ -2,7 +2,9 @@
|
|||||||
* @file 认证模块Service层实现
|
* @file 认证模块Service层实现
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @date 2024-12-19
|
* @date 2024-12-19
|
||||||
* @description 认证模块的用户注册业务逻辑实现
|
* @lastEditor AI Assistant
|
||||||
|
* @lastEditTime 2025-01-07
|
||||||
|
* @description 认证模块的业务逻辑实现,包括用户注册、邮箱激活、用户登录等
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
@ -16,12 +18,14 @@ import { successResponse, errorResponse, BusinessError } from '@/utils/response.
|
|||||||
import { nextId } from '@/utils/snowflake';
|
import { nextId } from '@/utils/snowflake';
|
||||||
import { jwtService } from '@/plugins/jwt/jwt.service';
|
import { jwtService } from '@/plugins/jwt/jwt.service';
|
||||||
import { emailService } from '@/plugins/email/email.service';
|
import { emailService } from '@/plugins/email/email.service';
|
||||||
import type { RegisterRequest, ActivateRequest } from './auth.schema';
|
import type { RegisterRequest, ActivateRequest, LoginRequest } from './auth.schema';
|
||||||
import type {
|
import type {
|
||||||
RegisterSuccessResponse,
|
RegisterSuccessResponse,
|
||||||
RegisterErrorResponse,
|
RegisterErrorResponse,
|
||||||
ActivateSuccessResponse,
|
ActivateSuccessResponse,
|
||||||
ActivateErrorResponse
|
ActivateErrorResponse,
|
||||||
|
LoginSuccessResponse,
|
||||||
|
LoginErrorResponse
|
||||||
} from './auth.response';
|
} from './auth.response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -429,6 +433,205 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
* @param request 用户登录请求参数
|
||||||
|
* @returns Promise<LoginSuccessResponse>
|
||||||
|
*/
|
||||||
|
async login(request: LoginRequest): Promise<LoginSuccessResponse> {
|
||||||
|
try {
|
||||||
|
Logger.info(`用户登录请求:${JSON.stringify({ ...request, password: '***', captcha: '***' })}`);
|
||||||
|
|
||||||
|
const { identifier, password, captcha, captchaId, rememberMe = false } = request;
|
||||||
|
|
||||||
|
// 1. 验证验证码(如果提供)
|
||||||
|
if (captcha && captchaId) {
|
||||||
|
// await this.validateCaptcha(captcha, captchaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找用户(支持用户名或邮箱)
|
||||||
|
const user = await this.findUserByIdentifier(identifier);
|
||||||
|
|
||||||
|
// todo 判断帐号状态,是否锁定
|
||||||
|
|
||||||
|
// 3. 验证密码
|
||||||
|
await this.verifyPassword(password, user.passwordHash);
|
||||||
|
|
||||||
|
// 4. 检查账号状态
|
||||||
|
await this.checkAccountStatus(user);
|
||||||
|
|
||||||
|
// 5. 生成JWT令牌
|
||||||
|
const tokens = jwtService.generateTokens(user, rememberMe);
|
||||||
|
|
||||||
|
// 6. 更新最后登录时间
|
||||||
|
await this.updateLastLoginTime(user.id);
|
||||||
|
|
||||||
|
// 7. 记录登录日志
|
||||||
|
await this.recordLoginLog(user.id, identifier);
|
||||||
|
|
||||||
|
Logger.info(`用户登录成功:${user.id} - ${user.username}`);
|
||||||
|
|
||||||
|
console.log(tokens);
|
||||||
|
|
||||||
|
return successResponse({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
status: user.status,
|
||||||
|
lastLoginAt: user.lastLoginAt
|
||||||
|
},
|
||||||
|
tokens
|
||||||
|
}, '登录成功');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(new Error(`用户登录失败:${error}`));
|
||||||
|
|
||||||
|
if (error instanceof BusinessError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessError('登录失败,请稍后重试', ERROR_CODES.INTERNAL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标识符查找用户(支持用户名或邮箱)
|
||||||
|
* @param identifier 用户标识符(用户名或邮箱)
|
||||||
|
* @returns Promise<User> 用户信息
|
||||||
|
*/
|
||||||
|
private async findUserByIdentifier(identifier: string): Promise<{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
status: string;
|
||||||
|
passwordHash: string;
|
||||||
|
lastLoginAt: string | null;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// 判断是否为邮箱格式
|
||||||
|
const isEmail = identifier.includes('@');
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const whereCondition = isEmail
|
||||||
|
? eq(sysUsers.email, identifier)
|
||||||
|
: eq(sysUsers.username, identifier);
|
||||||
|
|
||||||
|
const [user] = await db().select({
|
||||||
|
id: sysUsers.id,
|
||||||
|
username: sysUsers.username,
|
||||||
|
email: sysUsers.email,
|
||||||
|
status: sysUsers.status,
|
||||||
|
passwordHash: sysUsers.passwordHash,
|
||||||
|
lastLoginAt: sysUsers.lastLoginAt
|
||||||
|
})
|
||||||
|
.from(sysUsers)
|
||||||
|
.where(whereCondition)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new BusinessError('用户不存在', ERROR_CODES.USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id.toString(), // 转换为字符串避免精度丢失
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
status: user.status,
|
||||||
|
passwordHash: user.passwordHash,
|
||||||
|
lastLoginAt: user.lastLoginAt
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BusinessError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
Logger.error(new Error(`查找用户失败:${error}`));
|
||||||
|
throw new BusinessError('查找用户失败', ERROR_CODES.INTERNAL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码
|
||||||
|
* @param password 原始密码
|
||||||
|
* @param passwordHash 密码哈希
|
||||||
|
*/
|
||||||
|
private async verifyPassword(password: string, passwordHash: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const isValid = await bcrypt.compare(password, passwordHash);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
// todo 记录错误登录次数,如果超过5次,则锁定账号
|
||||||
|
throw new BusinessError('密码错误', ERROR_CODES.INVALID_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BusinessError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
Logger.error(new Error(`密码验证失败:${error}`));
|
||||||
|
throw new BusinessError('密码验证失败', ERROR_CODES.INTERNAL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查账号状态
|
||||||
|
* @param user 用户信息
|
||||||
|
*/
|
||||||
|
private async checkAccountStatus(user: { status: string }): Promise<void> {
|
||||||
|
if (user.status === 'pending') {
|
||||||
|
throw new BusinessError('账号未激活,请先激活账号', ERROR_CODES.ACCOUNT_NOT_ACTIVATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status === 'locked') {
|
||||||
|
throw new BusinessError('账号已被锁定,请联系管理员', ERROR_CODES.ACCOUNT_LOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status !== 'active') {
|
||||||
|
throw new BusinessError('账号状态异常,请联系管理员', ERROR_CODES.ACCOUNT_LOCKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新最后登录时间
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
private async updateLastLoginTime(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await db().update(sysUsers)
|
||||||
|
.set({ lastLoginAt: new Date().toISOString() })
|
||||||
|
.where(eq(sysUsers.id, BigInt(userId)));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误但不影响登录流程
|
||||||
|
Logger.error(new Error(`更新最后登录时间失败:${error}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录日志
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param identifier 登录标识符
|
||||||
|
*/
|
||||||
|
private async recordLoginLog(userId: string, identifier: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
Logger.info(`用户登录日志:用户ID=${userId}, 标识符=${identifier}, 时间=${new Date().toISOString()}`);
|
||||||
|
|
||||||
|
// TODO: 如果有登录日志表,可以在这里记录到数据库
|
||||||
|
// await db().insert(loginLogs).values({
|
||||||
|
// userId: BigInt(userId),
|
||||||
|
// identifier,
|
||||||
|
// loginTime: new Date().toISOString(),
|
||||||
|
// ip: '0.0.0.0', // 从请求中获取
|
||||||
|
// userAgent: 'unknown' // 从请求中获取
|
||||||
|
// });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误但不影响登录流程
|
||||||
|
Logger.error(new Error(`记录登录日志失败:${error}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送激活成功邮件
|
* 发送激活成功邮件
|
||||||
* @param email 邮箱地址
|
* @param email 邮箱地址
|
||||||
|
@ -8,26 +8,18 @@
|
|||||||
*/
|
*/
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { jwt } from '@elysiajs/jwt';
|
import { jwt } from '@elysiajs/jwt';
|
||||||
import { jwtConfig } from '@/config/jwt.config';
|
|
||||||
import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type';
|
import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type';
|
||||||
|
import { jwtService } from './jwt.service';
|
||||||
export const jwtPlugin = jwt({
|
|
||||||
name: 'jwt',
|
|
||||||
secret: jwtConfig.secret,
|
|
||||||
exp: jwtConfig.exp,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const jwtAuthPlugin = (app: Elysia) =>
|
export const jwtAuthPlugin = (app: Elysia) =>
|
||||||
app
|
app
|
||||||
.use(jwtPlugin)
|
.derive(async ({ headers, status }) => {
|
||||||
.derive(async ({ jwt, headers, status }) => {
|
|
||||||
const authHeader = headers['authorization'];
|
const authHeader = headers['authorization'];
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
return status(401, '未携带Token');
|
return status(401, '未携带Token');
|
||||||
}
|
}
|
||||||
const token = authHeader.replace('Bearer ', '');
|
const token = authHeader.replace('Bearer ', '');
|
||||||
try {
|
try {
|
||||||
const payload = await jwt.verify(token) as JwtPayloadType | false;
|
const payload = jwtService.verifyToken(token) as JwtPayloadType | false;
|
||||||
if (!payload) return status(401, 'Token无效');
|
if (!payload) return status(401, 'Token无效');
|
||||||
|
|
||||||
// 提取用户信息
|
// 提取用户信息
|
||||||
|
@ -1,455 +1,88 @@
|
|||||||
/**
|
/**
|
||||||
* @file JWT服务类
|
* @file JWT服务类 - 原生版
|
||||||
* @author hotok
|
* @author AI Assistant
|
||||||
* @date 2025-06-29
|
* @date 2025-01-07
|
||||||
* @lastEditor hotok
|
* @description 使用原生jsonwebtoken库提供JWT功能
|
||||||
* @lastEditTime 2025-07-06
|
|
||||||
* @description 提供类型安全的JWT生成、验证和管理功能,支持不同类型的token
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import jwt from 'jsonwebtoken';
|
||||||
import { jwtConfig, tokenConfig, getTokenConfig } from '@/config/jwt.config';
|
import { jwtConfig } from '@/config';
|
||||||
import { Logger } from '@/plugins/logger/logger.service';
|
|
||||||
import { TOKEN_TYPES } from '@/type/jwt.type';
|
import { TOKEN_TYPES } from '@/type/jwt.type';
|
||||||
import type {
|
|
||||||
JwtUserType,
|
|
||||||
JwtPayloadType,
|
|
||||||
JwtSignOptionsType,
|
|
||||||
TokenType,
|
|
||||||
BaseJwtPayload,
|
|
||||||
ActivationTokenPayload,
|
|
||||||
AccessTokenPayload,
|
|
||||||
RefreshTokenPayload,
|
|
||||||
PasswordResetTokenPayload,
|
|
||||||
} from '@/type/jwt.type';
|
|
||||||
import type { UserInfoType } from '@/modules/example/example.schema';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT服务类
|
* JWT服务类 - 原生版
|
||||||
* @description 提供JWT Token的生成、验证、刷新等功能,支持不同类型的token
|
|
||||||
*/
|
*/
|
||||||
export class JwtService {
|
export class JwtService {
|
||||||
/**
|
/**
|
||||||
* 生成JWT Token
|
* 生成激活Token
|
||||||
* @param userInfo 完整的用户信息
|
|
||||||
* @param options 可选的JWT配置
|
|
||||||
* @returns Promise<string> JWT Token字符串
|
|
||||||
* @modification hotok 2025-06-29 初始实现JWT生成功能
|
|
||||||
*/
|
*/
|
||||||
async generateToken(userInfo: UserInfoType, options?: Partial<JwtSignOptionsType>): Promise<string> {
|
generateActivationToken(userId: string, email: string, username: string){
|
||||||
try {
|
return jwt.sign(
|
||||||
// 从完整用户信息提取JWT载荷所需的字段
|
{
|
||||||
const jwtUser: JwtUserType = {
|
|
||||||
userId: String(userInfo.id),
|
|
||||||
username: userInfo.username,
|
|
||||||
email: userInfo.email,
|
|
||||||
nickname: userInfo.nickname,
|
|
||||||
status: userInfo.status,
|
|
||||||
role: options?.user?.role, // 如果有传入角色信息
|
|
||||||
};
|
|
||||||
|
|
||||||
// 构建JWT载荷
|
|
||||||
const payload: Omit<JwtPayloadType, 'iat' | 'exp'> = {
|
|
||||||
...jwtUser,
|
|
||||||
sub: String(userInfo.id),
|
|
||||||
iss: options?.issuer || 'elysia-api',
|
|
||||||
aud: options?.audience || 'web-client',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 注意:实际的token生成需要使用Elysia的jwt实例
|
|
||||||
// 这里提供接口定义,具体实现需要在controller中使用jwt.sign
|
|
||||||
const token = 'generated-token-placeholder';
|
|
||||||
|
|
||||||
Logger.info(`JWT Token生成成功,用户ID: ${userInfo.id}, 用户名: ${userInfo.username}`);
|
|
||||||
return token;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`JWT Token生成失败: ${error}`));
|
|
||||||
throw new Error('Token生成失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证JWT Token
|
|
||||||
* @param token JWT Token字符串
|
|
||||||
* @returns Promise<JwtPayloadType | null> 验证成功返回载荷,失败返回null
|
|
||||||
* @modification hotok 2025-06-29 初始实现JWT验证功能
|
|
||||||
*/
|
|
||||||
async verifyToken(token: string): Promise<JwtPayloadType | null> {
|
|
||||||
try {
|
|
||||||
// 注意:实际的token验证需要使用Elysia的jwt实例
|
|
||||||
// 这里提供接口定义,具体实现需要在controller中使用jwt.verify
|
|
||||||
const payload = null as any as JwtPayloadType;
|
|
||||||
|
|
||||||
if (!payload || !payload.userId) {
|
|
||||||
Logger.warn(`JWT Token验证失败:载荷无效`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查Token是否过期
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
if (payload.exp && payload.exp < now) {
|
|
||||||
Logger.warn(`JWT Token已过期,用户ID: ${payload.userId}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`JWT Token验证成功,用户ID: ${payload.userId}`);
|
|
||||||
return payload;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.warn(`JWT Token验证失败: ${error}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新JWT Token
|
|
||||||
* @param oldToken 旧的JWT Token
|
|
||||||
* @param userInfo 最新的用户信息
|
|
||||||
* @returns Promise<string | null> 新的JWT Token,失败返回null
|
|
||||||
* @modification hotok 2025-06-29 初始实现JWT刷新功能
|
|
||||||
*/
|
|
||||||
async refreshToken(oldToken: string, userInfo: UserInfoType): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
// 验证旧Token
|
|
||||||
const oldPayload = await this.verifyToken(oldToken);
|
|
||||||
if (!oldPayload) {
|
|
||||||
Logger.warn('Token刷新失败:旧Token无效');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成新Token
|
|
||||||
const newToken = await this.generateToken(userInfo);
|
|
||||||
|
|
||||||
Logger.info(`JWT Token刷新成功,用户ID: ${userInfo.id}`);
|
|
||||||
return newToken;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`JWT Token刷新失败: ${error}`));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取JWT载荷中的用户信息
|
|
||||||
* @param payload JWT载荷
|
|
||||||
* @returns JwtUserType 用户信息
|
|
||||||
* @modification hotok 2025-06-29 添加用户信息提取功能
|
|
||||||
*/
|
|
||||||
extractUserFromPayload(payload: JwtPayloadType): JwtUserType {
|
|
||||||
return {
|
|
||||||
userId: payload.userId,
|
|
||||||
username: payload.username,
|
|
||||||
email: payload.email,
|
|
||||||
nickname: payload.nickname,
|
|
||||||
status: payload.status,
|
|
||||||
role: payload.role,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查用户Token是否即将过期
|
|
||||||
* @param payload JWT载荷
|
|
||||||
* @param thresholdMinutes 阈值分钟数(默认30分钟)
|
|
||||||
* @returns boolean 是否即将过期
|
|
||||||
*/
|
|
||||||
isTokenExpiringSoon(payload: JwtPayloadType, thresholdMinutes: number = 30): boolean {
|
|
||||||
if (!payload.exp) return false;
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const threshold = thresholdMinutes * 60; // 转换为秒
|
|
||||||
|
|
||||||
return (payload.exp - now) <= threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Token剩余有效时间
|
|
||||||
* @param payload JWT载荷
|
|
||||||
* @returns number 剩余秒数,-1表示已过期
|
|
||||||
*/
|
|
||||||
getTokenRemainingTime(payload: JwtPayloadType): number {
|
|
||||||
if (!payload.exp) return -1;
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const remaining = payload.exp - now;
|
|
||||||
|
|
||||||
return remaining > 0 ? remaining : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成盐值哈希
|
|
||||||
* @param tokenType Token类型
|
|
||||||
* @param userId 用户ID(字符串形式)
|
|
||||||
* @param email 用户邮箱
|
|
||||||
* @returns 盐值哈希
|
|
||||||
*/
|
|
||||||
private generateSaltHash(tokenType: TokenType, userId: string, email: string): string {
|
|
||||||
const config = getTokenConfig(tokenType);
|
|
||||||
const data = `${config.salt}:${userId}:${email}:${Date.now()}`;
|
|
||||||
return crypto.createHash('sha256').update(data).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证盐值哈希
|
|
||||||
* @param saltHash 盐值哈希
|
|
||||||
* @param tokenType Token类型
|
|
||||||
* @param userId 用户ID(字符串形式)
|
|
||||||
* @param email 用户邮箱
|
|
||||||
* @returns 是否有效
|
|
||||||
*/
|
|
||||||
private verifySaltHash(saltHash: string, tokenType: TokenType, userId: string, email: string): boolean {
|
|
||||||
try {
|
|
||||||
// 简单验证,实际应用中可以实现更复杂的验证逻辑
|
|
||||||
return Boolean(saltHash) && saltHash.length === 64; // SHA256哈希长度
|
|
||||||
} catch (error) {
|
|
||||||
Logger.warn(`盐值哈希验证失败: ${error}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成邮箱激活Token
|
|
||||||
* @param userId 用户ID(字符串形式)
|
|
||||||
* @param email 用户邮箱
|
|
||||||
* @param username 用户名
|
|
||||||
* @returns Promise<string> 激活Token
|
|
||||||
*/
|
|
||||||
async generateActivationToken(userId: string, email: string, username: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const config = getTokenConfig(TOKEN_TYPES.ACTIVATION);
|
|
||||||
const saltHash = this.generateSaltHash(TOKEN_TYPES.ACTIVATION, userId, email);
|
|
||||||
|
|
||||||
// 注意:这里返回的是载荷对象,实际的token生成需要在controller中使用jwt.sign
|
|
||||||
const payload: Omit<ActivationTokenPayload, 'iat' | 'exp'> = {
|
|
||||||
userId,
|
userId,
|
||||||
username,
|
|
||||||
email,
|
email,
|
||||||
tokenType: TOKEN_TYPES.ACTIVATION,
|
username,
|
||||||
saltHash,
|
type: TOKEN_TYPES.ACTIVATION,
|
||||||
purpose: 'email_activation',
|
},
|
||||||
iss: jwtConfig.issuer,
|
jwtConfig.secret,
|
||||||
aud: jwtConfig.audience,
|
{ expiresIn: '1D' }
|
||||||
sub: userId,
|
);
|
||||||
};
|
|
||||||
|
|
||||||
Logger.info(`邮箱激活Token载荷生成成功,用户ID: ${userId}, 邮箱: ${email}`);
|
|
||||||
|
|
||||||
// 返回JSON字符串,controller中需要使用jwt.sign进行实际签名
|
|
||||||
return JSON.stringify(payload);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`邮箱激活Token生成失败: ${error}`));
|
|
||||||
throw new Error('激活Token生成失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成访问Token
|
* 生成登录Token对
|
||||||
* @param userInfo 用户信息
|
|
||||||
* @param role 用户角色
|
|
||||||
* @returns Promise<string> 访问Token载荷JSON
|
|
||||||
*/
|
*/
|
||||||
async generateAccessToken(userInfo: {
|
generateTokens(userInfo: {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
status: string;
|
status: string;
|
||||||
}, role?: string): Promise<string> {
|
}, rememberMe = false) {
|
||||||
|
const userPayload = {
|
||||||
|
userId: userInfo.id,
|
||||||
|
username: userInfo.username,
|
||||||
|
email: userInfo.email,
|
||||||
|
nickname: userInfo.nickname,
|
||||||
|
status: userInfo.status,
|
||||||
|
};
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
...userInfo,
|
||||||
|
type: TOKEN_TYPES.ACCESS,
|
||||||
|
},
|
||||||
|
jwtConfig.secret,
|
||||||
|
{ expiresIn: '20M' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshToken = jwt.sign(
|
||||||
|
{
|
||||||
|
...userInfo,
|
||||||
|
type: TOKEN_TYPES.REFRESH,
|
||||||
|
},
|
||||||
|
jwtConfig.secret,
|
||||||
|
{ expiresIn: '14D' }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expiresIn: '20M',
|
||||||
|
refreshExpiresIn: '14D',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证激活Token
|
||||||
|
*/
|
||||||
|
verifyToken(token: string) {
|
||||||
try {
|
try {
|
||||||
const config = getTokenConfig(TOKEN_TYPES.ACCESS);
|
return jwt.verify(token, jwtConfig.secret)
|
||||||
const saltHash = this.generateSaltHash(TOKEN_TYPES.ACCESS, userInfo.id, userInfo.email);
|
} catch {
|
||||||
|
return { valid: false };
|
||||||
const payload: Omit<AccessTokenPayload, 'iat' | 'exp'> = {
|
|
||||||
userId: userInfo.id,
|
|
||||||
username: userInfo.username,
|
|
||||||
email: userInfo.email,
|
|
||||||
nickname: userInfo.nickname,
|
|
||||||
status: userInfo.status,
|
|
||||||
role,
|
|
||||||
tokenType: TOKEN_TYPES.ACCESS,
|
|
||||||
saltHash,
|
|
||||||
iss: jwtConfig.issuer,
|
|
||||||
aud: jwtConfig.audience,
|
|
||||||
sub: userInfo.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.info(`访问Token载荷生成成功,用户ID: ${userInfo.id}, 用户名: ${userInfo.username}`);
|
|
||||||
return JSON.stringify(payload);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`访问Token生成失败: ${error}`));
|
|
||||||
throw new Error('访问Token生成失败');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成刷新Token
|
|
||||||
* @param userId 用户ID(字符串形式)
|
|
||||||
* @param username 用户名
|
|
||||||
* @param email 用户邮箱
|
|
||||||
* @param accessTokenId 关联的访问token ID(可选)
|
|
||||||
* @returns Promise<string> 刷新Token载荷JSON
|
|
||||||
*/
|
|
||||||
async generateRefreshToken(userId: string, username: string, email: string, accessTokenId?: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const config = getTokenConfig(TOKEN_TYPES.REFRESH);
|
|
||||||
const saltHash = this.generateSaltHash(TOKEN_TYPES.REFRESH, userId, email);
|
|
||||||
|
|
||||||
const payload: Omit<RefreshTokenPayload, 'iat' | 'exp'> = {
|
|
||||||
userId,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
tokenType: TOKEN_TYPES.REFRESH,
|
|
||||||
saltHash,
|
|
||||||
accessTokenId,
|
|
||||||
iss: jwtConfig.issuer,
|
|
||||||
aud: jwtConfig.audience,
|
|
||||||
sub: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.info(`刷新Token载荷生成成功,用户ID: ${userId}`);
|
|
||||||
return JSON.stringify(payload);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`刷新Token生成失败: ${error}`));
|
|
||||||
throw new Error('刷新Token生成失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成密码重置Token
|
|
||||||
* @param userId 用户ID(字符串形式)
|
|
||||||
* @param email 用户邮箱
|
|
||||||
* @param username 用户名
|
|
||||||
* @returns Promise<string> 密码重置Token载荷JSON
|
|
||||||
*/
|
|
||||||
async generatePasswordResetToken(userId: string, email: string, username: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const config = getTokenConfig(TOKEN_TYPES.PASSWORD_RESET);
|
|
||||||
const saltHash = this.generateSaltHash(TOKEN_TYPES.PASSWORD_RESET, userId, email);
|
|
||||||
|
|
||||||
const payload: Omit<PasswordResetTokenPayload, 'iat' | 'exp'> = {
|
|
||||||
userId,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
tokenType: TOKEN_TYPES.PASSWORD_RESET,
|
|
||||||
saltHash,
|
|
||||||
purpose: 'password_reset',
|
|
||||||
iss: jwtConfig.issuer,
|
|
||||||
aud: jwtConfig.audience,
|
|
||||||
sub: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.info(`密码重置Token载荷生成成功,用户ID: ${userId}, 邮箱: ${email}`);
|
|
||||||
return JSON.stringify(payload);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`密码重置Token生成失败: ${error}`));
|
|
||||||
throw new Error('密码重置Token生成失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证激活Token载荷
|
|
||||||
* @param payload JWT载荷
|
|
||||||
* @returns 是否有效的激活Token
|
|
||||||
*/
|
|
||||||
verifyActivationTokenPayload(payload: any): payload is ActivationTokenPayload {
|
|
||||||
try {
|
|
||||||
// 检查基础字段
|
|
||||||
if (!payload || typeof payload !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查token类型
|
|
||||||
if (payload.tokenType !== TOKEN_TYPES.ACTIVATION) {
|
|
||||||
Logger.warn(`Token类型不匹配,期望: ${TOKEN_TYPES.ACTIVATION}, 实际: ${payload.tokenType}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查必需字段
|
|
||||||
const requiredFields = ['userId', 'username', 'email', 'saltHash', 'purpose'];
|
|
||||||
for (const field of requiredFields) {
|
|
||||||
if (!payload[field]) {
|
|
||||||
Logger.warn(`激活Token载荷缺少字段: ${field}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查purpose
|
|
||||||
if (payload.purpose !== 'email_activation') {
|
|
||||||
Logger.warn(`激活Token用途不正确: ${payload.purpose}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证盐值哈希
|
|
||||||
if (!this.verifySaltHash(payload.saltHash, TOKEN_TYPES.ACTIVATION, payload.userId, payload.email)) {
|
|
||||||
Logger.warn(`激活Token盐值哈希验证失败`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`激活Token载荷验证成功,用户ID: ${payload.userId}`);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`激活Token载荷验证失败: ${error}`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证访问Token载荷
|
|
||||||
* @param payload JWT载荷
|
|
||||||
* @returns 是否有效的访问Token
|
|
||||||
*/
|
|
||||||
verifyAccessTokenPayload(payload: any): payload is AccessTokenPayload {
|
|
||||||
try {
|
|
||||||
if (!payload || typeof payload !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.tokenType !== TOKEN_TYPES.ACCESS) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredFields = ['userId', 'username', 'email', 'saltHash', 'status'];
|
|
||||||
for (const field of requiredFields) {
|
|
||||||
if (!payload[field]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.verifySaltHash(payload.saltHash, TOKEN_TYPES.ACCESS, payload.userId, payload.email)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(new Error(`访问Token载荷验证失败: ${error}`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Token配置(用于Controller)
|
|
||||||
* @param tokenType Token类型
|
|
||||||
* @returns Token配置
|
|
||||||
*/
|
|
||||||
getTokenConfig(tokenType: TokenType) {
|
|
||||||
return getTokenConfig(tokenType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取JWT基础配置(用于Controller)
|
|
||||||
*/
|
|
||||||
getJwtConfig() {
|
|
||||||
return jwtConfig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出JWT服务实例
|
|
||||||
*/
|
|
||||||
export const jwtService = new JwtService();
|
export const jwtService = new JwtService();
|
@ -70,18 +70,18 @@
|
|||||||
- [x] 1.4 创建auth.controller.ts - 实现注册路由
|
- [x] 1.4 创建auth.controller.ts - 实现注册路由
|
||||||
- [x] 1.5 创建auth.test.ts - 编写注册测试用例
|
- [x] 1.5 创建auth.test.ts - 编写注册测试用例
|
||||||
|
|
||||||
- [ ] 2.0 POST /auth/activate - 邮箱激活接口
|
- [x] 2.0 POST /auth/activate - 邮箱激活接口
|
||||||
- [ ] 2.1 扩展auth.schema.ts - 定义激活Schema
|
- [x] 2.1 扩展auth.schema.ts - 定义激活Schema
|
||||||
- [ ] 2.2 扩展auth.response.ts - 定义激活响应格式
|
- [x] 2.2 扩展auth.response.ts - 定义激活响应格式
|
||||||
- [ ] 2.3 扩展auth.service.ts - 实现激活业务逻辑
|
- [x] 2.3 扩展auth.service.ts - 实现激活业务逻辑
|
||||||
- [ ] 2.4 扩展auth.controller.ts - 实现激活路由
|
- [x] 2.4 扩展auth.controller.ts - 实现激活路由
|
||||||
- [ ] 2.5 扩展auth.test.ts - 编写激活测试用例
|
- [x] 2.5 扩展auth.test.ts - 编写激活测试用例
|
||||||
|
|
||||||
- [ ] 3.0 POST /auth/login - 用户登录接口
|
- [ ] 3.0 POST /auth/login - 用户登录接口
|
||||||
- [ ] 3.1 扩展auth.schema.ts - 定义登录Schema
|
- [x] 3.1 扩展auth.schema.ts - 定义登录Schema
|
||||||
- [ ] 3.2 扩展auth.response.ts - 定义登录响应格式
|
- [x] 3.2 扩展auth.response.ts - 定义登录响应格式
|
||||||
- [ ] 3.3 扩展auth.service.ts - 实现登录业务逻辑
|
- [x] 3.3 扩展auth.service.ts - 实现登录业务逻辑
|
||||||
- [ ] 3.4 扩展auth.controller.ts - 实现登录路由
|
- [x] 3.4 扩展auth.controller.ts - 实现登录路由
|
||||||
- [ ] 3.5 扩展auth.test.ts - 编写登录测试用例
|
- [ ] 3.5 扩展auth.test.ts - 编写登录测试用例
|
||||||
|
|
||||||
- [ ] 4.0 POST /auth/refresh - Token刷新接口
|
- [ ] 4.0 POST /auth/refresh - Token刷新接口
|
||||||
@ -112,12 +112,15 @@
|
|||||||
- [ ] 7.4 扩展auth.controller.ts - 实现重置密码路由
|
- [ ] 7.4 扩展auth.controller.ts - 实现重置密码路由
|
||||||
- [ ] 7.5 扩展auth.test.ts - 编写重置密码测试用例
|
- [ ] 7.5 扩展auth.test.ts - 编写重置密码测试用例
|
||||||
|
|
||||||
- [ ] 8.0 GET /auth/captcha - 图形验证码接口
|
- [x] 8.0 GET /auth/captcha - 图形验证码接口
|
||||||
- [ ] 8.1 扩展auth.schema.ts - 定义验证码Schema
|
- [x] 8.1 扩展auth.schema.ts - 定义验证码Schema
|
||||||
- [ ] 8.2 扩展auth.response.ts - 定义验证码响应格式
|
- [x] 8.2 扩展auth.response.ts - 定义验证码响应格式
|
||||||
- [ ] 8.3 扩展auth.service.ts - 集成验证码服务
|
- [x] 8.3 扩展auth.service.ts - 集成验证码服务
|
||||||
- [ ] 8.4 扩展auth.controller.ts - 实现验证码路由
|
- [x] 8.4 扩展auth.controller.ts - 实现验证码路由
|
||||||
- [ ] 8.5 扩展auth.test.ts - 编写验证码测试用例
|
- [x] 8.5 扩展auth.test.ts - 编写验证码测试用例
|
||||||
|
|
||||||
|
- [ ] 9.0 检查认证模块 (Auth Module)那些接口需要加分布式锁,并加锁
|
||||||
|
|
||||||
|
|
||||||
### 👤 用户管理模块 (User Module) - P0优先级
|
### 👤 用户管理模块 (User Module) - P0优先级
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user