| const { AppError } = require("../utils/appError"); |
|
|
| function extractBearerToken(authorizationHeader) { |
| if (!authorizationHeader || typeof authorizationHeader !== "string") { |
| return null; |
| } |
|
|
| const [scheme, token] = authorizationHeader.trim().split(/\s+/, 2); |
| if (!scheme || !token) { |
| return null; |
| } |
|
|
| if (scheme.toLowerCase() !== "bearer") { |
| return null; |
| } |
|
|
| return token; |
| } |
|
|
| function createRequireAuthMiddleware(authService) { |
| if (!authService) { |
| throw new Error("createRequireAuthMiddleware requires authService."); |
| } |
|
|
| return async function requireAuth(req, _res, next) { |
| try { |
| const token = extractBearerToken(req.headers.authorization); |
| if (!token) { |
| throw new AppError("Authorization bearer token is required.", 401, { |
| code: "TOKEN_MISSING", |
| }); |
| } |
|
|
| const decoded = authService.verifyAccessToken(token); |
| const userId = String(decoded.sub || "").trim(); |
| if (!userId) { |
| throw new AppError("Invalid token subject.", 401, { |
| code: "TOKEN_SUBJECT_INVALID", |
| }); |
| } |
|
|
| const authenticatedUser = await authService.getAuthenticatedUserById(userId); |
|
|
| req.auth = { |
| id: authenticatedUser.id, |
| email: authenticatedUser.email, |
| fullName: authenticatedUser.fullName, |
| roles: authenticatedUser.roles, |
| isSuperAdmin: authenticatedUser.isSuperAdmin, |
| tenant: authenticatedUser.tenant, |
| }; |
|
|
| if (!req.context) { |
| req.context = {}; |
| } |
| req.context.actorUserId = authenticatedUser.id; |
|
|
| return next(); |
| } catch (error) { |
| return next(error); |
| } |
| }; |
| } |
|
|
| function createRequireRolesMiddleware(allowedRoles) { |
| const normalizedAllowed = (Array.isArray(allowedRoles) ? allowedRoles : []) |
| .filter((role) => typeof role === "string") |
| .map((role) => role.trim().toLowerCase()) |
| .filter(Boolean); |
|
|
| return function requireRoles(req, _res, next) { |
| if (!req.auth) { |
| return next(new AppError("Unauthorized.", 401)); |
| } |
|
|
| if (req.auth.isSuperAdmin) { |
| return next(); |
| } |
|
|
| if (normalizedAllowed.length === 0) { |
| return next(); |
| } |
|
|
| const roleSet = new Set((req.auth.roles || []).map((role) => String(role).toLowerCase())); |
| const hasAllowedRole = normalizedAllowed.some((role) => roleSet.has(role)); |
|
|
| if (!hasAllowedRole) { |
| return next( |
| new AppError("Insufficient permissions for this action.", 403, { |
| code: "RBAC_FORBIDDEN", |
| requiredRoles: normalizedAllowed, |
| }) |
| ); |
| } |
|
|
| return next(); |
| }; |
| } |
|
|
| function createTenantScopeMiddleware() { |
| return function enforceTenantScope(req, _res, next) { |
| if (!req.auth) { |
| return next(); |
| } |
|
|
| if (!req.context) { |
| req.context = {}; |
| } |
|
|
| const requestedTenantCodeRaw = req.query?.tenantCode; |
| const requestedTenantCode = |
| typeof requestedTenantCodeRaw === "string" && requestedTenantCodeRaw.trim() |
| ? requestedTenantCodeRaw.trim() |
| : null; |
|
|
| if (req.auth.isSuperAdmin) { |
| req.context.tenantCode = requestedTenantCode; |
| if (requestedTenantCode) { |
| req.query.tenantCode = requestedTenantCode; |
| } |
| return next(); |
| } |
|
|
| const userTenantCode = req.auth.tenant?.code || null; |
| if (!userTenantCode) { |
| return next( |
| new AppError("Authenticated user has no tenant scope.", 403, { |
| code: "TENANT_SCOPE_MISSING", |
| }) |
| ); |
| } |
|
|
| if (requestedTenantCode && requestedTenantCode !== userTenantCode) { |
| return next( |
| new AppError("Cross-tenant access is not allowed.", 403, { |
| code: "TENANT_SCOPE_VIOLATION", |
| requestedTenantCode, |
| tenantCode: userTenantCode, |
| }) |
| ); |
| } |
|
|
| req.context.tenantCode = userTenantCode; |
| req.query.tenantCode = userTenantCode; |
|
|
| return next(); |
| }; |
| } |
|
|
| module.exports = { |
| createRequireAuthMiddleware, |
| createRequireRolesMiddleware, |
| createTenantScopeMiddleware, |
| }; |
|
|