Files
pulse-signage/src/web/lib/session.js
T
2026-07-25 02:29:19 +01:00

130 lines
4.3 KiB
JavaScript

const { normalizePermissionKeys } = require('../../rbac');
function createSessionService(options) {
const sessionCookieName = String(options && options.sessionCookieName || '').trim();
const sessionMaxAgeMs = Number(options && options.sessionMaxAgeMs);
const hashSessionToken = options && options.hashSessionToken;
const createSessionToken = options && options.createSessionToken;
if (!sessionCookieName || !Number.isFinite(sessionMaxAgeMs) || typeof hashSessionToken !== 'function' || typeof createSessionToken !== 'function') {
throw new Error('createSessionService requires the session dependencies.');
}
function parseCookies(cookieHeader) {
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
if (!pair) {
return cookies;
}
const separatorIndex = pair.indexOf('=');
if (separatorIndex === -1) {
return cookies;
}
const name = decodeURIComponent(pair.slice(0, separatorIndex).trim());
const value = decodeURIComponent(pair.slice(separatorIndex + 1).trim());
if (name) {
cookies[name] = value;
}
return cookies;
}, {});
}
function serializeCookie(name, value, options) {
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
if (options && options.maxAge !== undefined) {
parts.push(`Max-Age=${Math.max(0, Math.trunc(Number(options.maxAge) / 1000))}`);
}
parts.push('Path=/');
parts.push('HttpOnly');
parts.push('SameSite=Lax');
return parts.join('; ');
}
function clearSessionCookie(res) {
res.setHeader('Set-Cookie', serializeCookie(sessionCookieName, '', { maxAge: 0 }));
}
function setSessionCookie(res, token) {
res.setHeader('Set-Cookie', serializeCookie(sessionCookieName, token, { maxAge: sessionMaxAgeMs }));
}
async function loadCurrentUser(pool, req) {
const cookies = parseCookies(req.headers.cookie || '');
const token = cookies[sessionCookieName];
if (!token) {
return null;
}
const tokenHash = hashSessionToken(token);
const [rows] = await pool.query(
`SELECT s.user_id, u.id, u.name, u.username
FROM auth_sessions s
JOIN users u ON u.id = s.user_id
WHERE s.session_hash = ?
AND s.expires_at > NOW()
LIMIT 1`,
[tokenHash]
);
if (!rows.length) {
return null;
}
const userId = Number(rows[0].id);
const [roleRows] = await pool.query(
`SELECT r.role_key
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = ?
ORDER BY r.name ASC`,
[userId]
);
const [permissionRows] = await pool.query(
`SELECT p.permission_key
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = ?
ORDER BY p.section_name ASC, p.name ASC`,
[userId]
);
await pool.query('UPDATE auth_sessions SET last_used_at = CURRENT_TIMESTAMP, modified_by = ? WHERE session_hash = ?', [rows[0].user_id, tokenHash]);
return Object.assign({}, rows[0], {
roleKeys: roleRows.map(function (row) {
return String(row.role_key || '').trim();
}).filter(Boolean),
permissionKeys: normalizePermissionKeys(permissionRows.map(function (row) {
return String(row.permission_key || '').trim();
}))
});
}
async function createUserSession(pool, userId) {
const token = createSessionToken();
const tokenHash = hashSessionToken(token);
const expiresAt = new Date(Date.now() + sessionMaxAgeMs);
await pool.query(
'INSERT INTO auth_sessions (session_hash, user_id, expires_at, created_by, modified_by) VALUES (?, ?, ?, ?, ?)',
[tokenHash, userId, expiresAt, userId, userId]
);
return token;
}
function requireAuth(req, res, next) {
if (req.currentUser) {
return next();
}
res.redirect('/login?message=' + encodeURIComponent('Please sign in to continue.'));
}
return {
parseCookies: parseCookies,
serializeCookie: serializeCookie,
clearSessionCookie: clearSessionCookie,
setSessionCookie: setSessionCookie,
loadCurrentUser: loadCurrentUser,
createUserSession: createUserSession,
requireAuth: requireAuth
};
}
module.exports = { createSessionService };