import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { Request } from 'express'; import { ADMIN_PUBLIC_KEY } from '../decorators/admin-public.decorator'; import * as crypto from 'crypto'; function sha256(input: string): string { return crypto.createHash('sha256').update(input).digest('hex'); } @Injectable() export class AdminAuthGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly prisma: PrismaService, ) {} async canActivate(context: ExecutionContext): Promise { const isPublic = this.reflector.getAllAndOverride(ADMIN_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; const request = context.switchToHttp().getRequest(); // Try JWT Bearer token first, then x-api-key const token = this.extractToken(request); if (token) { return this.authenticateByJwt(request, token); } const apiKey = this.extractApiKey(request); if (apiKey) { return this.authenticateByApiKey(request, apiKey); } throw new UnauthorizedException('请先登录'); } private async authenticateByJwt(request: Request, token: string): Promise { try { const payload = await this.jwtService.verifyAsync(token, { secret: this.configService.get('jwt.adminSecret'), }); if (payload.type !== 'admin') { throw new UnauthorizedException('无效的管理员令牌'); } const adminUser = await this.prisma.adminUser.findUnique({ where: { id: payload.sub }, }); if (!adminUser || adminUser.deletedAt) { throw new UnauthorizedException('管理员账号不存在'); } if (adminUser.status !== 'ACTIVE') { throw new UnauthorizedException('管理员账号已被禁用'); } if (adminUser.lockedUntil && new Date(adminUser.lockedUntil) > new Date()) { throw new UnauthorizedException('管理员账号已被锁定,请稍后再试'); } if (payload.sessionId) { const session = await this.prisma.adminSession.findUnique({ where: { id: payload.sessionId }, }); if (!session || session.revokedAt) { throw new UnauthorizedException('会话已失效'); } if (new Date(session.expiresAt) < new Date()) { throw new UnauthorizedException('会话已过期,请重新登录'); } } (request as any).adminUser = adminUser; return true; } catch (err) { if (err instanceof UnauthorizedException) throw err; throw new UnauthorizedException('登录已过期,请重新登录'); } } private async authenticateByApiKey(request: Request, rawKey: string): Promise { const keyHash = sha256(rawKey); const apiKey = await this.prisma.adminApiKey.findUnique({ where: { keyHash } }); if (!apiKey) { throw new UnauthorizedException('无效的 API Key'); } if (apiKey.expiresAt && new Date(apiKey.expiresAt) < new Date()) { throw new UnauthorizedException('API Key 已过期'); } const adminUser = await this.prisma.adminUser.findUnique({ where: { id: apiKey.adminUserId }, }); if (!adminUser || adminUser.deletedAt) { throw new UnauthorizedException('管理员账号不存在'); } if (adminUser.status !== 'ACTIVE') { throw new UnauthorizedException('管理员账号已被禁用'); } if (adminUser.lockedUntil && new Date(adminUser.lockedUntil) > new Date()) { throw new UnauthorizedException('管理员账号已被锁定'); } // Update lastUsedAt async (don't block the request) this.prisma.adminApiKey.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() }, }).catch(() => {}); (request as any).adminUser = adminUser; (request as any).apiKeyId = apiKey.id; return true; } private extractToken(request: Request): string | undefined { // 优先从 httpOnly cookie 读取(新前端) const cookieToken = (request as any).cookies?.admin_access_token; if (cookieToken) return cookieToken; // 兼容 Authorization: Bearer header(旧客户端、移动端) const authHeader = request.headers.authorization; if (!authHeader?.startsWith('Bearer ')) return undefined; return authHeader.split(' ')[1]; } private extractApiKey(request: Request): string | undefined { const header = request.headers['x-api-key']; if (typeof header === 'string' && header.length > 0) return header; return undefined; } }