You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

170 lines
3.9 KiB

/**
* Authorization Middleware
* JWT authentication and role-based access control
*/
import { verifyToken } from '../utils/auth.js';
import { query } from '../db/db.js';
/**
* Extract token from cookie
* @param {Object} req - Express request object
* @returns {string|null} Token from cookie
*/
function getTokenFromCookie(req) {
return req.cookies?.token || null;
}
/**
* Middleware to authenticate JWT token
* Adds user data to req.user
*/
export async function authenticate(req, res, next) {
try {
const token = getTokenFromCookie(req);
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
const result = await query(
'SELECT id, login, full_name, role, department_id, staff_id FROM users WHERE id = $1',
[decoded.userId]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'User not found' });
}
const user = result.rows[0];
const staffId = user.staff_id ?? decoded.staffId;
req.user = {
id: user.id,
login: user.login,
fullName: user.full_name,
role: user.role,
departmentId: user.department_id,
};
if (staffId != null) {
req.user.staffId = staffId;
}
next();
} catch (error) {
console.error('Auth middleware error:', error);
return res.status(500).json({ error: 'Authentication error' });
}
}
/**
* Middleware factory to require specific roles
* @param {string|string[]} roles - Required role(s)
* @returns {Function} Express middleware
*/
export function requireRole(roles) {
const allowedRoles = Array.isArray(roles) ? roles : [roles];
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
/**
* Middleware to require specific department access
* For managers to access their department's data
* @param {number} departmentId - Required department ID
* @returns {Function} Express middleware
*/
export function requireDepartment(departmentId) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Admins can access all departments
if (req.user.role === 'admin') {
return next();
}
// Managers can only access their department
if (req.user.role === 'manager' && req.user.departmentId !== departmentId) {
return res.status(403).json({ error: 'Access denied to this department' });
}
next();
};
}
/**
* Optional authentication middleware
* Attaches user to request if token is valid, but doesn't require it
*/
export async function optionalAuth(req, res, next) {
try {
const token = getTokenFromCookie(req);
if (!token) {
req.user = null;
return next();
}
const decoded = verifyToken(token);
if (!decoded) {
req.user = null;
return next();
}
const result = await query(
'SELECT id, login, full_name, role, department_id, staff_id FROM users WHERE id = $1',
[decoded.userId]
);
if (result.rows.length === 0) {
req.user = null;
return next();
}
const user = result.rows[0];
const staffId = user.staff_id ?? decoded.staffId;
req.user = {
id: user.id,
login: user.login,
fullName: user.full_name,
role: user.role,
departmentId: user.department_id,
};
if (staffId != null) {
req.user.staffId = staffId;
}
next();
} catch (error) {
// Don't block request on auth errors
req.user = null;
next();
}
}
export default {
authenticate,
requireRole,
requireDepartment,
optionalAuth,
};