Re_Backend/src/middlewares/authorization.middleware.ts

259 lines
7.3 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import { Participant } from '@models/Participant';
import { WorkflowRequest } from '@models/WorkflowRequest';
import { Op } from 'sequelize';
type AllowedType = 'INITIATOR' | 'APPROVER' | 'SPECTATOR';
// Helper to check if identifier is UUID or requestNumber
function isUuid(identifier: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(identifier);
}
// Helper to find workflow by either requestId or requestNumber
async function findWorkflowByIdentifier(identifier: string): Promise<WorkflowRequest | null> {
if (isUuid(identifier)) {
return await WorkflowRequest.findByPk(identifier);
} else {
return await WorkflowRequest.findOne({
where: { requestNumber: identifier }
});
}
}
export function requireParticipantTypes(allowed: AllowedType[]) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const userId: string | undefined = (req as any).user?.userId || (req as any).user?.id;
const requestIdentifier: string | undefined = (req.params as any)?.id;
if (!userId || !requestIdentifier) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
// Resolve requestIdentifier to actual requestId (UUID)
const workflow = await findWorkflowByIdentifier(requestIdentifier);
if (!workflow) {
return res.status(404).json({ success: false, error: 'Workflow not found' });
}
const actualRequestId = (workflow as any).requestId;
// Check initiator
if (allowed.includes('INITIATOR')) {
if ((workflow as any).initiatorId === userId) {
return next();
}
}
// Check participants table for SPECTATOR
if (allowed.includes('SPECTATOR')) {
const participant = await Participant.findOne({
where: {
requestId: actualRequestId,
userId,
participantType: 'SPECTATOR',
isActive: true
},
});
if (participant) {
return next();
}
}
// For APPROVER role, check ApprovalLevel table
// This is the primary source of truth for approvers
if (allowed.includes('APPROVER')) {
const { ApprovalLevel } = await import('@models/ApprovalLevel');
const approvalLevel = await ApprovalLevel.findOne({
where: {
requestId: actualRequestId,
approverId: userId,
status: { [Op.in]: ['PENDING', 'IN_PROGRESS'] as any }
}
});
if (approvalLevel) {
return next();
}
// Fallback: also check Participants table (some approvers might be added there)
const participant = await Participant.findOne({
where: {
requestId: actualRequestId,
userId,
participantType: 'APPROVER',
isActive: true
},
});
if (participant) {
return next();
}
}
return res.status(403).json({ success: false, error: 'Insufficient permissions' });
} catch (err) {
console.error('❌ Authorization check failed:', err);
return res.status(500).json({ success: false, error: 'Authorization check failed' });
}
};
}
/**
* Role-Based Access Control (RBAC) Middleware
*
* Roles:
* - USER: Default role - can create/view own requests, participate in assigned workflows
* - MANAGEMENT: Read access to all requests, enhanced dashboard visibility
* - ADMIN: Full system access - configuration, user management, all workflows
*/
/**
* Middleware: requireAdmin
*
* Purpose: Restrict access to ADMIN role only
*
* Use Cases:
* - System configuration management
* - User role assignment
* - Holiday calendar management
* - Email/notification settings
* - Audit log access
*
* Returns: 403 Forbidden if user is not ADMIN
*/
export function requireAdmin(req: Request, res: Response, next: NextFunction): void {
try {
const userRole = req.user?.role;
if (userRole !== 'ADMIN') {
res.status(403).json({
success: false,
error: 'Admin access required. Only administrators can perform this action.'
});
return;
}
next();
} catch (error) {
console.error('❌ Admin authorization failed:', error);
res.status(500).json({
success: false,
error: 'Authorization check failed'
});
}
}
/**
* Middleware: requireManagement
*
* Purpose: Restrict access to MANAGEMENT and ADMIN roles
*
* Use Cases:
* - View all requests (read-only)
* - Access comprehensive dashboards
* - Export reports
* - View analytics across all departments
*
* Permissions:
* - MANAGEMENT: Read access to all data
* - ADMIN: Read + Write access
*
* Returns: 403 Forbidden if user is only USER role
*/
export function requireManagement(req: Request, res: Response, next: NextFunction): void {
try {
const userRole = req.user?.role;
if (userRole !== 'MANAGEMENT' && userRole !== 'ADMIN') {
res.status(403).json({
success: false,
error: 'Management access required. This feature is available to management and admin users only.'
});
return;
}
next();
} catch (error) {
console.error('❌ Management authorization failed:', error);
res.status(500).json({
success: false,
error: 'Authorization check failed'
});
}
}
/**
* Middleware: requireRole
*
* Purpose: Flexible role checking - accepts multiple allowed roles
*
* Example Usage:
* - requireRole(['ADMIN']) - Admin only
* - requireRole(['MANAGEMENT', 'ADMIN']) - Management or Admin
* - requireRole(['USER', 'MANAGEMENT', 'ADMIN']) - Any authenticated user
*
* @param allowedRoles - Array of allowed role strings
* @returns Express middleware function
*/
export function requireRole(allowedRoles: ('USER' | 'MANAGEMENT' | 'ADMIN')[]) {
return (req: Request, res: Response, next: NextFunction): void => {
try {
const userRole = req.user?.role;
if (!userRole || !allowedRoles.includes(userRole as any)) {
res.status(403).json({
success: false,
error: `Access denied. Required roles: ${allowedRoles.join(' or ')}`
});
return;
}
next();
} catch (error) {
console.error('❌ Role authorization failed:', error);
res.status(500).json({
success: false,
error: 'Authorization check failed'
});
}
};
}
/**
* Helper: Check if user has specific role
*
* Purpose: Programmatic role checking within controllers
*
* @param user - Express req.user object
* @param role - Role to check
* @returns boolean
*/
export function hasRole(user: any, role: 'USER' | 'MANAGEMENT' | 'ADMIN'): boolean {
return user?.role === role;
}
/**
* Helper: Check if user has management or admin access
*
* Purpose: Quick check for enhanced permissions
*
* @param user - Express req.user object
* @returns boolean
*/
export function hasManagementAccess(user: any): boolean {
return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN';
}
/**
* Helper: Check if user has admin access
*
* Purpose: Quick check for admin-only permissions
*
* @param user - Express req.user object
* @returns boolean
*/
export function hasAdminAccess(user: any): boolean {
return user?.role === 'ADMIN';
}