/** * @ai-summary Standalone tier guard middleware for route-level feature gating * @ai-context Returns a Fastify preHandler that checks user subscription tier against feature requirements. * Must be composed AFTER requireAuth in preHandler arrays. */ import { FastifyRequest, FastifyReply } from 'fastify'; import { canAccessFeature, getFeatureConfig } from '../config/feature-tiers'; import { logger } from '../logging/logger'; /** * Creates a preHandler middleware that enforces subscription tier requirements. * * Reads the user's tier from request.userContext.subscriptionTier (set by auth middleware). * Must be placed AFTER requireAuth in the preHandler chain. * * Usage: * fastify.post('/premium-route', { * preHandler: [requireAuth, requireTier('fuelLog.receiptScan')], * handler: controller.method * }); * * @param featureKey - Key from FEATURE_TIERS registry (e.g. 'fuelLog.receiptScan') * @returns Fastify preHandler function */ export function requireTier(featureKey: string) { return async (request: FastifyRequest, reply: FastifyReply): Promise => { // Validate feature key exists in registry const featureConfig = getFeatureConfig(featureKey); if (!featureConfig) { logger.error('requireTier: unknown feature key', { featureKey }); return reply.code(500).send({ error: 'INTERNAL_ERROR', message: 'Unknown feature configuration', }); } // Get user tier from userContext (populated by auth middleware) const currentTier = request.userContext?.subscriptionTier || 'free'; if (!canAccessFeature(currentTier, featureKey)) { logger.warn('requireTier: access denied', { userId: request.userContext?.userId?.substring(0, 8) + '...', currentTier, requiredTier: featureConfig.minTier, featureKey, }); return reply.code(403).send({ error: 'TIER_REQUIRED', requiredTier: featureConfig.minTier, currentTier, featureName: featureConfig.name, upgradePrompt: featureConfig.upgradePrompt, }); } logger.debug('requireTier: access granted', { userId: request.userContext?.userId?.substring(0, 8) + '...', currentTier, featureKey, }); }; }