feat: 用户登录

1. 同意HTTP返回类型,写方法
2. 优化错误拦截,返回正确的错误类型
3. 优化auth中的返回类型
This commit is contained in:
expressgy 2025-07-06 03:45:31 +08:00
parent ad9bf3896b
commit 541dd50ea3
12 changed files with 606 additions and 601 deletions

View File

@ -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=="],

View File

@ -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",

View File

@ -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}`);
}
}

View File

@ -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
View 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()

View File

@ -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,
}
);

View File

@ -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: '用户IDbigint类型以字符串形式返回防止精度丢失',
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>;

View File

@ -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>;

View File

@ -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

View File

@ -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无效');
// 提取用户信息

View File

@ -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 Tokennull
* @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();

View File

@ -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优先级