// ── Session middleware ───────────────────────────────────────────────────── // // Express middleware built on the session service. Three pieces: // // attachSession — best-effort: decorate the request with session info if a // valid token is present, but never reject. For routes that // behave differently for anon vs authed callers. // requireAuth — the gate for protected routes. Preserves the exact behavior // of the old isLoggedIn: re-validate the user against the DB on // every request so a demoted/deleted user loses access // immediately, and set req.user to the fresh DB row. // requireRole — role gate factory, unchanged from the original. const sessionService = require('./session.service') const users = require('../model/users/users.model') const log = require('../utils/logger')('session') // True if this session was issued at or before the user's tokens_valid_after // cutoff (i.e. revoked by a password change / log-out-everywhere). Both the JWT // iat and the cutoff are second-granular, so the comparison is inclusive: a token // minted in the same second as the bump must still be revoked (otherwise it would // survive its full lifetime through that 1s alignment). The only cost is that a // re-login within the same second as the change is rejected until the next second // — a self-healing blip, and far preferable to leaving a stale token valid. function isBeforeCutoff(session, tokensValidAfter) { if (!tokensValidAfter || session.createdAt == null) return false return session.createdAt <= new Date(tokensValidAfter).getTime() } // Best-effort: if the request carries a valid session token, attach the decoded // session (no DB hit), its auth method, and request metadata. Never rejects — // anonymous requests simply pass through with req.session undefined. function attachSession(req, res, next) { const session = sessionService.validateSession(req) if (session) { req.session = session req.authMethod = session.authMethod req.sessionMeta = sessionService.sessionMeta(req) } return next() } // Gate middleware for protected (admin) routes. Re-validates the token against // the database on every request so a demoted or deleted user loses access // immediately, instead of keeping their old role (or a working session) until // the JWT expires. req.user carries the fresh DB row, not the token payload. async function requireAuth(req, res, next) { const session = sessionService.validateSession(req) if (!session) return res.status(401).json({ message: 'Unauthorized' }) try { const user = await users.getById(session.userId) if (!user) return res.status(401).json({ message: 'Unauthorized' }) // deleted since token issued // Status gate, enforced on every request (same immediacy as the cutoff // below): a player disabled/banned by staff loses access on their very next // request, not when their JWT eventually expires. if (user.status && user.status !== 'active') { return res.status(403).json({ message: 'Account disabled' }) } // Revocation, enforced here (not in stateless token verification): // 1. per-user cutoff — password change / "log out everywhere" bumps // tokens_valid_after; any token issued before it is dead. // 2. per-session denylist — a single logout adds this jti to revoked_sessions. if (isBeforeCutoff(session, user.tokens_valid_after)) { return res.status(401).json({ message: 'Unauthorized' }) } if (await sessionService.isSessionRevoked(session.sessionId)) { return res.status(401).json({ message: 'Unauthorized' }) } req.user = user req.session = session req.authMethod = session.authMethod return next() } catch (err) { log.error('requireAuth', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // Gate middleware factory: allow only the listed roles. Assumes requireAuth ran // first so req.user is populated. Use for admin-only endpoints (users, site // mode, settings) so a lower-privilege editor cannot reach them. function requireRole(...roles) { return (req, res, next) => { if (roles.includes(req.user?.role)) return next() return res.status(403).json({ message: 'Forbidden' }) } } module.exports = { attachSession, requireAuth, requireRole, }