import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { IS_PUBLIC_KEY } from './decorators.js'; import { AuthService } from './auth.service.js'; import { BusinessException } from '../common/business-exception.js'; import { ErrorCodes } from '../common/error-codes.js'; export interface AuthUser { userId: string; tenantCode: string; username: string; super?: boolean; } @Injectable() export class JwtAuthGuard implements CanActivate { constructor( private readonly jwt: JwtService, private readonly reflector: Reflector, private readonly auth: AuthService, ) {} async canActivate(context: ExecutionContext): Promise { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; const request = context.switchToHttp().getRequest(); const url: string = request.originalUrl ?? request.url ?? ''; if (url.startsWith('/docs')) return true; const token = this.extractToken(request); if (!token) throw new UnauthorizedException('未登录'); try { const payload = await this.jwt.verifyAsync<{ sub: string; tenantCode: string; username: string; ver?: number; sid?: string; super?: boolean }>(token); const { active, mustChangePassword } = await this.auth.checkUserActive(payload.tenantCode, payload.sub, payload.ver ?? 0, payload.super ?? false); if (!active) throw new UnauthorizedException('账号已停用或登录已失效'); if (payload.sid && (await this.auth.isSessionRevoked(payload.sid))) { throw new BusinessException(ErrorCodes.AUTH_SESSION_REVOKED, '会话已失效,请重新登录', HttpStatus.UNAUTHORIZED); } if (mustChangePassword && !this.isAllowedWhileMustChange(request)) { throw new BusinessException(ErrorCodes.AUTH_MUST_CHANGE_PWD, '请先修改初始密码', HttpStatus.FORBIDDEN); } request.user = { userId: payload.sub, tenantCode: payload.tenantCode, username: payload.username, super: payload.super ?? false, } satisfies AuthUser; return true; } catch (e) { if (e instanceof HttpException) throw e; throw new UnauthorizedException('token 无效或已过期'); } } private extractToken(request: { headers?: Record }): string | undefined { const auth = request.headers?.authorization; if (!auth) return undefined; const [type, token] = auth.split(' '); return type === 'Bearer' && token ? token : undefined; } /** 待改密门禁白名单:修改本人密码 + 登出(登出目前 @Public,此处兜底防未来改注解)。 */ private isAllowedWhileMustChange(request: { method?: string; originalUrl?: string; url?: string }): boolean { const url: string = (request.originalUrl ?? request.url ?? '').split('?')[0]; if (request.method === 'PATCH' && url.endsWith('/auth/me/password')) return true; if (request.method === 'POST' && url.endsWith('/auth/logout')) return true; return false; } }