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 a_sessions s JOIN a_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 a_user_roles ur JOIN a_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 a_user_roles ur JOIN a_role_permissions rp ON rp.role_id = ur.role_id JOIN a_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 a_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 a_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 };