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",
|
||||
"dependencies": {
|
||||
"@elysiajs/jwt": "^1.3.1",
|
||||
"@elysiajs/swagger": "^1.3.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"bcrypt": "^6.0.0",
|
||||
"canvas": "^3.1.2",
|
||||
"chalk": "^5.4.1",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.14.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"nodemailer": "^7.0.4",
|
||||
@ -24,6 +24,7 @@
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/bun": "^1.0.25",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/redis": "^4.0.11",
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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/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-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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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.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=="],
|
||||
|
||||
"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",
|
||||
"chalk": "^5.4.1",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.14.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"nodemailer": "^7.0.4",
|
||||
|
@ -7,7 +7,6 @@
|
||||
* @description 统一导出JWT密钥和过期时间,支持不同类型的token配置
|
||||
*/
|
||||
|
||||
import { TOKEN_TYPES, type TokenType } from '@/type/jwt.type';
|
||||
|
||||
/**
|
||||
* JWT基础配置
|
||||
@ -22,72 +21,4 @@ export const jwtConfig = {
|
||||
issuer: process.env.JWT_ISSUER || 'elysia-api',
|
||||
/** JWT受众 */
|
||||
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 统一错误码定义
|
||||
* @author AI助手
|
||||
* @date 2025-06-29
|
||||
* @lastEditor AI Assistant
|
||||
* @lastEditTime 2025-01-07
|
||||
* @description 定义整个应用的统一错误码,提供类型安全的错误处理
|
||||
*/
|
||||
|
||||
@ -47,6 +49,8 @@ export const ERROR_CODES = {
|
||||
ACCOUNT_NOT_ACTIVATED: 'ACCOUNT_NOT_ACTIVATED', // 账号未激活
|
||||
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED', // 账号被锁定
|
||||
TOO_MANY_FAILED_ATTEMPTS: 'TOO_MANY_FAILED_ATTEMPTS', // 失败次数过多
|
||||
TOO_MANY_ATTEMPTS: 'TOO_MANY_ATTEMPTS', // 登录次数过多
|
||||
CAPTCHA_REQUIRED: 'CAPTCHA_REQUIRED', // 需要验证码
|
||||
|
||||
// 密码重置相关错误
|
||||
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_LOCKED]: 403,
|
||||
[ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: 429,
|
||||
[ERROR_CODES.TOO_MANY_ATTEMPTS]: 429,
|
||||
[ERROR_CODES.CAPTCHA_REQUIRED]: 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_LOCKED]: '账号已被锁定',
|
||||
[ERROR_CODES.TOO_MANY_FAILED_ATTEMPTS]: '登录失败次数过多',
|
||||
[ERROR_CODES.TOO_MANY_ATTEMPTS]: '登录次数过多,请稍后再试',
|
||||
[ERROR_CODES.CAPTCHA_REQUIRED]: '请输入验证码',
|
||||
|
||||
// 密码重置相关错误
|
||||
[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层实现
|
||||
* @author AI Assistant
|
||||
* @date 2024-12-19
|
||||
* @description 认证模块的路由控制器,处理用户注册和邮箱激活相关请求
|
||||
* @lastEditor AI Assistant
|
||||
* @lastEditTime 2025-01-07
|
||||
* @description 认证模块的路由控制器,处理用户注册、邮箱激活、用户登录等请求
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { RegisterSchema, ActivateSchema } from './auth.schema';
|
||||
import { RegisterResponses, ActivateResponses } from './auth.response';
|
||||
import { RegisterSchema, ActivateSchema, LoginSchema } from './auth.schema';
|
||||
import { RegisterResponses, ActivateResponses, LoginResponses } from './auth.response';
|
||||
import { authService } from './auth.service';
|
||||
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(
|
||||
'/register',
|
||||
async ({ body, set }) => {
|
||||
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, set }) => authService.register(body),
|
||||
{
|
||||
body: RegisterSchema,
|
||||
detail: {
|
||||
@ -71,43 +49,7 @@ export const authController = new Elysia()
|
||||
*/
|
||||
.post(
|
||||
'/activate',
|
||||
async ({ body, set }) => {
|
||||
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, set }) => authService.activate(body),
|
||||
{
|
||||
body: ActivateSchema,
|
||||
detail: {
|
||||
@ -118,4 +60,26 @@ export const authController = new Elysia()
|
||||
},
|
||||
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 认证模块响应格式定义
|
||||
* @author AI Assistant
|
||||
* @date 2024-12-19
|
||||
* @description 定义认证模块的用户注册响应格式
|
||||
* @lastEditor AI Assistant
|
||||
* @lastEditTime 2025-01-07
|
||||
* @description 定义认证模块的响应格式,包括用户注册、邮箱激活、用户登录等
|
||||
*/
|
||||
|
||||
import { t, type Static } from 'elysia';
|
||||
@ -224,3 +226,186 @@ export type ActivateSuccessResponse = Static<typeof ActivateSuccessResponseSchem
|
||||
|
||||
/** 邮箱激活失败响应类型 */
|
||||
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 ActivateRequest = Static<typeof ActivateSchema>;
|
||||
|
||||
/** 用户登录请求类型 */
|
||||
export type LoginRequest = Static<typeof LoginSchema>;
|
@ -2,7 +2,9 @@
|
||||
* @file 认证模块Service层实现
|
||||
* @author AI Assistant
|
||||
* @date 2024-12-19
|
||||
* @description 认证模块的用户注册业务逻辑实现
|
||||
* @lastEditor AI Assistant
|
||||
* @lastEditTime 2025-01-07
|
||||
* @description 认证模块的业务逻辑实现,包括用户注册、邮箱激活、用户登录等
|
||||
*/
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
@ -16,12 +18,14 @@ import { successResponse, errorResponse, BusinessError } from '@/utils/response.
|
||||
import { nextId } from '@/utils/snowflake';
|
||||
import { jwtService } from '@/plugins/jwt/jwt.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 {
|
||||
RegisterSuccessResponse,
|
||||
RegisterErrorResponse,
|
||||
ActivateSuccessResponse,
|
||||
ActivateErrorResponse
|
||||
ActivateErrorResponse,
|
||||
LoginSuccessResponse,
|
||||
LoginErrorResponse
|
||||
} 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 邮箱地址
|
||||
|
@ -8,26 +8,18 @@
|
||||
*/
|
||||
import { Elysia } from 'elysia';
|
||||
import { jwt } from '@elysiajs/jwt';
|
||||
import { jwtConfig } from '@/config/jwt.config';
|
||||
import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type';
|
||||
|
||||
export const jwtPlugin = jwt({
|
||||
name: 'jwt',
|
||||
secret: jwtConfig.secret,
|
||||
exp: jwtConfig.exp,
|
||||
});
|
||||
|
||||
import { jwtService } from './jwt.service';
|
||||
export const jwtAuthPlugin = (app: Elysia) =>
|
||||
app
|
||||
.use(jwtPlugin)
|
||||
.derive(async ({ jwt, headers, status }) => {
|
||||
.derive(async ({ headers, status }) => {
|
||||
const authHeader = headers['authorization'];
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return status(401, '未携带Token');
|
||||
}
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
try {
|
||||
const payload = await jwt.verify(token) as JwtPayloadType | false;
|
||||
const payload = jwtService.verifyToken(token) as JwtPayloadType | false;
|
||||
if (!payload) return status(401, 'Token无效');
|
||||
|
||||
// 提取用户信息
|
||||
|
@ -1,455 +1,88 @@
|
||||
/**
|
||||
* @file JWT服务类
|
||||
* @author hotok
|
||||
* @date 2025-06-29
|
||||
* @lastEditor hotok
|
||||
* @lastEditTime 2025-07-06
|
||||
* @description 提供类型安全的JWT生成、验证和管理功能,支持不同类型的token
|
||||
* @file JWT服务类 - 原生版
|
||||
* @author AI Assistant
|
||||
* @date 2025-01-07
|
||||
* @description 使用原生jsonwebtoken库提供JWT功能
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { jwtConfig, tokenConfig, getTokenConfig } from '@/config/jwt.config';
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { jwtConfig } from '@/config';
|
||||
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服务类
|
||||
* @description 提供JWT Token的生成、验证、刷新等功能,支持不同类型的token
|
||||
* JWT服务类 - 原生版
|
||||
*/
|
||||
export class JwtService {
|
||||
/**
|
||||
* 生成JWT Token
|
||||
* @param userInfo 完整的用户信息
|
||||
* @param options 可选的JWT配置
|
||||
* @returns Promise<string> JWT Token字符串
|
||||
* @modification hotok 2025-06-29 初始实现JWT生成功能
|
||||
* 生成激活Token
|
||||
*/
|
||||
async generateToken(userInfo: UserInfoType, options?: Partial<JwtSignOptionsType>): Promise<string> {
|
||||
try {
|
||||
// 从完整用户信息提取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'> = {
|
||||
generateActivationToken(userId: string, email: string, username: string){
|
||||
return jwt.sign(
|
||||
{
|
||||
userId,
|
||||
username,
|
||||
email,
|
||||
tokenType: TOKEN_TYPES.ACTIVATION,
|
||||
saltHash,
|
||||
purpose: 'email_activation',
|
||||
iss: jwtConfig.issuer,
|
||||
aud: jwtConfig.audience,
|
||||
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生成失败');
|
||||
}
|
||||
username,
|
||||
type: TOKEN_TYPES.ACTIVATION,
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: '1D' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问Token
|
||||
* @param userInfo 用户信息
|
||||
* @param role 用户角色
|
||||
* @returns Promise<string> 访问Token载荷JSON
|
||||
* 生成登录Token对
|
||||
*/
|
||||
async generateAccessToken(userInfo: {
|
||||
generateTokens(userInfo: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
nickname?: string;
|
||||
status: string;
|
||||
}, role?: string): Promise<string> {
|
||||
try {
|
||||
const config = getTokenConfig(TOKEN_TYPES.ACCESS);
|
||||
const saltHash = this.generateSaltHash(TOKEN_TYPES.ACCESS, userInfo.id, userInfo.email);
|
||||
|
||||
const payload: Omit<AccessTokenPayload, 'iat' | 'exp'> = {
|
||||
}, rememberMe = false) {
|
||||
const userPayload = {
|
||||
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,
|
||||
};
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
...userInfo,
|
||||
type: TOKEN_TYPES.ACCESS,
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: '20M' }
|
||||
)
|
||||
|
||||
Logger.info(`访问Token载荷生成成功,用户ID: ${userInfo.id}, 用户名: ${userInfo.username}`);
|
||||
return JSON.stringify(payload);
|
||||
const refreshToken = jwt.sign(
|
||||
{
|
||||
...userInfo,
|
||||
type: TOKEN_TYPES.REFRESH,
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: '14D' }
|
||||
)
|
||||
|
||||
} 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,
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: '20M',
|
||||
refreshExpiresIn: '14D',
|
||||
};
|
||||
|
||||
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
|
||||
* 验证激活Token
|
||||
*/
|
||||
async generatePasswordResetToken(userId: string, email: string, username: string): Promise<string> {
|
||||
verifyToken(token: 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生成失败');
|
||||
return jwt.verify(token, jwtConfig.secret)
|
||||
} catch {
|
||||
return { valid: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证激活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();
|
@ -70,18 +70,18 @@
|
||||
- [x] 1.4 创建auth.controller.ts - 实现注册路由
|
||||
- [x] 1.5 创建auth.test.ts - 编写注册测试用例
|
||||
|
||||
- [ ] 2.0 POST /auth/activate - 邮箱激活接口
|
||||
- [ ] 2.1 扩展auth.schema.ts - 定义激活Schema
|
||||
- [ ] 2.2 扩展auth.response.ts - 定义激活响应格式
|
||||
- [ ] 2.3 扩展auth.service.ts - 实现激活业务逻辑
|
||||
- [ ] 2.4 扩展auth.controller.ts - 实现激活路由
|
||||
- [ ] 2.5 扩展auth.test.ts - 编写激活测试用例
|
||||
- [x] 2.0 POST /auth/activate - 邮箱激活接口
|
||||
- [x] 2.1 扩展auth.schema.ts - 定义激活Schema
|
||||
- [x] 2.2 扩展auth.response.ts - 定义激活响应格式
|
||||
- [x] 2.3 扩展auth.service.ts - 实现激活业务逻辑
|
||||
- [x] 2.4 扩展auth.controller.ts - 实现激活路由
|
||||
- [x] 2.5 扩展auth.test.ts - 编写激活测试用例
|
||||
|
||||
- [ ] 3.0 POST /auth/login - 用户登录接口
|
||||
- [ ] 3.1 扩展auth.schema.ts - 定义登录Schema
|
||||
- [ ] 3.2 扩展auth.response.ts - 定义登录响应格式
|
||||
- [ ] 3.3 扩展auth.service.ts - 实现登录业务逻辑
|
||||
- [ ] 3.4 扩展auth.controller.ts - 实现登录路由
|
||||
- [x] 3.1 扩展auth.schema.ts - 定义登录Schema
|
||||
- [x] 3.2 扩展auth.response.ts - 定义登录响应格式
|
||||
- [x] 3.3 扩展auth.service.ts - 实现登录业务逻辑
|
||||
- [x] 3.4 扩展auth.controller.ts - 实现登录路由
|
||||
- [ ] 3.5 扩展auth.test.ts - 编写登录测试用例
|
||||
|
||||
- [ ] 4.0 POST /auth/refresh - Token刷新接口
|
||||
@ -112,12 +112,15 @@
|
||||
- [ ] 7.4 扩展auth.controller.ts - 实现重置密码路由
|
||||
- [ ] 7.5 扩展auth.test.ts - 编写重置密码测试用例
|
||||
|
||||
- [ ] 8.0 GET /auth/captcha - 图形验证码接口
|
||||
- [ ] 8.1 扩展auth.schema.ts - 定义验证码Schema
|
||||
- [ ] 8.2 扩展auth.response.ts - 定义验证码响应格式
|
||||
- [ ] 8.3 扩展auth.service.ts - 集成验证码服务
|
||||
- [ ] 8.4 扩展auth.controller.ts - 实现验证码路由
|
||||
- [ ] 8.5 扩展auth.test.ts - 编写验证码测试用例
|
||||
- [x] 8.0 GET /auth/captcha - 图形验证码接口
|
||||
- [x] 8.1 扩展auth.schema.ts - 定义验证码Schema
|
||||
- [x] 8.2 扩展auth.response.ts - 定义验证码响应格式
|
||||
- [x] 8.3 扩展auth.service.ts - 集成验证码服务
|
||||
- [x] 8.4 扩展auth.controller.ts - 实现验证码路由
|
||||
- [x] 8.5 扩展auth.test.ts - 编写验证码测试用例
|
||||
|
||||
- [ ] 9.0 检查认证模块 (Auth Module)那些接口需要加分布式锁,并加锁
|
||||
|
||||
|
||||
### 👤 用户管理模块 (User Module) - P0优先级
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user