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:
111
frontend/src/core/hooks/useTierAccess.ts
Normal file
111
frontend/src/core/hooks/useTierAccess.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @ai-summary React hook for tier-based feature access checking
|
||||
* @ai-context Used to gate premium features based on user subscription tier
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { apiClient } from '../api/client';
|
||||
import type { SubscriptionTier } from '../../features/settings/types/profile.types';
|
||||
|
||||
// Feature tier configuration (mirrors backend)
|
||||
export interface FeatureConfig {
|
||||
minTier: SubscriptionTier;
|
||||
name: string;
|
||||
upgradePrompt: string;
|
||||
}
|
||||
|
||||
interface FeatureTiersResponse {
|
||||
tiers: Record<SubscriptionTier, number>;
|
||||
features: Record<string, FeatureConfig>;
|
||||
}
|
||||
|
||||
interface AccessCheckResult {
|
||||
allowed: boolean;
|
||||
requiredTier: SubscriptionTier | null;
|
||||
config: FeatureConfig | null;
|
||||
}
|
||||
|
||||
// Tier hierarchy for comparison
|
||||
const TIER_LEVELS: Record<SubscriptionTier, number> = {
|
||||
free: 0,
|
||||
pro: 1,
|
||||
enterprise: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if user can access tier-gated features
|
||||
* Fetches user profile for tier and feature config from backend
|
||||
*/
|
||||
export const useTierAccess = () => {
|
||||
const { isAuthenticated, isLoading: authLoading } = useAuth0();
|
||||
|
||||
// Fetch user profile for current tier
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['user-profile'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/user/profile');
|
||||
return response.data;
|
||||
},
|
||||
enabled: isAuthenticated && !authLoading,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Fetch feature tier config from backend (single source of truth)
|
||||
const featureConfigQuery = useQuery({
|
||||
queryKey: ['feature-tiers'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get<FeatureTiersResponse>('/config/feature-tiers');
|
||||
return response.data;
|
||||
},
|
||||
enabled: isAuthenticated && !authLoading,
|
||||
staleTime: 30 * 60 * 1000, // 30 minutes - config rarely changes
|
||||
gcTime: 60 * 60 * 1000, // 1 hour cache
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const tier: SubscriptionTier = profileQuery.data?.subscriptionTier || 'free';
|
||||
const features = featureConfigQuery.data?.features || {};
|
||||
|
||||
/**
|
||||
* Check if user can access a feature by key
|
||||
*/
|
||||
const hasAccess = (featureKey: string): boolean => {
|
||||
const config = features[featureKey];
|
||||
if (!config) {
|
||||
// Unknown features are allowed (fail open for safety)
|
||||
return true;
|
||||
}
|
||||
return TIER_LEVELS[tier] >= TIER_LEVELS[config.minTier];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get detailed access information for a feature
|
||||
*/
|
||||
const checkAccess = (featureKey: string): AccessCheckResult => {
|
||||
const config = features[featureKey] || null;
|
||||
if (!config) {
|
||||
return {
|
||||
allowed: true,
|
||||
requiredTier: null,
|
||||
config: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
allowed: TIER_LEVELS[tier] >= TIER_LEVELS[config.minTier],
|
||||
requiredTier: config.minTier,
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
tier,
|
||||
loading: profileQuery.isLoading || featureConfigQuery.isLoading,
|
||||
hasAccess,
|
||||
checkAccess,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTierAccess;
|
||||
Reference in New Issue
Block a user