2026-01-28 14:36:33 -07:00
|
|
|
// Authentication Middleware
|
2026-05-06 11:44:17 -06:00
|
|
|
const pool = require('../db');
|
2026-06-24 11:36:25 -06:00
|
|
|
const { teamToIvanti } = require('../helpers/teams');
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-05-06 11:44:17 -06:00
|
|
|
// Require authenticated user — no parameters needed, pool is imported directly
|
|
|
|
|
function requireAuth() {
|
2026-01-28 14:36:33 -07:00
|
|
|
return async (req, res, next) => {
|
|
|
|
|
const sessionId = req.cookies?.session_id;
|
|
|
|
|
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-06 11:44:17 -06:00
|
|
|
const { rows } = await pool.query(
|
2026-06-24 12:53:05 -06:00
|
|
|
`SELECT s.*, s.impersonate_user_id,
|
|
|
|
|
u.id as user_id, u.username, u.email, u.role, u.user_group, u.bu_teams, u.is_active
|
2026-05-06 11:44:17 -06:00
|
|
|
FROM sessions s
|
|
|
|
|
JOIN users u ON s.user_id = u.id
|
|
|
|
|
WHERE s.session_id = $1 AND s.expires_at > NOW()`,
|
|
|
|
|
[sessionId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const session = rows[0];
|
2026-01-28 14:36:33 -07:00
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
return res.status(401).json({ error: 'Session expired or invalid' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!session.is_active) {
|
|
|
|
|
return res.status(401).json({ error: 'Account is disabled' });
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 12:53:05 -06:00
|
|
|
// Store the real admin identity (always the session owner)
|
|
|
|
|
req.realUser = {
|
2026-01-28 14:36:33 -07:00
|
|
|
id: session.user_id,
|
|
|
|
|
username: session.username,
|
|
|
|
|
email: session.email,
|
2026-04-06 16:18:07 -06:00
|
|
|
role: session.role,
|
2026-05-05 11:04:53 -06:00
|
|
|
group: session.user_group,
|
|
|
|
|
teams: session.bu_teams ? session.bu_teams.split(',').filter(Boolean) : []
|
2026-01-28 14:36:33 -07:00
|
|
|
};
|
|
|
|
|
|
2026-06-24 12:53:05 -06:00
|
|
|
// If impersonating, load the target user's identity
|
|
|
|
|
if (session.impersonate_user_id) {
|
|
|
|
|
const { rows: targetRows } = await pool.query(
|
|
|
|
|
`SELECT id, username, email, role, user_group, bu_teams, is_active FROM users WHERE id = $1`,
|
|
|
|
|
[session.impersonate_user_id]
|
|
|
|
|
);
|
|
|
|
|
const target = targetRows[0];
|
|
|
|
|
|
|
|
|
|
if (target && target.is_active) {
|
|
|
|
|
req.user = {
|
|
|
|
|
id: target.id,
|
|
|
|
|
username: target.username,
|
|
|
|
|
email: target.email,
|
|
|
|
|
role: target.role,
|
|
|
|
|
group: target.user_group,
|
|
|
|
|
teams: target.bu_teams ? target.bu_teams.split(',').filter(Boolean) : []
|
|
|
|
|
};
|
|
|
|
|
req.impersonating = true;
|
|
|
|
|
} else {
|
|
|
|
|
// Target user no longer valid — clear impersonation and use real user
|
|
|
|
|
await pool.query(`UPDATE sessions SET impersonate_user_id = NULL WHERE session_id = $1`, [sessionId]);
|
|
|
|
|
req.user = req.realUser;
|
|
|
|
|
req.impersonating = false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
req.user = req.realUser;
|
|
|
|
|
req.impersonating = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
next();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Auth middleware error:', err);
|
|
|
|
|
return res.status(500).json({ error: 'Authentication error' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 16:18:07 -06:00
|
|
|
// Require specific group(s)
|
|
|
|
|
function requireGroup(...allowedGroups) {
|
2026-01-28 14:36:33 -07:00
|
|
|
return (req, res, next) => {
|
|
|
|
|
if (!req.user) {
|
|
|
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 16:18:07 -06:00
|
|
|
if (!allowedGroups.includes(req.user.group)) {
|
2026-01-28 14:36:33 -07:00
|
|
|
return res.status(403).json({
|
|
|
|
|
error: 'Insufficient permissions',
|
2026-04-06 16:18:07 -06:00
|
|
|
required: allowedGroups,
|
|
|
|
|
current: req.user.group
|
2026-01-28 14:36:33 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 11:36:25 -06:00
|
|
|
// Require team assignment — enforces team-scoped data access.
|
|
|
|
|
// Admin group bypasses (req.teamScope = null means "no filter").
|
|
|
|
|
// Non-admin users without teams get 403.
|
|
|
|
|
// Non-admin users with teams get req.teamScope = { short: [...], ivanti: [...] }.
|
|
|
|
|
function requireTeam() {
|
|
|
|
|
return (req, res, next) => {
|
|
|
|
|
if (!req.user) {
|
|
|
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admin bypass — full access to all teams
|
|
|
|
|
if (req.user.group === 'Admin') {
|
|
|
|
|
req.teamScope = null;
|
|
|
|
|
return next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No teams assigned — block access
|
|
|
|
|
if (!req.user.teams || req.user.teams.length === 0) {
|
|
|
|
|
return res.status(403).json({
|
|
|
|
|
error: 'No team assignment. Contact an administrator to assign BU teams to your account.',
|
|
|
|
|
code: 'NO_TEAM_ASSIGNMENT'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build scope with both naming conventions
|
|
|
|
|
req.teamScope = {
|
|
|
|
|
short: req.user.teams,
|
|
|
|
|
ivanti: req.user.teams.map(t => teamToIvanti(t))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { requireAuth, requireGroup, requireTeam };
|