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'; import { isSuperAdmin } from './super-admin'; /** 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). * With several rids, holding any one of them is enough. */ export const REQUIRED_RID_KEY = 'requiredRid'; export const RequirePermission = (...rids: [string, ...string[]]) => SetMetadata(REQUIRED_RID_KEY, rids); /** * Restricts a route (or a whole controller) to the super admin — see * super-admin.ts. `allowBranch` also admits an admin bound to a branch, because * the branch panel serves its staff from the same retail endpoints. */ export const SUPER_ADMIN_ONLY_KEY = 'superAdminOnly'; export interface SuperAdminOnlyOptions { allowBranch?: boolean; } export const SuperAdminOnly = (options: SuperAdminOnlyOptions = {}) => SetMetadata(SUPER_ADMIN_ONLY_KEY, options); /** 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 @SuperAdminOnly() and @RequirePermission(rid) against the expanded permission list. */ @Injectable() export class PermissionsGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const targets = [context.getHandler(), context.getClass()]; const superAdminOnly = this.reflector.getAllAndOverride( SUPER_ADMIN_ONLY_KEY, targets, ); const requiredRids = this.reflector.getAllAndOverride(REQUIRED_RID_KEY, targets); if (!superAdminOnly && !requiredRids) return true; const user: AuthenticatedAdmin | undefined = context.switchToHttp().getRequest().user; if (!user) throw new ForbiddenException(); if (superAdminOnly && !isSuperAdmin(user.mobile)) { if (!(superAdminOnly.allowBranch && user.branchId)) { throw new ForbiddenException('این بخش فقط در دسترس مدیر ارشد سامانه است'); } } if (!requiredRids) return true; if (!requiredRids.some((rid) => user.permissions.includes(rid))) { throw new ForbiddenException(`Missing permission: ${requiredRids.join(' | ')}`); } return true; } }