← back to NationalPaperHangers
lib/auth/policies.js
90 lines
// lib/auth/policies.js
//
// Role-based access middleware factories. Reads req.session.userId (which
// Phase 2 will start setting; Phase 1 still uses installerId).
//
// Phase 1: NOT yet imported from server.js or any route. Exported for
// Phase 2 cutover. lib/auth.js (legacy installer-only auth) is left
// completely untouched.
//
// Design notes:
// - Middleware factories only — no module-level DB calls.
// - Each request hits `roles` once per call. Acceptable for Phase 2;
// if it becomes hot we can stash roles on req.user during attachUser.
// - JSON vs HTML response chosen via req.accepts('html'), matching the
// existing requireInstaller pattern in lib/auth.js.
'use strict';
const usersService = require('../services/users');
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
function rejectAuth(req, res) {
if (req.accepts && req.accepts('html')) {
return res.redirect('/login?next=' + encodeURIComponent(req.originalUrl || '/'));
}
return res.status(401).json({ error: 'auth_required' });
}
function rejectForbidden(req, res, role) {
if (req.accepts && req.accepts('html')) {
return res.status(403).render('public/error', {
title: 'Forbidden',
message: `This page requires the ${role} role.`
});
}
return res.status(403).json({ error: 'forbidden', required_role: role });
}
// ─────────────────────────────────────────────────────────────────────────
// requireRole(role | [roles])
//
// Allows the request through if the session user holds ANY of the listed
// roles. Decorates req.user with { id, roles[] } on success.
// ─────────────────────────────────────────────────────────────────────────
function requireRole(roleOrRoles) {
const wanted = Array.isArray(roleOrRoles) ? roleOrRoles : [roleOrRoles];
return async function policyMiddleware(req, res, next) {
try {
const userId = req.session && req.session.userId;
if (!userId) return rejectAuth(req, res);
const roles = await usersService.userRoles(userId);
const allowed = wanted.some(r => roles.includes(r));
if (!allowed) return rejectForbidden(req, res, wanted.join('|'));
// Decorate request for downstream handlers — cheap, scoped to one req.
req.user = req.user || { id: userId };
req.user.roles = roles;
return next();
} catch (e) {
return next(e);
}
};
}
// ─────────────────────────────────────────────────────────────────────────
// Convenience policies
// ─────────────────────────────────────────────────────────────────────────
const requireAdmin = requireRole('admin');
const requireOps = requireRole(['admin', 'ops']);
const requireInstallerOwner = requireRole(['admin', 'installer_owner']);
// requireInstallerMember — owner OR member counts; useful for /admin/bookings
// where any seat on the installer team should be allowed.
const requireInstallerMember = requireRole(['admin', 'installer_owner', 'installer_member']);
module.exports = {
requireRole,
requireAdmin,
requireOps,
requireInstallerOwner,
requireInstallerMember
};