feat: Implement user tier-based feature gating system (refs #8)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m35s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 27s
Deploy to Staging / Verify Staging (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m35s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 27s
Deploy to Staging / Verify Staging (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Add subscription tier system to gate features behind Free/Pro/Enterprise tiers. Backend: - Create feature-tiers.ts with FEATURE_TIERS config and utilities - Add /api/config/feature-tiers endpoint for frontend config fetch - Create requireTier middleware for route-level tier enforcement - Add subscriptionTier to request.userContext in auth plugin - Gate scanForMaintenance in documents controller (Pro+ required) - Add migration to reset scanForMaintenance for free users Frontend: - Create useTierAccess hook for tier checking - Create UpgradeRequiredDialog component (responsive) - Gate DocumentForm checkbox with lock icon for free users - Add SubscriptionTier type to profile.types.ts Documentation: - Add TIER-GATING.md with usage guide Tests: 30 passing (feature-tiers, tier-guard, controller) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import { Transform, TransformCallback } from 'stream';
|
||||
import crypto from 'crypto';
|
||||
import FileType from 'file-type';
|
||||
import { Readable } from 'stream';
|
||||
import { canAccessFeature, getFeatureConfig } from '../../../core/config/feature-tiers';
|
||||
import { SubscriptionTier } from '../../user-profile/domain/user-profile.types';
|
||||
|
||||
export class DocumentsController {
|
||||
private readonly service = new DocumentsService();
|
||||
@@ -73,6 +75,7 @@ export class DocumentsController {
|
||||
|
||||
async create(request: FastifyRequest<{ Body: CreateBody }>, reply: FastifyReply) {
|
||||
const userId = (request as any).user?.sub as string;
|
||||
const userTier: SubscriptionTier = request.userContext?.subscriptionTier || 'free';
|
||||
|
||||
logger.info('Document create requested', {
|
||||
operation: 'documents.create',
|
||||
@@ -82,6 +85,26 @@ export class DocumentsController {
|
||||
title: request.body.title,
|
||||
});
|
||||
|
||||
// Tier validation: scanForMaintenance requires Pro tier
|
||||
const featureKey = 'document.scanMaintenanceSchedule';
|
||||
if (request.body.scanForMaintenance && !canAccessFeature(userTier, featureKey)) {
|
||||
const config = getFeatureConfig(featureKey);
|
||||
logger.warn('Tier required for scanForMaintenance', {
|
||||
operation: 'documents.create.tier_required',
|
||||
userId,
|
||||
userTier,
|
||||
requiredTier: config?.minTier,
|
||||
});
|
||||
return reply.code(403).send({
|
||||
error: 'TIER_REQUIRED',
|
||||
requiredTier: config?.minTier || 'pro',
|
||||
currentTier: userTier,
|
||||
feature: featureKey,
|
||||
featureName: config?.name || null,
|
||||
upgradePrompt: config?.upgradePrompt || 'Upgrade to Pro to access this feature.',
|
||||
});
|
||||
}
|
||||
|
||||
const created = await this.service.createDocument(userId, request.body);
|
||||
|
||||
logger.info('Document created', {
|
||||
@@ -98,6 +121,7 @@ export class DocumentsController {
|
||||
|
||||
async update(request: FastifyRequest<{ Params: IdParams; Body: UpdateBody }>, reply: FastifyReply) {
|
||||
const userId = (request as any).user?.sub as string;
|
||||
const userTier: SubscriptionTier = request.userContext?.subscriptionTier || 'free';
|
||||
const documentId = request.params.id;
|
||||
|
||||
logger.info('Document update requested', {
|
||||
@@ -107,6 +131,27 @@ export class DocumentsController {
|
||||
updateFields: Object.keys(request.body),
|
||||
});
|
||||
|
||||
// Tier validation: scanForMaintenance requires Pro tier
|
||||
const featureKey = 'document.scanMaintenanceSchedule';
|
||||
if (request.body.scanForMaintenance && !canAccessFeature(userTier, featureKey)) {
|
||||
const config = getFeatureConfig(featureKey);
|
||||
logger.warn('Tier required for scanForMaintenance', {
|
||||
operation: 'documents.update.tier_required',
|
||||
userId,
|
||||
documentId,
|
||||
userTier,
|
||||
requiredTier: config?.minTier,
|
||||
});
|
||||
return reply.code(403).send({
|
||||
error: 'TIER_REQUIRED',
|
||||
requiredTier: config?.minTier || 'pro',
|
||||
currentTier: userTier,
|
||||
feature: featureKey,
|
||||
featureName: config?.name || null,
|
||||
upgradePrompt: config?.upgradePrompt || 'Upgrade to Pro to access this feature.',
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await this.service.updateDocument(userId, documentId, request.body);
|
||||
if (!updated) {
|
||||
logger.warn('Document not found for update', {
|
||||
|
||||
Reference in New Issue
Block a user