feature/refactor-project-structure #20
@@ -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';
|
||||
|
||||
/**
|
||||
* 应用根控制器
|
||||
|
||||
@@ -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('*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* 应用服务类
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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邮箱直接验证
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user