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. * Uses current subscription data so stale token claims cannot preserve revoked rights. * * 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; } // Always resolve current rights, including after suspension or downgrade. try { const subscription = await this.subscriptionRepository.findByOrganizationId( user.organizationId ); if (!subscription) { // No subscription means Bronze (free) plan — no premium features this.throwFeatureRequired(requiredFeatures); } const missingFeatures = requiredFeatures.filter( feature => !subscription!.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.` ); } }