feat(freight-api): add non-PII auth context to request log line

This commit is contained in:
Nathnael
2026-08-12 11:35:36 +00:00
parent 2a816912fc
commit 0f1bac8080
2 changed files with 132 additions and 9 deletions

View File

@@ -20,7 +20,39 @@ interface LoggedRequest {
headers: Record<string, string | string[] | undefined>;
ip?: string;
query?: Record<string, unknown>;
user?: Record<string, unknown> | null;
/** Set by the IAM JwtGuard AFTER this middleware runs — read at emit time. */
user?: AuthenticatedUser | null;
currentUnitId?: string;
}
/**
* The subset of `TCurrentUser` (@tria-plc/api-common) the log line reads.
* Everything here is an identifier, a key or a status — the personal fields on
* that type (name, email, username, phoneNumber) are deliberately absent so
* they cannot be picked up by accident.
*/
interface AuthenticatedUser {
id?: string;
sub?: string;
userId?: string;
sessionId?: string;
userType?: string;
status?: string;
roles?: { key?: string }[];
permissions?: unknown[];
employee?: {
id?: string;
organizationId?: string;
unitId?: string;
position?: {
id?: string;
key?: string;
employeePositionId?: string;
isDelegate?: boolean;
delegatorId?: string;
positionType?: { key?: string };
};
};
}
interface LoggedResponse {
@@ -35,13 +67,48 @@ const header = (req: LoggedRequest, name: string): string | undefined => {
return Array.isArray(value) ? value[0] : value;
};
const userId = (req: LoggedRequest): string | undefined => {
const userId = (req: LoggedRequest): string | undefined =>
req.user?.id ?? req.user?.sub ?? req.user?.userId;
/**
* Who the caller was acting as — WITHOUT any personal data. Ids, role/position
* keys and statuses only: enough to answer "which desk did this", "was it a
* delegate", "which tenant", and to spot an authorization problem, with nothing
* that identifies the human behind the account beyond the opaque user id.
*
* `authenticated: false` with `hasBearer: true` is the signature of a rejected
* token (expired session, bad signature) as opposed to a missing one.
*/
const authContext = (req: LoggedRequest): Record<string, unknown> => {
const user = req.user;
if (!user) return undefined;
const id = user.id ?? user.sub ?? user.userId;
return typeof id === "string" || typeof id === "number"
? String(id)
: undefined;
const position = user?.employee?.position;
return {
authenticated: Boolean(user),
hasBearer: header(req, "authorization")?.startsWith("Bearer ") ?? false,
// Which frontend called — /auth/login rejects cross-audience credentials on it.
clientApp: header(req, "x-client-app"),
userId: userId(req),
sessionId: user?.sessionId,
userType: user?.userType,
userStatus: user?.status,
roles: user?.roles?.map((role) => role.key).filter(Boolean),
// Count only: the full grant list is hundreds of keys and would dwarf the line.
permissionCount: user?.permissions?.length,
employeeId: user?.employee?.id,
organizationId: user?.employee?.organizationId,
unitId: user?.employee?.unitId ?? req.currentUnitId,
positionId: position?.id,
positionKey: position?.key,
positionType: position?.positionType?.key,
employeePositionId: position?.employeePositionId,
// Acting on someone else's behalf — the first thing to check when a staff
// action lands under an unexpected desk.
isDelegate: position?.isDelegate,
delegatorId: position?.delegatorId,
// Tenant/scope headers the frontends send alongside the token.
projectId:
header(req, "current-project-id") ?? header(req, "x-current-project-id"),
};
};
/**
@@ -101,6 +168,9 @@ export class RequestLogMiddleware implements NestMiddleware {
status,
durationMs,
userId: userId(req),
// Read at emit time on purpose: the guard populates req.user long
// after this middleware handed control on.
auth: authContext(req),
ip: req.ip,
userAgent: header(req, "user-agent"),
query: