Files
inventory/inventory-server/shared/auth/middleware.js
T

129 lines
3.6 KiB
JavaScript

import { extractBearerToken, verifyToken, TokenError } from './verify.js';
const USER_CACHE_TTL_MS = 60_000;
// Source IPs that bypass token auth — used so the office kiosk can render
// /small without anyone having to log in daily on the device. Synthetic user
// has no permissions, so only endpoints that don't gate on requirePermission()
// are reachable. Requires server.js `trust proxy` setting so req.ip is the
// real client behind Caddy, not 127.0.0.1.
function parseKioskIps(raw) {
return new Set(
(raw || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
);
}
function createUserCache() {
const entries = new Map();
return {
get(userId) {
const hit = entries.get(userId);
if (!hit) return null;
if (Date.now() - hit.cachedAt > USER_CACHE_TTL_MS) {
entries.delete(userId);
return null;
}
return hit.user;
},
set(userId, user) {
entries.set(userId, { user, cachedAt: Date.now() });
},
invalidate(userId) {
entries.delete(userId);
},
};
}
async function loadUser(pool, userId) {
const userResult = await pool.query(
'SELECT id, username, email, is_admin, is_active FROM users WHERE id = $1',
[userId]
);
const user = userResult.rows[0];
if (!user) return null;
if (user.is_admin) {
user.permissions = [];
return user;
}
const permResult = await pool.query(
`SELECT p.code
FROM permissions p
JOIN user_permissions up ON p.id = up.permission_id
WHERE up.user_id = $1`,
[userId]
);
user.permissions = permResult.rows.map((r) => r.code);
return user;
}
export function authenticate({ pool, secret = process.env.JWT_SECRET, kioskIps = process.env.KIOSK_IPS }) {
const cache = createUserCache();
const kioskIpSet = parseKioskIps(kioskIps);
return async function authenticateMiddleware(req, res, next) {
if (kioskIpSet.size > 0 && kioskIpSet.has(req.ip)) {
req.user = {
id: 'kiosk',
username: 'kiosk',
is_admin: false,
is_active: true,
permissions: [],
is_kiosk: true,
};
return next();
}
let decoded;
try {
const token = extractBearerToken(req.headers.authorization);
decoded = verifyToken(token, secret);
} catch (err) {
if (err instanceof TokenError) {
return res.status(401).json({ error: err.message });
}
return res.status(401).json({ error: 'Authentication required' });
}
try {
let user = cache.get(decoded.userId);
if (!user) {
user = await loadUser(pool, decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
cache.set(decoded.userId, user);
}
if (!user.is_active) {
cache.invalidate(decoded.userId);
return res.status(403).json({ error: 'Account inactive' });
}
req.user = user;
next();
} catch (err) {
next(err);
}
};
}
export function requirePermission(code) {
return function requirePermissionMiddleware(req, res, next) {
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
if (req.user.is_admin) return next();
if (Array.isArray(req.user.permissions) && req.user.permissions.includes(code)) {
return next();
}
res.status(403).json({ error: 'Insufficient permissions' });
};
}
export function requireAdmin(req, res, next) {
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
if (req.user.is_admin) return next();
res.status(403).json({ error: 'Admin only' });
}