feat(GRO-653): add portal session middleware and server-side audit logging (#300)
* feat(GRO-653): add portal session middleware and server-side audit logging
- Add validatePortalSession middleware that reads X-Impersonation-Session-Id header,
queries impersonationSessions, and sets portalClientId + portalSessionId on the context
- Add portalAudit middleware that logs all portal requests to impersonationAuditLogs table
- Apply both middlewares to the portalRouter
- Replace all getClientIdFromSession() calls with c.get("portalClientId")
- Remove getClientIdFromSession() helper and inline session checks in waitlist routes
- Consistent session.expiry > new Date() check across all routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-653): remove unused sessionId variable and and import
Fix lint errors flagged by QA:
- Remove unused `sessionId` variable from PATCH waitlist handler
- Remove unused `and` import from portal.ts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Flea Flicker <flea-flicker@groombook.ai>
This commit was merged in pull request #300.
This commit is contained in:
committed by
GitHub
parent
b904418628
commit
2577e33c50
@@ -0,0 +1,45 @@
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { getDb, impersonationAuditLogs } from "@groombook/db";
|
||||
import type { PortalEnv } from "./portalSession.js";
|
||||
|
||||
/**
|
||||
* Server-side audit logging middleware for portal routes.
|
||||
* Applied after validatePortalSession in the middleware chain.
|
||||
*
|
||||
* After the route handler completes (await next()), inserts an audit log entry
|
||||
* into impersonationAuditLogs:
|
||||
* - sessionId: from c.get("portalSessionId")
|
||||
* - action: "{METHOD} {routePath}" (e.g., "GET /portal/appointments")
|
||||
* - pageVisited: c.req.path
|
||||
* - metadata: { method, statusCode: c.res.status }
|
||||
*
|
||||
* Log entries are written for both success and error responses.
|
||||
* Does NOT throw if audit logging fails — errors are logged but the user's
|
||||
* request is not affected.
|
||||
*/
|
||||
export const portalAudit: MiddlewareHandler<PortalEnv> = async (c, next) => {
|
||||
await next();
|
||||
|
||||
const sessionId = c.get("portalSessionId");
|
||||
if (!sessionId) return;
|
||||
|
||||
const method = c.req.method;
|
||||
const routePath = c.req.path;
|
||||
const pageVisited = c.req.path;
|
||||
const statusCode = c.res.status;
|
||||
|
||||
try {
|
||||
const db = getDb();
|
||||
await db
|
||||
.insert(impersonationAuditLogs)
|
||||
.values({
|
||||
sessionId,
|
||||
action: `${method} ${routePath}`,
|
||||
pageVisited,
|
||||
metadata: { method, statusCode },
|
||||
})
|
||||
.returning();
|
||||
} catch (err) {
|
||||
console.error("[portalAudit] Failed to write audit log:", err);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { and, eq, getDb, impersonationSessions } from "@groombook/db";
|
||||
|
||||
export interface PortalEnv {
|
||||
Variables: {
|
||||
portalClientId: string;
|
||||
portalSessionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the X-Impersonation-Session-Id header against the impersonationSessions table.
|
||||
* Must be applied to all portal routes.
|
||||
*
|
||||
* Reads x-session-id from request headers, queries impersonationSessions for a row where
|
||||
* id = sessionId AND status = 'active', and checks session.expiresAt > new Date().
|
||||
* Returns 401 if session is invalid/missing/expired.
|
||||
* On success, sets c.set("portalClientId", session.clientId) and c.set("portalSessionId", session.id).
|
||||
*/
|
||||
export const validatePortalSession: MiddlewareHandler<PortalEnv> = async (c, next) => {
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
if (!sessionId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(impersonationSessions)
|
||||
.where(and(eq(impersonationSessions.id, sessionId), eq(impersonationSessions.status, "active")))
|
||||
.limit(1);
|
||||
|
||||
if (!session || session.expiresAt <= new Date()) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
c.set("portalClientId", session.clientId);
|
||||
c.set("portalSessionId", session.id);
|
||||
await next();
|
||||
};
|
||||
Reference in New Issue
Block a user