feat: 重构业务模块架构
- 新增auth模块处理认证逻辑 - 新增security模块处理安全相关功能 - 新增user-mgmt模块管理用户相关操作 - 新增shared模块存放共享组件 - 重构admin模块,添加DTO和Guards - 为admin模块添加测试文件结构
This commit is contained in:
@@ -15,10 +15,17 @@
|
||||
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards, ValidationPipe, UsePipes, Res, Logger } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiProduces, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../core/guards/admin.guard';
|
||||
import { AdminGuard } from './guards/admin.guard';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminLoginDto, AdminResetPasswordDto } from '../../dto/admin.dto';
|
||||
import { AdminLoginResponseDto, AdminUsersResponseDto, AdminCommonResponseDto, AdminUserResponseDto, AdminRuntimeLogsResponseDto } from '../../dto/admin_response.dto';
|
||||
import { AdminLoginDto, AdminResetPasswordDto } from './dto/admin-login.dto';
|
||||
import {
|
||||
AdminLoginResponseDto,
|
||||
AdminUsersResponseDto,
|
||||
AdminCommonResponseDto,
|
||||
AdminUserResponseDto,
|
||||
AdminRuntimeLogsResponseDto
|
||||
} from './dto/admin-response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../security/decorators/throttle.decorator';
|
||||
import type { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -35,6 +42,10 @@ export class AdminController {
|
||||
@ApiOperation({ summary: '管理员登录', description: '仅允许 role=9 的账户登录后台' })
|
||||
@ApiBody({ type: AdminLoginDto })
|
||||
@ApiResponse({ status: 200, description: '登录成功', type: AdminLoginResponseDto })
|
||||
@ApiResponse({ status: 401, description: '登录失败' })
|
||||
@ApiResponse({ status: 403, description: '权限不足或账户被禁用' })
|
||||
@ApiResponse({ status: 429, description: '登录尝试过于频繁' })
|
||||
@Throttle(ThrottlePresets.LOGIN)
|
||||
@Post('auth/login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@@ -73,7 +84,9 @@ export class AdminController {
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiBody({ type: AdminResetPasswordDto })
|
||||
@ApiResponse({ status: 200, description: '重置成功', type: AdminCommonResponseDto })
|
||||
@ApiResponse({ status: 429, description: '操作过于频繁' })
|
||||
@UseGuards(AdminGuard)
|
||||
@Throttle(ThrottlePresets.ADMIN_OPERATION)
|
||||
@Post('users/:id/reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
|
||||
@@ -21,5 +21,6 @@ import { AdminService } from './admin.service';
|
||||
imports: [AdminCoreModule, LoggerModule],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService],
|
||||
exports: [AdminService], // 导出AdminService供其他模块使用
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -11,12 +11,21 @@
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { AdminCoreService } from '../../core/admin_core/admin_core.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
import { UsersService } from '../../core/db/users/users.service';
|
||||
import { UsersMemoryService } from '../../core/db/users/users_memory.service';
|
||||
import { LogManagementService } from '../../core/utils/logger/log_management.service';
|
||||
import { UserStatus, getUserStatusDescription } from '../user-mgmt/enums/user-status.enum';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../user-mgmt/dto/user-status.dto';
|
||||
import {
|
||||
UserStatusResponseDto,
|
||||
BatchUserStatusResponseDto,
|
||||
UserStatusStatsResponseDto,
|
||||
UserStatusInfoDto,
|
||||
BatchOperationResultDto
|
||||
} from '../user-mgmt/dto/user-status-response.dto';
|
||||
|
||||
export interface AdminApiResponse<T = any> {
|
||||
success: boolean;
|
||||
@@ -108,8 +117,318 @@ export class AdminService {
|
||||
phone: user.phone,
|
||||
avatar_url: user.avatar_url,
|
||||
role: user.role,
|
||||
status: user.status || UserStatus.ACTIVE, // 兼容旧数据
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化用户状态信息
|
||||
*
|
||||
* @param user 用户实体
|
||||
* @returns 格式化的用户状态信息
|
||||
*/
|
||||
private formatUserStatus(user: Users): UserStatusInfoDto {
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
status: user.status || UserStatus.ACTIVE,
|
||||
status_description: getUserStatusDescription(user.status || UserStatus.ACTIVE),
|
||||
updated_at: user.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员修改指定用户的账户状态,支持激活、锁定、禁用等操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户是否存在
|
||||
* 2. 检查状态变更的合法性
|
||||
* 3. 更新用户状态
|
||||
* 4. 记录状态变更日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param userStatusDto 状态修改数据
|
||||
* @returns 修改结果
|
||||
*
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
* @throws BadRequestException 当状态变更不合法时
|
||||
*/
|
||||
async updateUserStatus(userId: bigint, userStatusDto: UserStatusDto): Promise<UserStatusResponseDto> {
|
||||
try {
|
||||
this.logger.log('开始修改用户状态', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 1. 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
this.logger.warn('修改用户状态失败:用户不存在', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString()
|
||||
});
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 2. 检查状态变更的合法性
|
||||
if (user.status === userStatusDto.status) {
|
||||
this.logger.warn('修改用户状态失败:状态未发生变化', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
currentStatus: user.status,
|
||||
newStatus: userStatusDto.status
|
||||
});
|
||||
throw new BadRequestException('用户状态未发生变化');
|
||||
}
|
||||
|
||||
// 3. 更新用户状态
|
||||
const updatedUser = await this.usersService.update(userId, {
|
||||
status: userStatusDto.status
|
||||
});
|
||||
|
||||
// 4. 记录状态变更日志
|
||||
this.logger.log('用户状态修改成功', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
oldStatus: user.status,
|
||||
newStatus: userStatusDto.status,
|
||||
reason: userStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
user: this.formatUserStatus(updatedUser),
|
||||
reason: userStatusDto.reason
|
||||
},
|
||||
message: '用户状态修改成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('修改用户状态失败', {
|
||||
operation: 'update_user_status',
|
||||
userId: userId.toString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态修改失败',
|
||||
error_code: 'USER_STATUS_UPDATE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改用户状态
|
||||
*
|
||||
* 功能描述:
|
||||
* 管理员批量修改多个用户的账户状态
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证用户ID列表
|
||||
* 2. 逐个处理用户状态修改
|
||||
* 3. 收集成功和失败的结果
|
||||
* 4. 返回批量操作结果
|
||||
*
|
||||
* @param batchUserStatusDto 批量状态修改数据
|
||||
* @returns 批量修改结果
|
||||
*/
|
||||
async batchUpdateUserStatus(batchUserStatusDto: BatchUserStatusDto): Promise<BatchUserStatusResponseDto> {
|
||||
try {
|
||||
this.logger.log('开始批量修改用户状态', {
|
||||
operation: 'batch_update_user_status',
|
||||
userCount: batchUserStatusDto.user_ids.length,
|
||||
newStatus: batchUserStatusDto.status,
|
||||
reason: batchUserStatusDto.reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const successUsers: UserStatusInfoDto[] = [];
|
||||
const failedUsers: Array<{ user_id: string; error: string }> = [];
|
||||
|
||||
// 1. 逐个处理用户状态修改
|
||||
for (const userIdStr of batchUserStatusDto.user_ids) {
|
||||
try {
|
||||
const userId = BigInt(userIdStr);
|
||||
|
||||
// 2. 验证用户是否存在
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) {
|
||||
failedUsers.push({
|
||||
user_id: userIdStr,
|
||||
error: '用户不存在'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 检查状态是否需要变更
|
||||
if (user.status === batchUserStatusDto.status) {
|
||||
failedUsers.push({
|
||||
user_id: userIdStr,
|
||||
error: '用户状态未发生变化'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 更新用户状态
|
||||
const updatedUser = await this.usersService.update(userId, {
|
||||
status: batchUserStatusDto.status
|
||||
});
|
||||
|
||||
successUsers.push(this.formatUserStatus(updatedUser));
|
||||
|
||||
} catch (error) {
|
||||
failedUsers.push({
|
||||
user_id: userIdStr,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 构建批量操作结果
|
||||
const result: BatchOperationResultDto = {
|
||||
success_users: successUsers,
|
||||
failed_users: failedUsers,
|
||||
success_count: successUsers.length,
|
||||
failed_count: failedUsers.length,
|
||||
total_count: batchUserStatusDto.user_ids.length
|
||||
};
|
||||
|
||||
this.logger.log('批量修改用户状态完成', {
|
||||
operation: 'batch_update_user_status',
|
||||
successCount: result.success_count,
|
||||
failedCount: result.failed_count,
|
||||
totalCount: result.total_count,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
result,
|
||||
reason: batchUserStatusDto.reason
|
||||
},
|
||||
message: `批量用户状态修改完成,成功:${result.success_count},失败:${result.failed_count}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('批量修改用户状态失败', {
|
||||
operation: 'batch_update_user_status',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '批量用户状态修改失败',
|
||||
error_code: 'BATCH_USER_STATUS_UPDATE_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户状态统计
|
||||
*
|
||||
* 功能描述:
|
||||
* 获取各种用户状态的数量统计信息
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 查询所有用户
|
||||
* 2. 按状态分组统计
|
||||
* 3. 计算各状态数量
|
||||
* 4. 返回统计结果
|
||||
*
|
||||
* @returns 状态统计信息
|
||||
*/
|
||||
async getUserStatusStats(): Promise<UserStatusStatsResponseDto> {
|
||||
try {
|
||||
this.logger.log('开始获取用户状态统计', {
|
||||
operation: 'get_user_status_stats',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 1. 查询所有用户(这里可以优化为直接查询统计信息)
|
||||
const allUsers = await this.usersService.findAll(10000, 0); // 假设最多1万用户
|
||||
|
||||
// 2. 按状态分组统计
|
||||
const stats = {
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
locked: 0,
|
||||
banned: 0,
|
||||
deleted: 0,
|
||||
pending: 0,
|
||||
total: allUsers.length
|
||||
};
|
||||
|
||||
// 3. 计算各状态数量
|
||||
allUsers.forEach((user: Users) => {
|
||||
const status = user.status || UserStatus.ACTIVE;
|
||||
switch (status) {
|
||||
case UserStatus.ACTIVE:
|
||||
stats.active++;
|
||||
break;
|
||||
case UserStatus.INACTIVE:
|
||||
stats.inactive++;
|
||||
break;
|
||||
case UserStatus.LOCKED:
|
||||
stats.locked++;
|
||||
break;
|
||||
case UserStatus.BANNED:
|
||||
stats.banned++;
|
||||
break;
|
||||
case UserStatus.DELETED:
|
||||
stats.deleted++;
|
||||
break;
|
||||
case UserStatus.PENDING:
|
||||
stats.pending++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log('用户状态统计获取成功', {
|
||||
operation: 'get_user_status_stats',
|
||||
stats,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
message: '用户状态统计获取成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取用户状态统计失败', {
|
||||
operation: 'get_user_status_stats',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '用户状态统计获取失败',
|
||||
error_code: 'USER_STATUS_STATS_FAILED'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/business/admin/dto/admin-login.dto.ts
Normal file
34
src/business/admin/dto/admin-login.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 管理员相关 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员登录与用户密码重置的请求结构
|
||||
* - 使用 class-validator 进行参数校验
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class AdminLoginDto {
|
||||
@ApiProperty({ description: '登录标识符(用户名/邮箱/手机号)', example: 'admin' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
identifier: string;
|
||||
|
||||
@ApiProperty({ description: '密码', example: 'Admin123456' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class AdminResetPasswordDto {
|
||||
@ApiProperty({ description: '新密码(至少8位,包含字母和数字)', example: 'NewPass1234' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
new_password: string;
|
||||
}
|
||||
104
src/business/admin/dto/admin-response.dto.ts
Normal file
104
src/business/admin/dto/admin-response.dto.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 管理员响应 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义管理员相关接口的响应格式
|
||||
* - 提供 Swagger 文档生成支持
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AdminLoginResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '登录成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: 'JWT Token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
|
||||
token?: string;
|
||||
|
||||
@ApiProperty({ description: '管理员信息', required: false })
|
||||
admin?: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
role: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminUsersResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '获取用户列表成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '用户列表', type: 'array' })
|
||||
users?: Array<{
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>;
|
||||
|
||||
@ApiProperty({ description: '总数', example: 100 })
|
||||
total?: number;
|
||||
|
||||
@ApiProperty({ description: '偏移量', example: 0 })
|
||||
offset?: number;
|
||||
|
||||
@ApiProperty({ description: '限制数量', example: 100 })
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export class AdminUserResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '获取用户详情成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '用户信息', required: false })
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_login_at?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminCommonResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '操作成功' })
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class AdminRuntimeLogsResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '消息', example: '获取日志成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '日志内容', type: 'array', items: { type: 'string' } })
|
||||
logs?: string[];
|
||||
|
||||
@ApiProperty({ description: '返回行数', example: 200 })
|
||||
lines?: number;
|
||||
}
|
||||
43
src/business/admin/guards/admin.guard.ts
Normal file
43
src/business/admin/guards/admin.guard.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 管理员鉴权守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 保护后台管理接口
|
||||
* - 校验 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 '../../../core/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;
|
||||
}
|
||||
}
|
||||
24
src/business/admin/index.ts
Normal file
24
src/business/admin/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 管理员模块统一导出
|
||||
*
|
||||
* 功能描述:
|
||||
* - 导出管理员相关的所有组件
|
||||
* - 提供统一的导入入口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
// 控制器
|
||||
export * from './admin.controller';
|
||||
|
||||
// 服务
|
||||
export * from './admin.service';
|
||||
|
||||
// DTO
|
||||
export * from './dto/admin-login.dto';
|
||||
export * from './dto/admin-response.dto';
|
||||
|
||||
// 模块
|
||||
export * from './admin.module';
|
||||
81
src/business/admin/tests
Normal file
81
src/business/admin/tests
Normal file
@@ -0,0 +1,81 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user