forked from datawhale/whale-town-end
feat:简单添加管理员后台功能
This commit is contained in:
@@ -8,6 +8,7 @@ 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 { RedisModule } from './core/redis/redis.module';
|
||||
import { AdminModule } from './business/admin/admin.module';
|
||||
|
||||
/**
|
||||
* 检查数据库配置是否完整 by angjustinl 2025-12-17
|
||||
@@ -61,6 +62,7 @@ function isDatabaseConfigured(): boolean {
|
||||
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||
LoginCoreModule,
|
||||
LoginModule,
|
||||
AdminModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
75
src/business/admin/admin.controller.ts
Normal file
75
src/business/admin/admin.controller.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 管理员控制器
|
||||
*
|
||||
* API端点:
|
||||
* - POST /admin/auth/login 管理员登录
|
||||
* - GET /admin/users 用户列表(需要管理员Token)
|
||||
* - GET /admin/users/:id 用户详情(需要管理员Token)
|
||||
* - POST /admin/users/:id/reset-password 重置指定用户密码(需要管理员Token)
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Post, Query, UseGuards, ValidationPipe, UsePipes } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../core/guards/admin.guard';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminLoginDto, AdminResetPasswordDto } from '../../dto/admin.dto';
|
||||
import { AdminLoginResponseDto, AdminUsersResponseDto, AdminCommonResponseDto, AdminUserResponseDto } from '../../dto/admin_response.dto';
|
||||
|
||||
@ApiTags('admin')
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
@ApiOperation({ summary: '管理员登录', description: '仅允许 role=9 的账户登录后台' })
|
||||
@ApiBody({ type: AdminLoginDto })
|
||||
@ApiResponse({ status: 200, description: '登录成功', type: AdminLoginResponseDto })
|
||||
@Post('auth/login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async login(@Body() dto: AdminLoginDto) {
|
||||
return await this.adminService.login(dto.identifier, dto.password);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '获取用户列表', description: '后台用户管理:分页获取用户列表' })
|
||||
@ApiQuery({ name: 'limit', required: false, description: '返回数量(默认100)' })
|
||||
@ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: AdminUsersResponseDto })
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('users')
|
||||
async listUsers(
|
||||
@Query('limit') limit?: string,
|
||||
@Query('offset') offset?: string,
|
||||
) {
|
||||
const parsedLimit = limit ? Number(limit) : 100;
|
||||
const parsedOffset = offset ? Number(offset) : 0;
|
||||
return await this.adminService.listUsers(parsedLimit, parsedOffset);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '获取用户详情' })
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiResponse({ status: 200, description: '获取成功', type: AdminUserResponseDto })
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('users/:id')
|
||||
async getUser(@Param('id') id: string) {
|
||||
return await this.adminService.getUser(BigInt(id));
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '重置用户密码', description: '管理员直接为用户设置新密码(需满足密码强度规则)' })
|
||||
@ApiParam({ name: 'id', description: '用户ID' })
|
||||
@ApiBody({ type: AdminResetPasswordDto })
|
||||
@ApiResponse({ status: 200, description: '重置成功', type: AdminCommonResponseDto })
|
||||
@UseGuards(AdminGuard)
|
||||
@Post('users/:id/reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async resetPassword(@Param('id') id: string, @Body() dto: AdminResetPasswordDto) {
|
||||
return await this.adminService.resetPassword(BigInt(id), dto.new_password);
|
||||
}
|
||||
}
|
||||
24
src/business/admin/admin.module.ts
Normal file
24
src/business/admin/admin.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 管理员业务模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供后台管理的HTTP API(管理员登录、用户管理、密码重置等)
|
||||
* - 仅负责HTTP层与业务流程编排
|
||||
* - 核心鉴权与密码策略由 AdminCoreService 提供
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminCoreModule } from '../../core/admin_core/admin_core.module';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
|
||||
@Module({
|
||||
imports: [AdminCoreModule],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
100
src/business/admin/admin.service.ts
Normal file
100
src/business/admin/admin.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 管理员业务服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 调用核心服务完成管理员登录
|
||||
* - 提供用户列表查询
|
||||
* - 提供用户密码重置能力
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Logger, NotFoundException } 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';
|
||||
|
||||
export interface AdminApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message: string;
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
private readonly logger = new Logger(AdminService.name);
|
||||
|
||||
constructor(
|
||||
private readonly adminCoreService: AdminCoreService,
|
||||
@Inject('UsersService') private readonly usersService: UsersService | UsersMemoryService,
|
||||
) {}
|
||||
|
||||
async login(identifier: string, password: string): Promise<AdminApiResponse> {
|
||||
try {
|
||||
const result = await this.adminCoreService.login({ identifier, password });
|
||||
return { success: true, data: result, message: '管理员登录成功' };
|
||||
} catch (error) {
|
||||
this.logger.error(`管理员登录失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '管理员登录失败',
|
||||
error_code: 'ADMIN_LOGIN_FAILED',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async listUsers(limit: number, offset: number): Promise<AdminApiResponse<{ users: any[]; limit: number; offset: number }>> {
|
||||
const users = await this.usersService.findAll(limit, offset);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
users: users.map((u: Users) => this.formatUser(u)),
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
message: '用户列表获取成功',
|
||||
};
|
||||
}
|
||||
|
||||
async getUser(id: bigint): Promise<AdminApiResponse<{ user: any }>> {
|
||||
const user = await this.usersService.findOne(id);
|
||||
return {
|
||||
success: true,
|
||||
data: { user: this.formatUser(user) },
|
||||
message: '用户信息获取成功',
|
||||
};
|
||||
}
|
||||
|
||||
async resetPassword(id: bigint, newPassword: string): Promise<AdminApiResponse> {
|
||||
// 确认用户存在
|
||||
const user = await this.usersService.findOne(id).catch((): null => null);
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
await this.adminCoreService.resetUserPassword(id, newPassword);
|
||||
|
||||
this.logger.log(`管理员重置密码成功: userId=${id.toString()}`);
|
||||
|
||||
return { success: true, message: '密码重置成功' };
|
||||
}
|
||||
|
||||
private formatUser(user: Users) {
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
email: user.email,
|
||||
email_verified: user.email_verified,
|
||||
phone: user.phone,
|
||||
avatar_url: user.avatar_url,
|
||||
role: user.role,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
27
src/core/admin_core/admin_core.module.ts
Normal file
27
src/core/admin_core/admin_core.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 管理员核心模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供管理员登录鉴权能力(签名Token)
|
||||
* - 提供管理员账户启动引导(可选)
|
||||
* - 为业务层 AdminModule 提供可复用的核心服务
|
||||
*
|
||||
* 依赖模块:
|
||||
* - UsersModule: 用户数据访问(数据库/内存双模式)
|
||||
* - ConfigModule: 环境变量与配置读取
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AdminCoreService } from './admin_core.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [AdminCoreService],
|
||||
exports: [AdminCoreService],
|
||||
})
|
||||
export class AdminCoreModule {}
|
||||
285
src/core/admin_core/admin_core.service.ts
Normal file
285
src/core/admin_core/admin_core.service.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 管理员核心服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 管理员登录校验(仅允许 role=9)
|
||||
* - 生成/验证管理员签名Token(HMAC-SHA256)
|
||||
* - 启动时可选引导创建管理员账号(通过环境变量启用)
|
||||
*
|
||||
* 安全说明:
|
||||
* - 本服务生成的Token为对称签名Token(非JWT标准实现),但具备有效期与签名校验
|
||||
* - 仅用于后台管理用途;生产环境务必配置强随机的 ADMIN_TOKEN_SECRET
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { BadRequestException, Inject, Injectable, Logger, OnModuleInit, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
import { Users } from '../db/users/users.entity';
|
||||
import { UsersService } from '../db/users/users.service';
|
||||
import { UsersMemoryService } from '../db/users/users_memory.service';
|
||||
|
||||
export interface AdminLoginRequest {
|
||||
identifier: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AdminAuthPayload {
|
||||
adminId: string;
|
||||
username: string;
|
||||
role: number;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export interface AdminLoginResult {
|
||||
admin: {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
role: number;
|
||||
};
|
||||
access_token: string;
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminCoreService implements OnModuleInit {
|
||||
private readonly logger = new Logger(AdminCoreService.name);
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@Inject('UsersService') private readonly usersService: UsersService | UsersMemoryService,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
await this.bootstrapAdminIfEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
async login(request: AdminLoginRequest): Promise<AdminLoginResult> {
|
||||
const { identifier, password } = request;
|
||||
|
||||
const adminUser = await this.findUserByIdentifier(identifier);
|
||||
if (!adminUser) {
|
||||
throw new UnauthorizedException('管理员账号不存在');
|
||||
}
|
||||
|
||||
if (adminUser.role !== 9) {
|
||||
throw new UnauthorizedException('无管理员权限');
|
||||
}
|
||||
|
||||
if (!adminUser.password_hash) {
|
||||
throw new UnauthorizedException('管理员账户未设置密码,无法登录');
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(password, adminUser.password_hash);
|
||||
if (!ok) {
|
||||
throw new UnauthorizedException('密码错误');
|
||||
}
|
||||
|
||||
const ttlSeconds = this.getAdminTokenTtlSeconds();
|
||||
const now = Date.now();
|
||||
const payload: AdminAuthPayload = {
|
||||
adminId: adminUser.id.toString(),
|
||||
username: adminUser.username,
|
||||
role: adminUser.role,
|
||||
iat: now,
|
||||
exp: now + ttlSeconds * 1000,
|
||||
};
|
||||
|
||||
const token = this.signPayload(payload);
|
||||
|
||||
return {
|
||||
admin: {
|
||||
id: adminUser.id.toString(),
|
||||
username: adminUser.username,
|
||||
nickname: adminUser.nickname,
|
||||
role: adminUser.role,
|
||||
},
|
||||
access_token: token,
|
||||
expires_at: payload.exp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验管理员Token并返回Payload
|
||||
*/
|
||||
verifyToken(token: string): AdminAuthPayload {
|
||||
const secret = this.getAdminTokenSecret();
|
||||
const [payloadPart, signaturePart] = token.split('.');
|
||||
|
||||
if (!payloadPart || !signaturePart) {
|
||||
throw new UnauthorizedException('Token格式错误');
|
||||
}
|
||||
|
||||
const expected = this.hmacSha256Base64Url(payloadPart, secret);
|
||||
if (!this.safeEqual(signaturePart, expected)) {
|
||||
throw new UnauthorizedException('Token签名无效');
|
||||
}
|
||||
|
||||
const payloadJson = Buffer.from(this.base64UrlToBase64(payloadPart), 'base64').toString('utf-8');
|
||||
let payload: AdminAuthPayload;
|
||||
|
||||
try {
|
||||
payload = JSON.parse(payloadJson) as AdminAuthPayload;
|
||||
} catch {
|
||||
throw new UnauthorizedException('Token解析失败');
|
||||
}
|
||||
|
||||
if (!payload?.adminId || payload.role !== 9) {
|
||||
throw new UnauthorizedException('无管理员权限');
|
||||
}
|
||||
|
||||
if (typeof payload.exp !== 'number' || Date.now() > payload.exp) {
|
||||
throw new UnauthorizedException('Token已过期');
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员重置用户密码(直接设置新密码)
|
||||
*/
|
||||
async resetUserPassword(userId: bigint, newPassword: string): Promise<void> {
|
||||
this.validatePasswordStrength(newPassword);
|
||||
|
||||
const passwordHash = await this.hashPassword(newPassword);
|
||||
await this.usersService.update(userId, { password_hash: passwordHash });
|
||||
}
|
||||
|
||||
private async findUserByIdentifier(identifier: string): Promise<Users | null> {
|
||||
const byUsername = await this.usersService.findByUsername(identifier);
|
||||
if (byUsername) return byUsername;
|
||||
|
||||
if (this.isEmail(identifier)) {
|
||||
const byEmail = await this.usersService.findByEmail(identifier);
|
||||
if (byEmail) return byEmail;
|
||||
}
|
||||
|
||||
if (this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll(1000, 0);
|
||||
return users.find((u: Users) => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async bootstrapAdminIfEnabled(): Promise<void> {
|
||||
const enabled = this.configService.get<string>('ADMIN_BOOTSTRAP_ENABLED', 'false') === 'true';
|
||||
if (!enabled) return;
|
||||
|
||||
const username = this.configService.get<string>('ADMIN_USERNAME');
|
||||
const password = this.configService.get<string>('ADMIN_PASSWORD');
|
||||
const nickname = this.configService.get<string>('ADMIN_NICKNAME', '管理员');
|
||||
|
||||
if (!username || !password) {
|
||||
this.logger.warn('已启用管理员引导,但未配置 ADMIN_USERNAME / ADMIN_PASSWORD,跳过创建');
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await this.usersService.findByUsername(username);
|
||||
if (existing) {
|
||||
if (existing.role !== 9) {
|
||||
this.logger.warn(`管理员引导发现同名用户但role!=9:${username},跳过`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.validatePasswordStrength(password);
|
||||
const passwordHash = await this.hashPassword(password);
|
||||
|
||||
await this.usersService.create({
|
||||
username,
|
||||
password_hash: passwordHash,
|
||||
nickname,
|
||||
role: 9,
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
this.logger.log(`管理员账号已创建:${username} (role=9)`);
|
||||
}
|
||||
|
||||
private getAdminTokenSecret(): string {
|
||||
const secret = this.configService.get<string>('ADMIN_TOKEN_SECRET');
|
||||
if (!secret || secret.length < 16) {
|
||||
throw new BadRequestException('ADMIN_TOKEN_SECRET 未配置或过短(至少16字符)');
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
private getAdminTokenTtlSeconds(): number {
|
||||
const raw = this.configService.get<string>('ADMIN_TOKEN_TTL_SECONDS', '28800'); // 8h
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return 28800;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private signPayload(payload: AdminAuthPayload): string {
|
||||
const secret = this.getAdminTokenSecret();
|
||||
const payloadJson = JSON.stringify(payload);
|
||||
const payloadPart = this.base64ToBase64Url(Buffer.from(payloadJson, 'utf-8').toString('base64'));
|
||||
const signature = this.hmacSha256Base64Url(payloadPart, secret);
|
||||
return `${payloadPart}.${signature}`;
|
||||
}
|
||||
|
||||
private hmacSha256Base64Url(data: string, secret: string): string {
|
||||
const digest = crypto.createHmac('sha256', secret).update(data).digest('base64');
|
||||
return this.base64ToBase64Url(digest);
|
||||
}
|
||||
|
||||
private base64ToBase64Url(base64: string): string {
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
private base64UrlToBase64(base64Url: string): string {
|
||||
const padded = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padLen = (4 - (padded.length % 4)) % 4;
|
||||
return padded + '='.repeat(padLen);
|
||||
}
|
||||
|
||||
private safeEqual(a: string, b: string): boolean {
|
||||
const aBuf = Buffer.from(a);
|
||||
const bBuf = Buffer.from(b);
|
||||
if (aBuf.length !== bBuf.length) return false;
|
||||
return crypto.timingSafeEqual(aBuf, bBuf);
|
||||
}
|
||||
|
||||
private isEmail(value: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||
}
|
||||
|
||||
private isPhoneNumber(value: string): boolean {
|
||||
return /^\+?[0-9\-\s]{6,20}$/.test(value);
|
||||
}
|
||||
|
||||
private validatePasswordStrength(password: string): void {
|
||||
if (password.length < 8) {
|
||||
throw new BadRequestException('密码长度至少8位');
|
||||
}
|
||||
|
||||
if (password.length > 128) {
|
||||
throw new BadRequestException('密码长度不能超过128位');
|
||||
}
|
||||
|
||||
const hasLetter = /[a-zA-Z]/.test(password);
|
||||
const hasNumber = /\d/.test(password);
|
||||
|
||||
if (!hasLetter || !hasNumber) {
|
||||
throw new BadRequestException('密码必须包含字母和数字');
|
||||
}
|
||||
}
|
||||
|
||||
private async hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 12;
|
||||
return await bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
}
|
||||
43
src/core/guards/admin.guard.ts
Normal file
43
src/core/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 '../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;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ describe('LoginCoreService', () => {
|
||||
providers: [
|
||||
LoginCoreService,
|
||||
{
|
||||
provide: UsersService,
|
||||
provide: 'UsersService',
|
||||
useValue: mockUsersService,
|
||||
},
|
||||
{
|
||||
@@ -70,7 +70,7 @@ describe('LoginCoreService', () => {
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoginCoreService>(LoginCoreService);
|
||||
usersService = module.get(UsersService);
|
||||
usersService = module.get('UsersService');
|
||||
emailService = module.get(EmailService);
|
||||
verificationService = module.get(VerificationService);
|
||||
});
|
||||
|
||||
34
src/dto/admin.dto.ts
Normal file
34
src/dto/admin.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;
|
||||
}
|
||||
139
src/dto/admin_response.dto.ts
Normal file
139
src/dto/admin_response.dto.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 管理员相关响应 DTO
|
||||
*
|
||||
* 功能描述:
|
||||
* - 为 Swagger 提供明确的响应结构定义
|
||||
* - 与 AdminService 返回结构保持一致
|
||||
*
|
||||
* @author jianuo
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-19
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
class AdminInfoDto {
|
||||
@ApiProperty({ example: '1' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ example: 'admin' })
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ example: '管理员' })
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({ example: 9 })
|
||||
role: number;
|
||||
}
|
||||
|
||||
class AdminLoginDataDto {
|
||||
@ApiProperty({ type: AdminInfoDto })
|
||||
admin: AdminInfoDto;
|
||||
|
||||
@ApiProperty({ description: '管理员访问Token(用于Authorization Bearer)' })
|
||||
access_token: string;
|
||||
|
||||
@ApiProperty({ description: '过期时间戳(毫秒)', example: 1766102400000 })
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export class AdminLoginResponseDto {
|
||||
@ApiProperty({ example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ type: AdminLoginDataDto, required: false })
|
||||
data?: AdminLoginDataDto;
|
||||
|
||||
@ApiProperty({ example: '管理员登录成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ required: false, example: 'ADMIN_LOGIN_FAILED' })
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
class AdminUserDto {
|
||||
@ApiProperty({ example: '1' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ example: 'user1' })
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ example: '小明' })
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({ required: false, example: 'user1@example.com', nullable: true })
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({ example: false })
|
||||
email_verified: boolean;
|
||||
|
||||
@ApiProperty({ required: false, example: '+8613800138000', nullable: true })
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({ required: false, example: 'https://example.com/avatar.png', nullable: true })
|
||||
avatar_url?: string;
|
||||
|
||||
@ApiProperty({ example: 1 })
|
||||
role: number;
|
||||
|
||||
@ApiProperty({ example: '2025-12-19T00:00:00.000Z' })
|
||||
created_at: Date;
|
||||
|
||||
@ApiProperty({ example: '2025-12-19T00:00:00.000Z' })
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
class AdminUsersDataDto {
|
||||
@ApiProperty({ type: [AdminUserDto] })
|
||||
users: AdminUserDto[];
|
||||
|
||||
@ApiProperty({ example: 100 })
|
||||
limit: number;
|
||||
|
||||
@ApiProperty({ example: 0 })
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export class AdminUsersResponseDto {
|
||||
@ApiProperty({ example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ type: AdminUsersDataDto, required: false })
|
||||
data?: AdminUsersDataDto;
|
||||
|
||||
@ApiProperty({ example: '用户列表获取成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ required: false, example: 'ADMIN_USERS_FAILED' })
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
class AdminUserDataDto {
|
||||
@ApiProperty({ type: AdminUserDto })
|
||||
user: AdminUserDto;
|
||||
}
|
||||
|
||||
export class AdminUserResponseDto {
|
||||
@ApiProperty({ example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ type: AdminUserDataDto, required: false })
|
||||
data?: AdminUserDataDto;
|
||||
|
||||
@ApiProperty({ example: '用户信息获取成功' })
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class AdminCommonResponseDto {
|
||||
@ApiProperty({ example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
data?: any;
|
||||
|
||||
@ApiProperty({ example: '密码重置成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ required: false, example: 'ADMIN_OPERATION_FAILED' })
|
||||
error_code?: string;
|
||||
}
|
||||
@@ -39,6 +39,12 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ['error', 'warn', 'log'],
|
||||
});
|
||||
|
||||
// 允许前端后台(如Vite/React)跨域访问
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// 全局启用校验管道(核心配置)
|
||||
app.useGlobalPipes(
|
||||
@@ -55,6 +61,7 @@ async function bootstrap() {
|
||||
.setDescription('像素游戏服务器API文档 - 包含用户认证、登录注册等功能')
|
||||
.setVersion('1.0.0')
|
||||
.addTag('auth', '用户认证相关接口')
|
||||
.addTag('admin', '管理员后台相关接口')
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: 'http',
|
||||
|
||||
Reference in New Issue
Block a user