259 lines
7.3 KiB
TypeScript
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';
|
|
}
|
|
|
|
|