/** * Reading the authenticated staff member off the request. * * `JwtGuard` (from `@tria-plc/api-common`) puts the decoded IAM user on `request.user`. * Sibling controllers reach for `req.user?.id ?? req.user?.sub`, because the shape differs * slightly between token versions. This module centralises that so callers get a small, * typed value object instead of an `any` bag. */ /** Bilingual name as IAM stores it. */ interface ActingUserName { en?: string; am?: string; } /** The slice of `request.user` this app actually reads. */ export interface ActingUserClaims { id?: string; /** Older tokens carry the subject as `sub` rather than `id`. */ sub?: string; name?: ActingUserName | string | null; username?: string; email?: string; } /** The minimal Express request shape needed to reach the authenticated user. */ export interface RequestWithActingUser { user?: ActingUserClaims; } /** Who performed an action, resolved once at write time so readers need no IAM lookup. */ export interface ActingUser { /** IAM user id. */ id: string; /** Human-readable name, denormalized alongside the id. */ name: string; } /** * Resolves the acting staff member from a guarded request. * * Returns `null` when no user is attached — callers decide what that means. Endpoints behind * `@PassengerStaff(...)` always have one, since the guard rejects anonymous requests; system * paths (ticketing, cleanup jobs) legitimately have none and record themselves explicitly. */ export function resolveActingUser(req: RequestWithActingUser): ActingUser | null { const claims = req.user; const id = claims?.id ?? claims?.sub; if (!id) return null; return { id, name: resolveActingUserName(claims) }; } function resolveActingUserName(claims: ActingUserClaims | undefined): string { if (!claims) return 'Unknown'; const { name } = claims; if (typeof name === 'string' && name.trim()) return name.trim(); if (name && typeof name === 'object') { const localized = name.en?.trim() || name.am?.trim(); if (localized) return localized; } return claims.username?.trim() || claims.email?.trim() || 'Unknown'; }