Files
edr-platform/apps/edr-passenger-api/src/common/acting-user.ts
2026-08-02 23:08:15 +03:00

64 lines
2.1 KiB
TypeScript

/**
* 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';
}