import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { DataSource } from 'typeorm'; import { CAPABILITIES_KEY } from '../decorators/capability.decorator'; import { resolveCapabilities } from '../permissions'; @Injectable() export class CapabilityGuard implements CanActivate { // Cache org settings (including permissionOverrides) per orgId private settingsCache = new Map; cachedAt: number }>(); private static readonly CACHE_TTL = 60_000; // 60 seconds constructor( private reflector: Reflector, private dataSource: DataSource, ) {} async canActivate(context: ExecutionContext): Promise { const requiredCapabilities = this.reflector.getAllAndOverride(CAPABILITIES_KEY, [ context.getHandler(), context.getClass(), ]); // No capabilities required — pass through (backward compatible) if (!requiredCapabilities || requiredCapabilities.length === 0) { return true; } const request = context.switchToHttp().getRequest(); const user = request.user; // No authenticated user — let other guards handle auth if (!user) return true; // Superadmins bypass all capability checks if (user.isSuperadmin) return true; const role = user.role; const orgId = user.orgId; if (!role || !orgId) return true; // Get org settings (with caching) const settings = await this.getOrgSettings(orgId); const userCapabilities = resolveCapabilities(role, settings?.permissionOverrides); // User must have ALL required capabilities const hasAll = requiredCapabilities.every((cap) => userCapabilities.has(cap)); if (!hasAll) { throw new ForbiddenException( 'You do not have the required permissions for this action.', ); } return true; } private async getOrgSettings(orgId: string): Promise | null> { const cached = this.settingsCache.get(orgId); if (cached && Date.now() - cached.cachedAt < CapabilityGuard.CACHE_TTL) { return cached.settings; } try { const result = await this.dataSource.query( `SELECT settings FROM shared.organizations WHERE id = $1`, [orgId], ); if (result.length > 0) { const settings = result[0].settings || {}; this.settingsCache.set(orgId, { settings, cachedAt: Date.now() }); return settings; } } catch { // Non-critical — fall through to use defaults only } return null; } /** Clear cached settings for an org (call after settings update) */ clearCache(orgId: string) { this.settingsCache.delete(orgId); } }