Spaces:
Build error
Build error
Upload api/middleware/rbac.ts
Browse files- api/middleware/rbac.ts +132 -0
api/middleware/rbac.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// RBAC Middleware for Express/Fastify API
|
| 2 |
+
|
| 3 |
+
export enum Role {
|
| 4 |
+
CLIENT = 'CLIENT',
|
| 5 |
+
VENDOR = 'VENDOR',
|
| 6 |
+
ADMIN = 'ADMIN',
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export enum Permission {
|
| 10 |
+
// Client permissions
|
| 11 |
+
VIEW_OWN_BOOKINGS = 'view_own_bookings',
|
| 12 |
+
CREATE_BOOKING = 'create_booking',
|
| 13 |
+
SIGN_CONTRACT = 'sign_contract',
|
| 14 |
+
VIEW_OWN_CONTRACTS = 'view_own_contracts',
|
| 15 |
+
REQUEST_AMENDMENT = 'request_amendment',
|
| 16 |
+
|
| 17 |
+
// Vendor permissions
|
| 18 |
+
MANAGE_PROFILE = 'manage_profile',
|
| 19 |
+
MANAGE_PACKAGES = 'manage_packages',
|
| 20 |
+
VIEW_LEADS = 'view_leads',
|
| 21 |
+
CREATE_CONTRACT = 'create_contract',
|
| 22 |
+
SEND_CONTRACT = 'send_contract',
|
| 23 |
+
MANAGE_DELIVERABLES = 'manage_deliverables',
|
| 24 |
+
|
| 25 |
+
// Admin permissions
|
| 26 |
+
MODERATE_VENDORS = 'moderate_vendors',
|
| 27 |
+
MODERATE_CONTENT = 'moderate_content',
|
| 28 |
+
OVERSEE_CONTRACTS = 'oversee_contracts',
|
| 29 |
+
MEDIATE_DISPUTES = 'mediate_disputes',
|
| 30 |
+
EXPORT_AUDIT_TRAIL = 'export_audit_trail',
|
| 31 |
+
MANAGE_CATEGORIES = 'manage_categories',
|
| 32 |
+
IMPERSONATE_USERS = 'impersonate_users',
|
| 33 |
+
MANAGE_USERS = 'manage_users',
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const rolePermissions: Record<Role, Permission[]> = {
|
| 37 |
+
[Role.CLIENT]: [
|
| 38 |
+
Permission.VIEW_OWN_BOOKINGS,
|
| 39 |
+
Permission.CREATE_BOOKING,
|
| 40 |
+
Permission.SIGN_CONTRACT,
|
| 41 |
+
Permission.VIEW_OWN_CONTRACTS,
|
| 42 |
+
Permission.REQUEST_AMENDMENT,
|
| 43 |
+
],
|
| 44 |
+
[Role.VENDOR]: [
|
| 45 |
+
Permission.MANAGE_PROFILE,
|
| 46 |
+
Permission.MANAGE_PACKAGES,
|
| 47 |
+
Permission.VIEW_LEADS,
|
| 48 |
+
Permission.CREATE_CONTRACT,
|
| 49 |
+
Permission.SEND_CONTRACT,
|
| 50 |
+
Permission.MANAGE_DELIVERABLES,
|
| 51 |
+
],
|
| 52 |
+
[Role.ADMIN]: Object.values(Permission),
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export function hasPermission(role: Role, permission: Permission): boolean {
|
| 56 |
+
return rolePermissions[role]?.includes(permission) ?? false
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function requirePermission(permission: Permission) {
|
| 60 |
+
return function (req: any, res: any, next: Function) {
|
| 61 |
+
const role = req.user?.role as Role
|
| 62 |
+
if (!role || !hasPermission(role, permission)) {
|
| 63 |
+
return res.status(403).json({
|
| 64 |
+
error: 'Forbidden',
|
| 65 |
+
message: `Missing required permission: ${permission}`,
|
| 66 |
+
})
|
| 67 |
+
}
|
| 68 |
+
next()
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export function requireRole(...roles: Role[]) {
|
| 73 |
+
return function (req: any, res: any, next: Function) {
|
| 74 |
+
const role = req.user?.role as Role
|
| 75 |
+
if (!role || !roles.includes(role)) {
|
| 76 |
+
return res.status(403).json({
|
| 77 |
+
error: 'Forbidden',
|
| 78 |
+
message: `Access restricted to: ${roles.join(', ')}`,
|
| 79 |
+
})
|
| 80 |
+
}
|
| 81 |
+
next()
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Contract-specific RBAC: only the vendor who created it or the client who received it can access
|
| 86 |
+
export function requireContractAccess(contractIdParam: string = 'contractId') {
|
| 87 |
+
return async function (req: any, res: any, next: Function) {
|
| 88 |
+
const contractId = req.params[contractIdParam]
|
| 89 |
+
const userId = req.user?.id
|
| 90 |
+
const role = req.user?.role as Role
|
| 91 |
+
|
| 92 |
+
if (role === Role.ADMIN && hasPermission(role, Permission.OVERSEE_CONTRACTS)) {
|
| 93 |
+
return next()
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Fetch contract and verify ownership
|
| 97 |
+
try {
|
| 98 |
+
const contract = await fetchContract(contractId)
|
| 99 |
+
if (!contract) return res.status(404).json({ error: 'Contract not found' })
|
| 100 |
+
|
| 101 |
+
const hasAccess =
|
| 102 |
+
(role === Role.VENDOR && contract.vendorId === userId) ||
|
| 103 |
+
(role === Role.CLIENT && contract.clientId === userId)
|
| 104 |
+
|
| 105 |
+
if (!hasAccess) return res.status(403).json({ error: 'Access denied' })
|
| 106 |
+
next()
|
| 107 |
+
} catch (err) {
|
| 108 |
+
return res.status(500).json({ error: 'Internal error' })
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
async function fetchContract(id: string) {
|
| 114 |
+
// Replace with actual DB call
|
| 115 |
+
return { id, vendorId: '', clientId: '' }
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// ββ Impersonation (admin-only) ββ
|
| 119 |
+
// Admins can impersonate with TTL-bound sessions and audit reason field
|
| 120 |
+
|
| 121 |
+
export async function impersonateUser(adminId: string, targetUserId: string, reason: string, ttlMinutes: number = 30) {
|
| 122 |
+
// 1. Verify admin has IMPERSONATE_USERS permission
|
| 123 |
+
// 2. Create session with role=targetUser.role, token expires in ttlMinutes
|
| 124 |
+
// 3. Log to AdminAuditLog:
|
| 125 |
+
// { adminId, action: 'impersonate_start', target: targetUserId, detail: reason, ttl: ttlMinutes }
|
| 126 |
+
// 4. Return impersonation session token
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// ββ Rate Limiting ββ
|
| 130 |
+
// Keyed on IP + method + pathname, backed by Redis
|
| 131 |
+
// Implementation: use rate-limiter-flexible with Redis store
|
| 132 |
+
// export const rateLimiter = new RateLimiterRedis({...})
|