Shared Middlewares Documentation
Overview
Total Middlewares: 4 Express middleware functions
Location: shared/utilities/middlewares/
Distribution: Copied to 7 services via npm run copySharedFiles
Purpose: Shared Express middleware for authentication, authorization, and access control
Used By: Internal API, External API, Conversation Socket
⚠️ CRITICAL: Git Behavior Warning
DO NOT EDIT middleware files in service directories:
- ❌
internal/api/v1/utilities/middlewares/*.js - ❌
external/Integrations/utilities/middlewares/*.js - ❌
queue-manager/utilities/middlewares/*.js - ❌
conversation-socket/utilities/middlewares/*.js - ❌
general-socket/utilities/middlewares/*.js - ❌
dashboard-gateway/utilities/middlewares/*.js - ❌
tests/utilities/middlewares/*.js
These folders are Git-ignored and regenerated on every build. Changes made here will be lost.
✅ ALWAYS EDIT: shared/utilities/middlewares/ files only, then run npm run copySharedFiles
Middleware Functions
conversation.support.js
Purpose: Access control for support conversation endpoints
Description: Validates that the authenticated user has access to view and manage support conversations. Ensures support agents can only access conversations assigned to them or their team.
Usage Pattern:
const conversationSupportMiddleware = require('./utilities/middlewares/conversation.support');
// Apply to support routes
router.get('/support/conversations/:id', conversationSupportMiddleware, controller);
router.post('/support/conversations/:id/messages', conversationSupportMiddleware, controller);
router.patch('/support/conversations/:id', conversationSupportMiddleware, controller);
Validation Logic:
- Authentication Check: Verifies user is authenticated (JWT token present)
- Support Agent Verification: Checks if user has support agent permissions
- Conversation Ownership: Validates user has access to the specific conversation
- Team Access: Allows access if conversation is assigned to user's support team
Request Flow:
// Middleware implementation pattern
async function conversationSupportMiddleware(req, res, next) {
const { id: conversationId } = req.params;
const { user_id, account_id } = req.auth;
// 1. Fetch conversation
const conversation = await SupportConversation.findOne({
_id: conversationId,
account_id,
});
if (!conversation) {
return res.status(404).json({ error: 'Conversation not found' });
}
// 2. Check if user is assigned
const isAssigned =
conversation.assigned_to?.toString() === user_id ||
conversation.team_members?.includes(user_id);
if (!isAssigned) {
return res.status(403).json({ error: 'Access denied' });
}
// 3. Attach conversation to request
req.conversation = conversation;
next();
}
Error Responses:
- 404: Conversation not found or not in user's account
- 403: User not assigned to conversation or team
Used By: Internal API (support routes), Conversation Socket (support conversations)
Benefits:
- Prevents unauthorized access to support conversations
- Enforces team-based access control
- Simplifies controller logic (conversation pre-fetched)
onebalance.js
Purpose: OneBalance credit system validation and enforcement
Description: Middleware that validates OneBalance credits before allowing operations. Ensures users have sufficient credits for the requested action and enforces credit-based rate limits.
Usage Pattern:
const oneBalanceMiddleware = require('./utilities/middlewares/onebalance');
// Apply with cost parameter
router.post('/api/send-email', oneBalanceMiddleware({ cost: 1 }), controller);
router.post('/api/generate-report', oneBalanceMiddleware({ cost: 10 }), controller);
router.post('/api/bulk-action', oneBalanceMiddleware({ cost: 100 }), controller);
Configuration Options:
oneBalanceMiddleware({
cost: 1, // Number of credits required
event: 'email', // Event type (email, sms, api_call, etc.)
quantityField: 'recipients', // Optional: field in req.body with quantity multiplier
});
// Dynamic cost based on request
router.post(
'/api/send-bulk-email',
oneBalanceMiddleware({
cost: 1,
quantityField: 'recipients', // Cost = 1 * req.body.recipients.length
}),
controller,
);
Validation Logic:
- Account Lookup: Fetches account document with OneBalance data
- Credit Calculation: Calculates total cost (cost × quantity)
- Balance Check: Verifies account has sufficient credits
- Event Validation: Checks if account has access to specific event type
- Rate Limiting: Enforces event-specific rate limits
Request Flow:
// Middleware implementation pattern
async function oneBalanceMiddleware(options) {
return async (req, res, next) => {
const { account_id } = req.auth;
const { cost, event, quantityField } = options;
// 1. Fetch account with OneBalance
const account = await Account.findById(account_id).lean();
const oneBalance = await OneBalance.findOne({ account_id }).lean();
if (!oneBalance) {
return res.status(403).json({ error: 'OneBalance not configured' });
}
// 2. Calculate total cost
let quantity = 1;
if (quantityField && req.body[quantityField]) {
quantity = Array.isArray(req.body[quantityField])
? req.body[quantityField].length
: req.body[quantityField];
}
const totalCost = cost * quantity;
// 3. Verify balance
if (oneBalance.balance < totalCost) {
return res.status(402).json({
error: 'Insufficient credits',
balance: oneBalance.balance,
required: totalCost,
});
}
// 4. Check event access
if (event && oneBalance.disabled_events?.includes(event)) {
return res.status(403).json({ error: 'Event not allowed' });
}
// 5. Attach data to request
req.oneBalance = {
balance: oneBalance.balance,
cost: totalCost,
event,
};
next();
};
}
Error Responses:
- 402: Insufficient credits (Payment Required)
- 403: Event not allowed or OneBalance not configured
Post-Processing:
After the controller successfully processes the request, credits are deducted via queue:
// In controller after successful operation
await OnebalanceQueue.create({
account_id: req.auth.account_id,
event: req.oneBalance.event,
quantity: req.oneBalance.cost,
additional_info: {
reference: resultId,
},
});
Used By: Internal API (credit-based features like email, SMS, lead generation, report generation)
Supported Events:
email- Email sendingsms- SMS sendingapi_call- External API requestslead_generation- Lead finder usagereport_generation- InstaReport creationsite_generation- InstaSite creation
Benefits:
- Prevents over-usage of paid features
- Enforces account-level quotas
- Provides consistent credit checking across services
- Enables pay-as-you-go pricing model
verify-access-and-status.middleware.js
Purpose: Combined access control and account status verification
Description: Comprehensive middleware that verifies user authentication, checks account status, and validates user permissions for resource access. Blocks access if account is inactive, suspended, or user lacks permissions.
Usage Pattern:
const verifyAccessAndStatus = require('./utilities/middlewares/verify-access-and-status.middleware');
// Apply to protected routes
router.get('/accounts/:id', verifyAccessAndStatus, controller);
router.post('/deals', verifyAccessAndStatus, controller);
router.patch('/users/:id', verifyAccessAndStatus, controller);
Configuration Options:
// Default usage (checks authentication + account status)
router.get('/resource', verifyAccessAndStatus, controller);
// With custom resource check
router.get(
'/resource/:id',
verifyAccessAndStatus({
resourceIdParam: 'id', // Param name in req.params
resourceModel: 'Deal', // Model to verify ownership
ownershipField: 'account_id', // Field that links to account
}),
controller,
);
Validation Logic:
- Authentication Check: Verifies JWT token is present and valid
- Account Lookup: Fetches account document with status
- Account Status Check: Validates account is active (not suspended/inactive)
- User Permissions: Checks user role and permissions
- Resource Ownership: Validates user's account owns the requested resource
Request Flow:
// Middleware implementation pattern
async function verifyAccessAndStatus(options = {}) {
return async (req, res, next) => {
const { account_id, user_id } = req.auth;
// 1. Verify authentication
if (!account_id || !user_id) {
return res.status(401).json({ error: 'Unauthorized', errno: 1000 });
}
// 2. Fetch account
const account = await Account.findById(account_id).lean();
if (!account) {
return res.status(404).json({ error: 'Account not found', errno: 1004 });
}
// 3. Check account status
const inactiveStatuses = ['suspended', 'inactive', 'cancelled', 'deleted'];
if (inactiveStatuses.includes(account.status)) {
return res.status(423).json({
error: 'Account inactive',
errno: 1005,
status: account.status,
});
}
// 4. Fetch user
const user = await User.findById(user_id).lean();
if (!user) {
return res.status(404).json({ error: 'User not found', errno: 1006 });
}
// 5. Check user status
if (user.status !== 'active') {
return res.status(423).json({ error: 'User inactive', errno: 1007 });
}
// 6. Verify resource ownership (if configured)
if (options.resourceIdParam && options.resourceModel) {
const resourceId = req.params[options.resourceIdParam];
const Model = require(`../models/${options.resourceModel}`);
const resource = await Model.findById(resourceId).lean();
if (!resource) {
return res.status(404).json({ error: 'Resource not found', errno: 1008 });
}
const ownershipField = options.ownershipField || 'account_id';
if (resource[ownershipField].toString() !== account_id) {
return res.status(403).json({ error: 'Access denied', errno: 1009 });
}
req.resource = resource;
}
// 7. Attach to request
req.account = account;
req.user = user;
next();
};
}
Error Responses:
- 401: Unauthorized (not authenticated) -
errno: 1000 - 403: Forbidden (no access to resource) -
errno: 1009 - 404: Account/User/Resource not found -
errno: 1004/1006/1008 - 423: Locked (account suspended/inactive) -
errno: 1005/1007
Account Status Values:
active- ✅ Account is active and functionalsuspended- ❌ Account temporarily suspendedinactive- ❌ Account deactivatedcancelled- ❌ Account subscription cancelleddeleted- ❌ Account marked for deletion
Used By: Internal API (most protected routes)
Benefits:
- Single middleware for multiple checks
- Reduces code duplication across routes
- Provides consistent error handling
- Pre-fetches account and user data for controllers
verifyScope.js
Purpose: OAuth 2.0 scope validation middleware
Description: Validates that the authenticated JWT token contains the required OAuth scopes for the requested operation. Enforces fine-grained permissions through scope-based access control.
Usage Pattern:
const { verifyScope } = require('./utilities/middlewares/verifyScope');
// Single scope
router.post('/v1/accounts', verifyScope('accounts:write'), controller);
// Multiple scopes (all required)
router.get('/v1/admin/users', verifyScope(['admin:read', 'users:read']), controller);
// Allow admin override
router.delete('/v1/accounts/:id', verifyScope('accounts:delete', true), controller);
Scope Naming Convention:
resource:action
Examples:
accounts:read- Read account dataaccounts:write- Create/update accountsusers:read- Read user datausers:write- Create/update usersdeals:read- Read CRM dealsdeals:write- Create/update dealsadmin:read- Admin read accessadmin:write- Admin write access
Configuration:
// Basic scope check
verifyScope('accounts:write');
// Multiple scopes (all required)
verifyScope(['accounts:write', 'users:read']);
// Allow admin override (admin scope bypasses check)
verifyScope('accounts:delete', true);
Validation Logic:
- JWT Token Extraction: Extracts JWT token from Authorization header
- Token Decoding: Decodes and validates JWT signature
- Scope Extraction: Reads
scopearray from JWT payload - Derived Scopes: Expands derived scopes (e.g.,
admin:*grants all admin scopes) - Scope Matching: Checks if required scopes are present in token scopes
- Admin Override: If enabled, checks for admin scope to bypass restrictions
Request Flow:
// Middleware implementation pattern
function verifyScope(allowedScopes, allowAdmin = false) {
return (req, res, next) => {
// 1. Extract JWT token
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided', errno: 1001 });
}
// 2. Decode JWT
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
return res.status(401).json({ error: 'Invalid token', errno: 1002 });
}
// 3. Extract scopes
const tokenScopes = decoded.scope || [];
// 4. Expand derived scopes
const expandedScopes = expandDerivedScopes(tokenScopes);
// 5. Normalize required scopes
const requiredScopes = Array.isArray(allowedScopes) ? allowedScopes : [allowedScopes];
// 6. Check admin override
if (allowAdmin && expandedScopes.includes('admin:*')) {
req.auth = decoded;
return next();
}
// 7. Verify scopes
const hasAllScopes = requiredScopes.every(scope => expandedScopes.includes(scope));
if (!hasAllScopes) {
return res.status(403).json({
error: 'Insufficient scope',
errno: 1001,
required: requiredScopes,
provided: expandedScopes,
});
}
// 8. Attach to request
req.auth = decoded;
req.scopes = expandedScopes;
next();
};
}
// Derived scope expansion
function expandDerivedScopes(scopes) {
const expanded = [...scopes];
for (const scope of scopes) {
if (scope.endsWith(':*')) {
// admin:* grants admin:read, admin:write
const prefix = scope.replace(':*', '');
expanded.push(`${prefix}:read`, `${prefix}:write`, `${prefix}:delete`);
}
}
return expanded;
}
Derived Scopes:
Wildcard scopes automatically grant related permissions:
// Token with admin:* scope
{
scope: ['admin:*'];
}
// Automatically grants:
// - admin:read
// - admin:write
// - admin:delete
// - Any admin:* operation
Error Responses:
- 401: No token provided or invalid token -
errno: 1001/1002 - 403: Insufficient scope -
errno: 1001
OAuth Flow Integration:
- Authorization: User authorizes app with specific scopes
- Token Issuance: JWT issued with granted scopes in payload
- API Request: Client sends JWT in Authorization header
- Scope Verification: Middleware validates token scopes
- Access Control: Request allowed if scopes match
Example JWT Payload:
{
"user_id": "507f1f77bcf86cd799439011",
"account_id": "507f1f77bcf86cd799439012",
"scope": ["accounts:read", "accounts:write", "users:read"],
"iat": 1234567890,
"exp": 1234571490
}
Used By: Internal API (OAuth routes), External API (all OAuth-protected endpoints)
Benefits:
- Fine-grained permission control
- OAuth 2.0 compliant
- Supports wildcard/derived scopes
- Enables third-party app integrations
- Provides audit trail of granted permissions
Usage Patterns
Combining Middlewares
// Stack multiple middlewares
router.post(
'/v1/deals',
verifyScope('deals:write'), // OAuth scope check
verifyAccessAndStatus, // Account status check
validate(dealSchema), // Request validation
catchAsync(controller), // Error handling
);
// Credit-based operation with OAuth
router.post(
'/v1/send-email',
verifyScope('emails:write'),
oneBalanceMiddleware({ cost: 1, quantityField: 'recipients' }),
verifyAccessAndStatus,
catchAsync(controller),
);
// Support conversation access
router.get(
'/v1/support/conversations/:id',
verifyScope('support:read'),
conversationSupportMiddleware,
catchAsync(controller),
);
Conditional Middleware Application
// Apply middleware conditionally
const authMiddlewares = [verifyAccessAndStatus];
if (requiresCredits) {
authMiddlewares.push(oneBalanceMiddleware({ cost: 10 }));
}
router.post('/v1/action', ...authMiddlewares, controller);
Custom Middleware Wrappers
// Create custom wrapper for common patterns
function protectedRoute(scopes, options = {}) {
const middlewares = [verifyScope(scopes), verifyAccessAndStatus];
if (options.requiresCredits) {
middlewares.push(oneBalanceMiddleware(options.creditOptions));
}
return middlewares;
}
// Usage
router.post('/v1/accounts', ...protectedRoute('accounts:write'), controller);
router.post(
'/v1/send-email',
...protectedRoute('emails:write', {
requiresCredits: true,
creditOptions: { cost: 1, quantityField: 'recipients' },
}),
controller,
);
Testing Middlewares
Unit Tests
describe('verifyScope Middleware', () => {
it('should allow access with valid scope', async () => {
const req = {
headers: {
authorization: 'Bearer valid_token',
},
};
const res = {};
const next = jest.fn();
await verifyScope('accounts:read')(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should reject access without scope', async () => {
const req = {
headers: {
authorization: 'Bearer token_without_scope',
},
};
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
const next = jest.fn();
await verifyScope('accounts:write')(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(next).not.toHaveBeenCalled();
});
});
Integration Tests
describe('Protected Route', () => {
it('should require authentication and valid scope', async () => {
const response = await request(app)
.get('/v1/accounts')
.set('Authorization', 'Bearer invalid_token');
expect(response.status).toBe(401);
});
it('should allow access with valid token and scope', async () => {
const token = generateValidToken({ scope: ['accounts:read'] });
const response = await request(app).get('/v1/accounts').set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
});
});
Security Considerations
Token Validation
- Always validate JWT signature
- Check token expiration
- Verify token issuer
- Use secure secret keys
Scope Best Practices
- Use least privilege principle
- Grant minimum required scopes
- Audit scope usage regularly
- Document all available scopes
Error Handling
- Don't expose sensitive error details
- Use standard HTTP status codes
- Log security events
- Rate limit authentication attempts
Related Documentation
- Shared Utilities - Helper functions used by middlewares
- Shared Models - Models accessed by middlewares
- Shared Overview - Main documentation with distribution process
- Internal API - Service using all middlewares
- External API - Service using OAuth middlewares
Resource Type: Express Middlewares
Total: 4 middleware functions
Distribution: 7 services receive middleware copies
Git Strategy: Only shared/utilities/middlewares/ is version controlled
Last Updated: October 2025