Skip to main content

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:

  1. Authentication Check: Verifies user is authenticated (JWT token present)
  2. Support Agent Verification: Checks if user has support agent permissions
  3. Conversation Ownership: Validates user has access to the specific conversation
  4. 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:

  1. Account Lookup: Fetches account document with OneBalance data
  2. Credit Calculation: Calculates total cost (cost × quantity)
  3. Balance Check: Verifies account has sufficient credits
  4. Event Validation: Checks if account has access to specific event type
  5. 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 sending
  • sms - SMS sending
  • api_call - External API requests
  • lead_generation - Lead finder usage
  • report_generation - InstaReport creation
  • site_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:

  1. Authentication Check: Verifies JWT token is present and valid
  2. Account Lookup: Fetches account document with status
  3. Account Status Check: Validates account is active (not suspended/inactive)
  4. User Permissions: Checks user role and permissions
  5. 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 functional
  • suspended - ❌ Account temporarily suspended
  • inactive - ❌ Account deactivated
  • cancelled - ❌ Account subscription cancelled
  • deleted - ❌ 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 data
  • accounts:write - Create/update accounts
  • users:read - Read user data
  • users:write - Create/update users
  • deals:read - Read CRM deals
  • deals:write - Create/update deals
  • admin:read - Admin read access
  • admin: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:

  1. JWT Token Extraction: Extracts JWT token from Authorization header
  2. Token Decoding: Decodes and validates JWT signature
  3. Scope Extraction: Reads scope array from JWT payload
  4. Derived Scopes: Expands derived scopes (e.g., admin:* grants all admin scopes)
  5. Scope Matching: Checks if required scopes are present in token scopes
  6. 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:

  1. Authorization: User authorizes app with specific scopes
  2. Token Issuance: JWT issued with granted scopes in payload
  3. API Request: Client sends JWT in Authorization header
  4. Scope Verification: Middleware validates token scopes
  5. 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

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

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM