import { ForbiddenException } from "@nestjs/common"; import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type"; const SUPER_ADMIN_ROLE = "super_admin"; const ORGANIZATION_ADMIN_ROLE = "organization_admin"; type PermissionLike = { key?: string }; type PositionLike = { permissions?: PermissionLike[] }; /** * The two token shapes IAM issues. `employee` is an object on a session token * (`TCurrentUser`) and an array on the raw payload (`TCurrentTokenUser`); both * reach controllers depending on how the session was minted, so every reader has * to handle both. Mirrors hr-api's `hr-permission.util.ts` and freight's * `collectPermissionKeys`. */ export type MeLikeUser = { id?: string; roles?: { key?: string }[]; permissions?: PermissionLike[]; employee?: | { id?: string; organizationId?: string; position?: PositionLike; delegatedPositions?: PositionLike[]; } | { id?: string; organizationId?: string; positions?: PositionLike[] }[] | null; }; export function isSuperAdmin(user: MeLikeUser | null | undefined): boolean { return Boolean(user?.roles?.some((r) => r.key === SUPER_ADMIN_ROLE)); } export function isOrganizationAdmin( user: MeLikeUser | null | undefined, ): boolean { return Boolean(user?.roles?.some((r) => r.key === ORGANIZATION_ADMIN_ROLE)); } /** Flat permission keys from roles and every position the token carries. */ export function collectPermissionKeys( user: MeLikeUser | null | undefined, ): string[] { if (!user) return []; const keys = new Set(); for (const p of user.permissions ?? []) { if (p.key) keys.add(p.key); } const employee = user.employee; if (!employee) return [...keys]; if (Array.isArray(employee)) { for (const emp of employee) { for (const pos of emp.positions ?? []) { for (const p of pos.permissions ?? []) { if (p.key) keys.add(p.key); } } } return [...keys]; } for (const p of employee.position?.permissions ?? []) { if (p.key) keys.add(p.key); } for (const delegated of employee.delegatedPositions ?? []) { for (const p of delegated.permissions ?? []) { if (p.key) keys.add(p.key); } } return [...keys]; } export function hasFinancePermission( user: MeLikeUser | null | undefined, permissionKey: string, ): boolean { if (!user) return false; if (isSuperAdmin(user)) return true; return collectPermissionKeys(user).includes(permissionKey); } export function assertFinancePermission( user: TCurrentUser | MeLikeUser | null | undefined, permissionKey: string, ): void { if (hasFinancePermission(user, permissionKey)) return; throw new ForbiddenException(`Missing permission: ${permissionKey}`); } /** * The caller's `iam.employees.id`. * * Finance records who prepared and who posted each entry, and that identity is * the employee — not the user — because it is the employee that carries the * organization. Returns null for a token with no employee. */ export function currentEmployeeId( user: MeLikeUser | null | undefined, ): string | null { const employee = user?.employee; if (!employee) return null; if (Array.isArray(employee)) return employee[0]?.id ?? null; return employee.id ?? null; }