refactor: 更新核心服务和应用配置

- 更新用户实体和DTO结构
- 重构用户服务逻辑
- 更新登录核心服务
- 调整应用模块配置以适配新的业务模块结构
- 更新应用控制器和服务
This commit is contained in:
moyin
2025-12-24 18:04:53 +08:00
parent 47a738067a
commit e6d8c28806
11 changed files with 132 additions and 136 deletions

View File

@@ -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';
/**
* 应用根控制器

View File

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

View File

@@ -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';
/**
* 应用服务类

View File

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

View File

@@ -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;
/**
* 创建时间
*

View File

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

View File

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

View File

@@ -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<AdminCoreService, 'verifyToken'> = {
verifyToken: jest.fn(),
};
const makeContext = (authorization?: any) => {
const req: any = { headers: {} };
if (authorization !== undefined) {
req.headers['authorization'] = authorization;
}
const ctx: Partial<ExecutionContext> = {
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);
});
});

View File

@@ -1,43 +0,0 @@
/**
* 管理员鉴权守卫
*
* 功能描述:
* - 保护后台管理接口
* - 校验 Authorization: Bearer <admin_token>
* - 仅允许 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<AdminRequest>();
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;
}
}

View File

@@ -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', () => {

View File

@@ -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邮箱直接验证
});