import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Inject, Logger, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { PlanFeature } from '@domain/value-objects/plan-feature.vo'; import { SubscriptionRepository, SUBSCRIPTION_REPOSITORY, } from '@domain/ports/out/subscription.repository'; import { REQUIRED_FEATURES_KEY } from '../decorators/requires-feature.decorator'; /** * Feature Flag Guard * * Checks if the user's subscription plan includes the required features. * First tries to read plan from JWT payload (fast path), falls back to DB lookup. * * Usage: * @UseGuards(JwtAuthGuard, RolesGuard, FeatureFlagGuard) * @RequiresFeature('dashboard') */ @Injectable() export class FeatureFlagGuard implements CanActivate { private readonly logger = new Logger(FeatureFlagGuard.name); constructor( private readonly reflector: Reflector, @Inject(SUBSCRIPTION_REPOSITORY) private readonly subscriptionRepository: SubscriptionRepository ) {} async canActivate(context: ExecutionContext): Promise { // Get required features from @RequiresFeature() decorator const requiredFeatures = this.reflector.getAllAndOverride( REQUIRED_FEATURES_KEY, [context.getHandler(), context.getClass()] ); // If no features are required, allow access if (!requiredFeatures || requiredFeatures.length === 0) { return true; } const request = context.switchToHttp().getRequest(); const user = request.user; if (!user || !user.organizationId) { return false; } // ADMIN users have full access to all features — no plan check needed if (user.role === 'ADMIN') { return true; } // Fast path: check plan features from JWT payload if (user.planFeatures && Array.isArray(user.planFeatures)) { const hasAllFeatures = requiredFeatures.every(feature => user.planFeatures.includes(feature)); if (hasAllFeatures) { return true; } // JWT says no — but JWT might be stale after an upgrade. // Fall through to DB check. } // Slow path: DB lookup for fresh subscription data try { const subscription = await this.subscriptionRepository.findByOrganizationId( user.organizationId ); if (!subscription) { // No subscription means Bronze (free) plan — no premium features this.throwFeatureRequired(requiredFeatures); } const plan = subscription!.plan; const missingFeatures = requiredFeatures.filter(feature => !plan.hasFeature(feature)); if (missingFeatures.length > 0) { this.throwFeatureRequired(requiredFeatures); } return true; } catch (error) { if (error instanceof ForbiddenException) { throw error; } this.logger.error(`Failed to check subscription features: ${error}`); // On DB error, deny access to premium features rather than 500 this.throwFeatureRequired(requiredFeatures); } } private throwFeatureRequired(features: PlanFeature[]): never { const featureNames = features.join(', '); throw new ForbiddenException( `Cette fonctionnalité nécessite un plan supérieur. Fonctionnalités requises : ${featureNames}. Passez à un plan Silver ou supérieur.` ); } }