REDROOM
PHP 8.2.30
Path:
Logout
Edit File
Size: 12.03 KB
Close
/home/byroehnu/easepay.easetack.com/middleware/subscription.js
Text
Base64
// middleware/subscription.js - Enhanced Subscription Guard Middleware const { prisma } = require('../config/prisma'); /** * Enhanced Subscription Guard Middleware * Prevents users from exceeding their plan limits and handles read-only mode * Integrates with Flutterwave subscription system */ /** * Check plan limits for specific resources * @param {string} resource - The resource to check (invoice, team, report) * @returns {Function} Middleware function */ const checkPlanLimits = (resource) => { return async (req, res, next) => { try { const userId = req.user.id; // Get user with subscription, usage, and limits const user = await prisma.user.findUnique({ where: { id: userId }, include: { subscription: true, usage: true, limits: true } }); if (!user) { return res.status(404).json({ success: false, message: "User not found" }); } // Initialize usage and limits if they don't exist if (!user.usage) { await prisma.userUsage.create({ data: { userId: userId } }); } if (!user.limits) { await prisma.userLimits.create({ data: { userId: userId } }); } // Refresh user data after initialization const updatedUser = await prisma.user.findUnique({ where: { id: userId }, include: { subscription: true, usage: true, limits: true } }); // Check subscription status and access const subscriptionStatus = updatedUser.subscription?.status || 'INACTIVE'; const hasActiveSubscription = subscriptionStatus === 'ACTIVE'; const isInGracePeriod = subscriptionStatus === 'PAST_DUE' && updatedUser.subscription?.gracePeriodEnd && new Date() <= new Date(updatedUser.subscription.gracePeriodEnd); // Handle resource-specific limits switch (resource) { case 'invoice': if (!hasActiveSubscription && !isInGracePeriod) { return res.status(403).json({ success: false, message: "Your subscription is inactive. Please renew to create invoices.", action: "RENEW_SUBSCRIPTION", subscriptionStatus }); } if (hasActiveSubscription && updatedUser.usage.invoiceCount >= updatedUser.limits.maxInvoices && updatedUser.limits.maxInvoices !== -1) { // -1 means unlimited return res.status(402).json({ success: false, message: `Invoice limit reached (${updatedUser.limits.maxInvoices}). Please upgrade your plan.`, action: "UPGRADE_PLAN", currentUsage: updatedUser.usage.invoiceCount, limit: updatedUser.limits.maxInvoices }); } if (isInGracePeriod) { return res.status(402).json({ success: false, message: "Your payment failed. Account is in read-only mode. Please update billing to create invoices.", action: "UPDATE_BILLING", subscriptionStatus, gracePeriodEnd: updatedUser.subscription.gracePeriodEnd }); } break; case 'team': if (!hasActiveSubscription && !isInGracePeriod) { return res.status(403).json({ success: false, message: "Your subscription is inactive. Please renew to add team members.", action: "RENEW_SUBSCRIPTION", subscriptionStatus }); } if (hasActiveSubscription && updatedUser.usage.teamMemberCount >= updatedUser.limits.maxTeamMembers && updatedUser.limits.maxTeamMembers !== -1) { return res.status(402).json({ success: false, message: `Team member limit reached (${updatedUser.limits.maxTeamMembers}). Please upgrade your plan.`, action: "UPGRADE_PLAN", currentUsage: updatedUser.usage.teamMemberCount, limit: updatedUser.limits.maxTeamMembers }); } if (isInGracePeriod) { return res.status(402).json({ success: false, message: "Your payment failed. Account is in read-only mode. Please update billing to add team members.", action: "UPDATE_BILLING", subscriptionStatus, gracePeriodEnd: updatedUser.subscription.gracePeriodEnd }); } break; case 'report': if (!hasActiveSubscription && !isInGracePeriod) { return res.status(403).json({ success: false, message: "Your subscription is inactive. Please renew to export reports.", action: "RENEW_SUBSCRIPTION", subscriptionStatus }); } if (hasActiveSubscription && updatedUser.usage.reportExports >= updatedUser.limits.maxReportExports && updatedUser.limits.maxReportExports !== -1) { return res.status(402).json({ success: false, message: `Report export limit reached (${updatedUser.limits.maxReportExports}). Please upgrade your plan.`, action: "UPGRADE_PLAN", currentUsage: updatedUser.usage.reportExports, limit: updatedUser.limits.maxReportExports }); } if (isInGracePeriod) { return res.status(402).json({ success: false, message: "Your payment failed. Account is in read-only mode. Please update billing to export reports.", action: "UPDATE_BILLING", subscriptionStatus, gracePeriodEnd: updatedUser.subscription.gracePeriodEnd }); } break; default: return res.status(400).json({ success: false, message: "Invalid resource type" }); } // Add user data to request for use in controllers req.userSubscription = { user: updatedUser, subscription: updatedUser.subscription, usage: updatedUser.usage, limits: updatedUser.limits, hasActiveSubscription, isInGracePeriod }; next(); } catch (error) { console.error('❌ Subscription middleware error:', error); return res.status(500).json({ success: false, message: "Internal server error", error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; }; /** * Authorize access based on subscription status * Allows read operations during grace period, blocks write operations */ const authorizeAccess = async (req, res, next) => { try { const userId = req.user.id; const user = await prisma.user.findUnique({ where: { id: userId }, include: { subscription: true } }); if (!user) { return res.status(404).json({ success: false, message: "User not found" }); } const subscription = user.subscription; const today = new Date(); // 1. FULL ACCESS: Subscription is active and paid if (subscription?.status === 'ACTIVE') { req.accessLevel = 'FULL'; return next(); } // 2. READ-ONLY ACCESS: Payment failed, but within 7-day grace period if (subscription?.status === 'PAST_DUE') { const isWithinGrace = subscription.gracePeriodEnd && today <= new Date(subscription.gracePeriodEnd); // Allow GET requests (viewing data), block everything else if (req.method === 'GET' && isWithinGrace) { req.accessLevel = 'READ_ONLY'; req.gracePeriodEnd = subscription.gracePeriodEnd; return next(); } return res.status(402).json({ success: false, message: "Your payment failed. Access is currently Read-Only. Please update billing to resume creating content.", action: "UPDATE_BILLING", subscriptionStatus: subscription.status, gracePeriodEnd: subscription.gracePeriodEnd }); } // 3. SUSPENDED: Grace period over or manual cancellation if (subscription?.status === 'SUSPENDED' || subscription?.status === 'CANCELLED') { return res.status(403).json({ success: false, message: "Your account is suspended. Please renew your subscription to access your data.", action: "RENEW_SUBSCRIPTION", subscriptionStatus: subscription.status }); } // 4. FREE TIER: Allow limited access if (!subscription || subscription.status === 'INACTIVE') { req.accessLevel = 'FREE'; return next(); } // Default: Allow access but flag for review req.accessLevel = 'LIMITED'; next(); } catch (error) { console.error('❌ Authorization middleware error:', error); return res.status(500).json({ success: false, message: "Internal server error", error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; /** * Increment usage counter for a specific resource * @param {string} resource - The resource to increment */ const incrementUsage = (resource) => { return async (req, res, next) => { try { const userId = req.user.id; const updateData = {}; switch (resource) { case 'invoice': updateData.invoiceCount = { increment: 1 }; break; case 'team': updateData.teamMemberCount = { increment: 1 }; break; case 'report': updateData.reportExports = { increment: 1 }; break; default: return next(); // Skip if unknown resource } // Upsert usage record await prisma.userUsage.upsert({ where: { userId }, update: updateData, create: { userId, ...Object.keys(updateData).reduce((acc, key) => { acc[key] = 1; return acc; }, {}) } }); console.log(`📊 Usage incremented for user ${userId}: ${resource}`); next(); } catch (error) { console.error('❌ Usage increment error:', error); // Don't block request for usage tracking errors next(); } }; }; /** * Check if user has access to specific features * @param {string} feature - Feature to check */ const checkFeatureAccess = (feature) => { return async (req, res, next) => { try { const userId = req.user.id; const user = await prisma.user.findUnique({ where: { id: userId }, include: { limits: true, subscription: true } }); if (!user?.limits) { return res.status(403).json({ success: false, message: "Access denied. Please contact support." }); } let hasAccess = false; switch (feature) { case 'advanced_reports': hasAccess = user.limits.hasAdvancedReports; break; case 'api_access': hasAccess = user.limits.hasAPIAccess; break; default: hasAccess = false; } if (!hasAccess) { return res.status(402).json({ success: false, message: `This feature requires a premium plan. Please upgrade to access ${feature.replace('_', ' ')}.`, action: "UPGRADE_PLAN", feature }); } next(); } catch (error) { console.error('❌ Feature access check error:', error); return res.status(500).json({ success: false, message: "Internal server error" }); } }; }; module.exports = { checkPlanLimits, authorizeAccess, incrementUsage, checkFeatureAccess };
Save
Close
Exit & Reset
Text mode: syntax highlighting auto-detects file type.
Directory Contents
Dirs: 0 × Files: 4
Delete Selected
Select All
Select None
Sort:
Name
Size
Modified
Enable drag-to-move
Name
Size
Perms
Modified
Actions
.htaccess
127 B
lr--r--r--
2026-03-14 01:49:04
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
auth.js
6.89 KB
lrw-r--r--
2026-02-19 22:06:46
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
roles.js
2.30 KB
lrw-r--r--
2026-02-05 17:19:17
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
subscription.js
12.03 KB
lrw-r--r--
2026-02-21 08:13:30
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
Zip Selected
If ZipArchive is unavailable, a
.tar
will be created (no compression).