import express from 'express'; import * as UserService from '../calendar/users/users.service.js'; import {sendServerError} from './feedback.errors.js'; /** * This file is the ONLY place in the feedback module that knows how admin * authentication works today. No route handler and no service outside this * file may import users.service, read session headers, or touch bcrypt. * * Today: reuses the existing Calendar users/sessions mechanism. Any * activated @nachklang.art account may administer feedback — no roles. * Migrating to Keycloak later means writing a keycloakJwtAuthenticator * below and changing the one `activeAuthenticator` binding (plus the * frontend's login route handler) — nothing else in the feedback module * needs to change. * * Explicitly forbidden: accepting sessionId/sessionKey from query * parameters, even "temporarily". That is the exact mistake documented in * DEFERRED_SECURITY.md item 1 for the Calendar domain, where credentials * end up in access logs, browser history, proxy logs, and Referer headers. * Headers only. */ // The only thing the rest of the feedback module knows about an admin. export interface AdminIdentity { id: string; email: string; displayName: string; } // Pluggable strategy: extract + verify credentials from a request. // Returns the identity, or null if unauthenticated. Throws only on // infrastructure errors (e.g. the DB being unreachable). export type AdminAuthenticator = (req: express.Request) => Promise; // Current implementation: reads X-Session-Id / X-Session-Key headers, // delegates to the existing calendar UserService.checkSession(...). export const sessionHeaderAuthenticator: AdminAuthenticator = async (req) => { const sessionId = req.header('X-Session-Id'); const sessionKey = req.header('X-Session-Key'); if (!sessionId || !sessionKey) { return null; } const ip = req.ip || ''; const user = await UserService.checkSession(sessionId, sessionKey, ip); // Mirrors the Calendar domain's own convention: a valid session on an // inactive (not yet activated) account is not sufficient. if (!user || !user.isActive) { return null; } return { id: String(user.userId), email: user.email, displayName: user.fullName }; }; // Swap point: change this one binding to migrate to Keycloak. export const activeAuthenticator: AdminAuthenticator = sessionHeaderAuthenticator; // Express middleware used by every admin route. On success: // res.locals.admin = AdminIdentity, calls next(). On failure: 401. export const requireAdminAuth: express.RequestHandler = async (req, res, next) => { try { const identity = await activeAuthenticator(req); if (!identity) { res.status(401).send({status: 'UNAUTHORIZED', message: 'Anmeldung erforderlich.'}); return; } res.locals.admin = identity; next(); } catch (e: any) { sendServerError(res, e); } };