feat: add standalone requireTier middleware (refs #138)
Create reusable preHandler middleware for subscription tier gating. Composable with requireAuth in route preHandler arrays. Returns 403 TIER_REQUIRED with upgrade prompt for insufficient tier, 500 for unknown feature keys. Includes 9 unit tests covering all acceptance criteria. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
64
backend/src/core/middleware/require-tier.ts
Normal file
64
backend/src/core/middleware/require-tier.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @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<void> => {
|
||||
// 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user