import { ExecutionContext, Injectable, UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { PermissionResolverService } from '../permissions/permission-resolver.service'; import { AuthenticatedUser } from '../types/authenticated-user'; import { ApiKeyPrincipal, ApiKeysService, extractApiKeyToken, } from '../../api-keys/api-keys.service'; interface RequestWithUser { headers: { authorization?: string }; ip?: string; user?: AuthenticatedUser; apiKey?: ApiKeyPrincipal; } /** * Enforces User.isActive and token freshness (issued after the last * password change) on every authenticated request, not just ones gated by * @RequirePermission. Without this, a deactivated user's still-valid access * token - or one stolen before a password change - would keep working on * any route that carries no permission decorator until the token's own * expiry (up to JWT_ACCESS_EXPIRES_IN). isTokenValid shares * PermissionsGuard's cache, so this check costs no extra query beyond what * permission resolution already does. */ @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor( private readonly reflector: Reflector, private readonly permissionResolver: PermissionResolverService, private readonly apiKeys: ApiKeysService, ) { super(); } 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(); // An API key ("Bearer ack_…") authenticates a machine, not a user: no // request.user is set, so it can never pass a user-only route, and // PermissionsGuard checks its scopes instead of a role. const apiKeyToken = extractApiKeyToken(request.headers.authorization); if (apiKeyToken !== null) { // request.ip honours TRUST_PROXY (see app.config.ts). const principal = await this.apiKeys.authenticate( apiKeyToken, request.ip, ); if (!principal) { throw new UnauthorizedException(); } request.apiKey = principal; return true; } const authenticated = (await super.canActivate(context)) as boolean; if (!authenticated) { return false; } const user = request.user; if ( !user || !(await this.permissionResolver.isTokenValid(user.id, user.tokenIssuedAt)) ) { throw new UnauthorizedException(); } return true; } }