From e6d8c288063672d35d877e4915a6ed663cb9b31c Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 24 Dec 2025 18:04:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=9C=8D=E5=8A=A1=E5=92=8C=E5=BA=94=E7=94=A8=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新用户实体和DTO结构 - 重构用户服务逻辑 - 更新登录核心服务 - 调整应用模块配置以适配新的业务模块结构 - 更新应用控制器和服务 --- src/app.controller.ts | 3 +- src/app.module.ts | 37 +++++++-- src/app.service.ts | 2 +- src/core/db/users/users.dto.ts | 30 ++++++- src/core/db/users/users.entity.ts | 39 +++++++++ src/core/db/users/users.service.ts | 2 + src/core/db/users/users_memory.service.ts | 2 + src/core/guards/admin.guard.spec.ts | 81 ------------------- src/core/guards/admin.guard.ts | 43 ---------- .../login_core/login_core.service.spec.ts | 19 ++++- src/core/login_core/login_core.service.ts | 10 ++- 11 files changed, 132 insertions(+), 136 deletions(-) delete mode 100644 src/core/guards/admin.guard.spec.ts delete mode 100644 src/core/guards/admin.guard.ts diff --git a/src/app.controller.ts b/src/app.controller.ts index 5b66006..c9a3aca 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,8 +1,7 @@ import { Controller, Get } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AppService } from './app.service'; -import { AppStatusResponseDto } from './dto/app.dto'; -import { ErrorResponseDto } from './dto/error_response.dto'; +import { AppStatusResponseDto, ErrorResponseDto } from './business/shared'; /** * 应用根控制器 diff --git a/src/app.module.ts b/src/app.module.ts index 4d0b2d2..858b3cd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,14 +1,19 @@ -import { Module } from '@nestjs/common'; +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { LoggerModule } from './core/utils/logger/logger.module'; import { UsersModule } from './core/db/users/users.module'; import { LoginCoreModule } from './core/login_core/login_core.module'; -import { LoginModule } from './business/login/login.module'; +import { AuthModule } from './business/auth/auth.module'; import { RedisModule } from './core/redis/redis.module'; import { AdminModule } from './business/admin/admin.module'; +import { UserMgmtModule } from './business/user-mgmt/user-mgmt.module'; +import { SecurityModule } from './business/security/security.module'; +import { MaintenanceMiddleware } from './business/security/middleware/maintenance.middleware'; +import { ContentTypeMiddleware } from './business/security/middleware/content-type.middleware'; /** * 检查数据库配置是否完整 by angjustinl 2025-12-17 @@ -61,10 +66,32 @@ function isDatabaseConfigured(): boolean { // 根据数据库配置选择用户模块模式 isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(), LoginCoreModule, - LoginModule, + AuthModule, + UserMgmtModule, AdminModule, + SecurityModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + // 注意:全局拦截器现在由SecurityModule提供 + ], }) -export class AppModule {} +export class AppModule implements NestModule { + /** + * 配置中间件 + * + * @param consumer 中间件消费者 + */ + configure(consumer: MiddlewareConsumer) { + // 1. 维护模式中间件 - 最高优先级 + consumer + .apply(MaintenanceMiddleware) + .forRoutes('*'); + + // 2. 内容类型检查中间件 + consumer + .apply(ContentTypeMiddleware) + .forRoutes('*'); + } +} diff --git a/src/app.service.ts b/src/app.service.ts index 59dea0d..ef9dea5 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { AppStatusResponseDto } from './dto/app.dto'; +import { AppStatusResponseDto } from './business/shared'; /** * 应用服务类 diff --git a/src/core/db/users/users.dto.ts b/src/core/db/users/users.dto.ts index 5ca1f43..269b741 100644 --- a/src/core/db/users/users.dto.ts +++ b/src/core/db/users/users.dto.ts @@ -24,8 +24,10 @@ import { Max, IsOptional, Length, - IsNotEmpty + IsNotEmpty, + IsEnum } from 'class-validator'; +import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum'; /** * 创建用户数据传输对象 @@ -232,4 +234,30 @@ export class CreateUserDto { */ @IsOptional() email_verified?: boolean = false; + + /** + * 用户状态 + * + * 业务规则: + * - 可选字段,默认为active(正常状态) + * - 控制用户账户的可用性和权限 + * - 支持多种状态:正常、未激活、锁定、禁用等 + * - 影响用户登录和API访问权限 + * + * 验证规则: + * - 可选字段验证 + * - 枚举类型验证 + * - 默认值:active(正常状态) + * + * 状态说明: + * - active: 正常状态,可以正常使用 + * - inactive: 未激活,需要邮箱验证 + * - locked: 已锁定,临时禁用 + * - banned: 已禁用,管理员操作 + * - deleted: 已删除,软删除状态 + * - pending: 待审核,需要管理员审核 + */ + @IsOptional() + @IsEnum(UserStatus, { message: '用户状态必须是有效的枚举值' }) + status?: UserStatus = UserStatus.ACTIVE; } \ No newline at end of file diff --git a/src/core/db/users/users.entity.ts b/src/core/db/users/users.entity.ts index 3835439..1a785cc 100644 --- a/src/core/db/users/users.entity.ts +++ b/src/core/db/users/users.entity.ts @@ -20,6 +20,7 @@ */ import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum'; /** * 用户实体类 @@ -337,6 +338,44 @@ export class Users { }) role: number; + /** + * 用户状态 + * + * 数据库设计: + * - 类型:VARCHAR(20),存储状态枚举值 + * - 约束:非空、默认值'active' + * - 索引:用于状态查询和统计 + * + * 业务规则: + * - 控制用户账户的可用性和权限 + * - active:正常状态,可以正常使用 + * - inactive:未激活,需要邮箱验证 + * - locked:已锁定,临时禁用 + * - banned:已禁用,管理员操作 + * - deleted:已删除,软删除状态 + * - pending:待审核,需要管理员审核 + * + * 安全控制: + * - 登录时检查状态权限 + * - API访问时验证状态 + * - 状态变更记录审计日志 + * - 支持批量状态管理 + * + * 应用场景: + * - 账户安全管理 + * - 用户生命周期控制 + * - 违规用户处理 + * - 系统维护和升级 + */ + @Column({ + type: 'varchar', + length: 20, + nullable: true, + default: UserStatus.ACTIVE, + comment: '用户状态:active-正常,inactive-未激活,locked-锁定,banned-禁用,deleted-删除,pending-待审核' + }) + status?: UserStatus; + /** * 创建时间 * diff --git a/src/core/db/users/users.service.ts b/src/core/db/users/users.service.ts index 51d77c7..854497b 100644 --- a/src/core/db/users/users.service.ts +++ b/src/core/db/users/users.service.ts @@ -16,6 +16,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, FindOptionsWhere } from 'typeorm'; import { Users } from './users.entity'; import { CreateUserDto } from './users.dto'; +import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @@ -97,6 +98,7 @@ export class UsersService { user.avatar_url = createUserDto.avatar_url || null; user.role = createUserDto.role || 1; user.email_verified = createUserDto.email_verified || false; + user.status = createUserDto.status || UserStatus.ACTIVE; // 保存到数据库 return await this.usersRepository.save(user); diff --git a/src/core/db/users/users_memory.service.ts b/src/core/db/users/users_memory.service.ts index cb515b0..e417423 100644 --- a/src/core/db/users/users_memory.service.ts +++ b/src/core/db/users/users_memory.service.ts @@ -24,6 +24,7 @@ import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; import { Users } from './users.entity'; import { CreateUserDto } from './users.dto'; +import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @@ -98,6 +99,7 @@ export class UsersMemoryService { user.avatar_url = createUserDto.avatar_url || null; user.role = createUserDto.role || 1; user.email_verified = createUserDto.email_verified || false; + user.status = createUserDto.status || UserStatus.ACTIVE; user.created_at = new Date(); user.updated_at = new Date(); diff --git a/src/core/guards/admin.guard.spec.ts b/src/core/guards/admin.guard.spec.ts deleted file mode 100644 index 86df850..0000000 --- a/src/core/guards/admin.guard.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; -import { AdminCoreService, AdminAuthPayload } from '../admin_core/admin_core.service'; -import { AdminGuard } from './admin.guard'; - -describe('AdminGuard', () => { - const payload: AdminAuthPayload = { - adminId: '1', - username: 'admin', - role: 9, - iat: 1, - exp: 2, - }; - - const adminCoreServiceMock: Pick = { - verifyToken: jest.fn(), - }; - - const makeContext = (authorization?: any) => { - const req: any = { headers: {} }; - if (authorization !== undefined) { - req.headers['authorization'] = authorization; - } - - const ctx: Partial = { - switchToHttp: () => ({ - getRequest: () => req, - getResponse: () => ({} as any), - getNext: () => ({} as any), - }), - }; - - return { ctx: ctx as ExecutionContext, req }; - }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should allow access with valid admin token', () => { - (adminCoreServiceMock.verifyToken as jest.Mock).mockReturnValue(payload); - - const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); - const { ctx, req } = makeContext('Bearer valid'); - - expect(guard.canActivate(ctx)).toBe(true); - expect(adminCoreServiceMock.verifyToken).toHaveBeenCalledWith('valid'); - expect(req.admin).toEqual(payload); - }); - - it('should deny access without token', () => { - const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); - const { ctx } = makeContext(undefined); - - expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); - }); - - it('should deny access with invalid Authorization format', () => { - const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); - const { ctx } = makeContext('InvalidFormat'); - - expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); - }); - - it('should deny access when verifyToken throws (invalid/expired)', () => { - (adminCoreServiceMock.verifyToken as jest.Mock).mockImplementation(() => { - throw new UnauthorizedException('Token已过期'); - }); - - const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); - const { ctx } = makeContext('Bearer bad'); - - expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); - }); - - it('should deny access when Authorization header is an array', () => { - const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); - const { ctx } = makeContext(['Bearer token']); - - expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); - }); -}); diff --git a/src/core/guards/admin.guard.ts b/src/core/guards/admin.guard.ts deleted file mode 100644 index d2c8296..0000000 --- a/src/core/guards/admin.guard.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 管理员鉴权守卫 - * - * 功能描述: - * - 保护后台管理接口 - * - 校验 Authorization: Bearer - * - 仅允许 role=9 的管理员访问 - * - * @author jianuo - * @version 1.0.0 - * @since 2025-12-19 - */ - -import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; -import { Request } from 'express'; -import { AdminCoreService, AdminAuthPayload } from '../admin_core/admin_core.service'; - -export interface AdminRequest extends Request { - admin?: AdminAuthPayload; -} - -@Injectable() -export class AdminGuard implements CanActivate { - constructor(private readonly adminCoreService: AdminCoreService) {} - - canActivate(context: ExecutionContext): boolean { - const req = context.switchToHttp().getRequest(); - const auth = req.headers['authorization']; - - if (!auth || Array.isArray(auth)) { - throw new UnauthorizedException('缺少Authorization头'); - } - - const [scheme, token] = auth.split(' '); - if (scheme !== 'Bearer' || !token) { - throw new UnauthorizedException('Authorization格式错误'); - } - - const payload = this.adminCoreService.verifyToken(token); - req.admin = payload; - return true; - } -} diff --git a/src/core/login_core/login_core.service.spec.ts b/src/core/login_core/login_core.service.spec.ts index 0c95a79..6149770 100644 --- a/src/core/login_core/login_core.service.spec.ts +++ b/src/core/login_core/login_core.service.spec.ts @@ -7,7 +7,8 @@ import { LoginCoreService } from './login_core.service'; import { UsersService } from '../db/users/users.service'; import { EmailService } from '../utils/email/email.service'; import { VerificationService } from '../utils/verification/verification.service'; -import { UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; +import { UnauthorizedException, ConflictException, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common'; +import { UserStatus } from '../../business/user-mgmt/enums/user-status.enum'; describe('LoginCoreService', () => { let service: LoginCoreService; @@ -26,6 +27,7 @@ describe('LoginCoreService', () => { avatar_url: null as string | null, role: 1, email_verified: false, + status: UserStatus.ACTIVE, // 使用正确的枚举类型 created_at: new Date(), updated_at: new Date() }; @@ -105,7 +107,9 @@ describe('LoginCoreService', () => { }); it('should throw UnauthorizedException for wrong password', async () => { - usersService.findByUsername.mockResolvedValue(mockUser); + // 创建一个正常状态的用户来测试密码验证 + const activeUser = { ...mockUser, status: UserStatus.ACTIVE }; + usersService.findByUsername.mockResolvedValue(activeUser); jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(false); await expect(service.login({ @@ -113,6 +117,17 @@ describe('LoginCoreService', () => { password: 'wrongpassword' })).rejects.toThrow(UnauthorizedException); }); + + it('should throw ForbiddenException for inactive user', async () => { + // 测试非活跃用户状态 + const inactiveUser = { ...mockUser, status: UserStatus.INACTIVE }; + usersService.findByUsername.mockResolvedValue(inactiveUser); + + await expect(service.login({ + identifier: 'testuser', + password: 'password123' + })).rejects.toThrow(ForbiddenException); + }); }); describe('register', () => { diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts index 7499a4d..edd0708 100644 --- a/src/core/login_core/login_core.service.ts +++ b/src/core/login_core/login_core.service.ts @@ -16,11 +16,12 @@ * @since 2025-12-17 */ -import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, Inject } from '@nestjs/common'; +import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, ForbiddenException, Inject } from '@nestjs/common'; import { Users } from '../db/users/users.entity'; import { UsersService } from '../db/users/users.service'; import { EmailService, EmailSendResult } from '../utils/email/email.service'; import { VerificationService, VerificationCodeType } from '../utils/verification/verification.service'; +import { UserStatus, canUserLogin, getUserStatusErrorMessage } from '../../business/user-mgmt/enums/user-status.enum'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; @@ -140,6 +141,11 @@ export class LoginCoreService { throw new UnauthorizedException('用户名、邮箱或手机号不存在'); } + // 检查用户状态 + if (!canUserLogin(user.status)) { + throw new ForbiddenException(getUserStatusErrorMessage(user.status)); + } + // 检查是否为OAuth用户(没有密码) if (!user.password_hash) { throw new UnauthorizedException('该账户使用第三方登录,请使用对应的登录方式'); @@ -196,6 +202,7 @@ export class LoginCoreService { email, phone, role: 1, // 默认普通用户 + status: UserStatus.ACTIVE, // 默认激活状态 email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证 }); @@ -257,6 +264,7 @@ export class LoginCoreService { github_id, avatar_url, role: 1, // 默认普通用户 + status: UserStatus.ACTIVE, // GitHub用户直接激活 email_verified: email ? true : false // GitHub邮箱直接验证 });