imeshuek's picture
Upload api/middleware/rbac.ts
0939458 verified
// RBAC Middleware for Express/Fastify API
export enum Role {
CLIENT = 'CLIENT',
VENDOR = 'VENDOR',
ADMIN = 'ADMIN',
}
export enum Permission {
// Client permissions
VIEW_OWN_BOOKINGS = 'view_own_bookings',
CREATE_BOOKING = 'create_booking',
SIGN_CONTRACT = 'sign_contract',
VIEW_OWN_CONTRACTS = 'view_own_contracts',
REQUEST_AMENDMENT = 'request_amendment',
// Vendor permissions
MANAGE_PROFILE = 'manage_profile',
MANAGE_PACKAGES = 'manage_packages',
VIEW_LEADS = 'view_leads',
CREATE_CONTRACT = 'create_contract',
SEND_CONTRACT = 'send_contract',
MANAGE_DELIVERABLES = 'manage_deliverables',
// Admin permissions
MODERATE_VENDORS = 'moderate_vendors',
MODERATE_CONTENT = 'moderate_content',
OVERSEE_CONTRACTS = 'oversee_contracts',
MEDIATE_DISPUTES = 'mediate_disputes',
EXPORT_AUDIT_TRAIL = 'export_audit_trail',
MANAGE_CATEGORIES = 'manage_categories',
IMPERSONATE_USERS = 'impersonate_users',
MANAGE_USERS = 'manage_users',
}
const rolePermissions: Record<Role, Permission[]> = {
[Role.CLIENT]: [
Permission.VIEW_OWN_BOOKINGS,
Permission.CREATE_BOOKING,
Permission.SIGN_CONTRACT,
Permission.VIEW_OWN_CONTRACTS,
Permission.REQUEST_AMENDMENT,
],
[Role.VENDOR]: [
Permission.MANAGE_PROFILE,
Permission.MANAGE_PACKAGES,
Permission.VIEW_LEADS,
Permission.CREATE_CONTRACT,
Permission.SEND_CONTRACT,
Permission.MANAGE_DELIVERABLES,
],
[Role.ADMIN]: Object.values(Permission),
}
export function hasPermission(role: Role, permission: Permission): boolean {
return rolePermissions[role]?.includes(permission) ?? false
}
export function requirePermission(permission: Permission) {
return function (req: any, res: any, next: Function) {
const role = req.user?.role as Role
if (!role || !hasPermission(role, permission)) {
return res.status(403).json({
error: 'Forbidden',
message: `Missing required permission: ${permission}`,
})
}
next()
}
}
export function requireRole(...roles: Role[]) {
return function (req: any, res: any, next: Function) {
const role = req.user?.role as Role
if (!role || !roles.includes(role)) {
return res.status(403).json({
error: 'Forbidden',
message: `Access restricted to: ${roles.join(', ')}`,
})
}
next()
}
}
// Contract-specific RBAC: only the vendor who created it or the client who received it can access
export function requireContractAccess(contractIdParam: string = 'contractId') {
return async function (req: any, res: any, next: Function) {
const contractId = req.params[contractIdParam]
const userId = req.user?.id
const role = req.user?.role as Role
if (role === Role.ADMIN && hasPermission(role, Permission.OVERSEE_CONTRACTS)) {
return next()
}
// Fetch contract and verify ownership
try {
const contract = await fetchContract(contractId)
if (!contract) return res.status(404).json({ error: 'Contract not found' })
const hasAccess =
(role === Role.VENDOR && contract.vendorId === userId) ||
(role === Role.CLIENT && contract.clientId === userId)
if (!hasAccess) return res.status(403).json({ error: 'Access denied' })
next()
} catch (err) {
return res.status(500).json({ error: 'Internal error' })
}
}
}
async function fetchContract(id: string) {
// Replace with actual DB call
return { id, vendorId: '', clientId: '' }
}
// ── Impersonation (admin-only) ──
// Admins can impersonate with TTL-bound sessions and audit reason field
export async function impersonateUser(adminId: string, targetUserId: string, reason: string, ttlMinutes: number = 30) {
// 1. Verify admin has IMPERSONATE_USERS permission
// 2. Create session with role=targetUser.role, token expires in ttlMinutes
// 3. Log to AdminAuditLog:
// { adminId, action: 'impersonate_start', target: targetUserId, detail: reason, ttl: ttlMinutes }
// 4. Return impersonation session token
}
// ── Rate Limiting ──
// Keyed on IP + method + pathname, backed by Redis
// Implementation: use rate-limiter-flexible with Redis store
// export const rateLimiter = new RateLimiterRedis({...})