import { CanActivate, ExecutionContext, ForbiddenException, Injectable, SetMetadata, createParamDecorator, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Reflector } from '@nestjs/core'; import type { AuthenticatedAdmin } from './jwt.strategy'; /** Marks a route as reachable without a token (login, health checks). */ export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); /** Requires the caller to hold this rid (ancestors count — see expandWithParents). */ export const REQUIRED_RID_KEY = 'requiredRid'; export const RequirePermission = (rid: string) => SetMetadata(REQUIRED_RID_KEY, rid); /** Injects the authenticated admin: `@CurrentAdmin() admin: AuthenticatedAdmin`. */ export const CurrentAdmin = createParamDecorator( (_data: unknown, ctx: ExecutionContext): AuthenticatedAdmin => ctx.switchToHttp().getRequest().user, ); /** * JWT guard that honours @Public(). * * Registered globally in AuthModule, so every route is authenticated by default * and opting out is explicit. The legacy app had the inverse default — its router * guard skipped a hardcoded `noneAuthUrl` list — which meant any route added * without a `meta.role` was silently unprotected. */ @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor(private readonly reflector: Reflector) { super(); } canActivate(context: ExecutionContext) { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; return super.canActivate(context); } } /** Enforces @RequirePermission(rid) against the expanded permission list. */ @Injectable() export class PermissionsGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRid = this.reflector.getAllAndOverride(REQUIRED_RID_KEY, [ context.getHandler(), context.getClass(), ]); if (!requiredRid) return true; const user: AuthenticatedAdmin | undefined = context.switchToHttp().getRequest().user; if (!user) throw new ForbiddenException(); if (!user.permissions.includes(requiredRid)) { throw new ForbiddenException(`Missing permission: ${requiredRid}`); } return true; } }