Add Feedback domain module: public submission flow, admin CRUD, reporting (#7)
Jenkins Production Deployment
Jenkins Production Deployment
Co-authored-by: Patrick Müller <mail@pmueller.me> Reviewed-on: #7 Co-authored-by: Patrick Mueller <patrick@mueller-patrick.tech> Co-committed-by: Patrick Mueller <patrick@mueller-patrick.tech>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import express from 'express';
|
||||
import * as UserService from '../calendar/users/users.service';
|
||||
import {sendServerError} from './feedback.errors';
|
||||
|
||||
/**
|
||||
* 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<AdminIdentity | null>;
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user