import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { DataSource } from 'typeorm'; import { collectPermissionKeys, isSuperAdmin, } from '../../common/freight-permission.util'; import { PERMISSIONS_CATALOG } from '../../seed/freight-permissions.registry'; @Injectable() export class FreightMeService { constructor(@InjectDataSource() private readonly dataSource: DataSource) {} /** * The JWT session snapshot has no position TYPE, but the backoffice needs it * (GL sub-positions are identified by type key). Resolved live from IAM. */ private async lookupPositionType( positionId: string | undefined, ): Promise<{ key: string; name: unknown } | null> { if (!positionId) return null; try { const rows: { key: string; name: unknown }[] = await this.dataSource.query( `SELECT pt.key, pt.name FROM iam.positions p JOIN iam.position_types pt ON pt.id = p.position_type_id WHERE p.id = $1`, [positionId], ); return rows[0] ?? null; } catch { return null; // iam schema unreachable — degrade to the old payload shape } } /** * Permissions granted to the position's TYPE (`iam.position_type_permissions`). * A position type is the platform's notion of a role, and admin-created * positions carry their grants there rather than on the position itself — but * the JWT only ever snapshots direct position permissions. Without this, staff * on such a position resolve to zero permissions and every permission-gated * route rejects them (this is what locked GL officers out of their clearance * detail pages). Resolved live from IAM, same as the position type above. */ private async lookupPositionTypePermissions( positionId: string | undefined, ): Promise { if (!positionId) return []; try { const rows: { key: string }[] = await this.dataSource.query( `SELECT DISTINCT perm.key FROM iam.positions p JOIN iam.position_type_permissions ptp ON ptp.position_type_id = p.position_type_id JOIN iam.permissions perm ON perm.id = ptp.permission_id WHERE p.id = $1`, [positionId], ); return rows.map((r) => r.key).filter(Boolean); } catch { return []; // iam schema unreachable — degrade to position-only permissions } } async getEnrichedProfile(user: TCurrentUser) { const positionId = user.employee?.position?.id; const [positionType, positionTypePermissionKeys] = await Promise.all([ this.lookupPositionType(positionId), this.lookupPositionTypePermissions(positionId), ]); // Merge the type-level grants into the position's own permission list so // BOTH consumers see them: `collectPermissionKeys` below, and the // backoffice's `getPermissionKeys`, which walks this same nested array. const positionPermissions = [ ...(user.employee?.position?.permissions ?? []), ]; const seenPermissionKeys = new Set( positionPermissions.map((p) => p?.key).filter(Boolean), ); for (const key of positionTypePermissionKeys) { if (!seenPermissionKeys.has(key)) { seenPermissionKeys.add(key); positionPermissions.push({ key } as (typeof positionPermissions)[number]); } } const employee = user.employee ? [ { id: user.employee.id, organizationId: user.employee.organizationId, unitId: user.employee.unitId, name: user.employee.name, positions: user.employee.position ? [ { id: user.employee.position.id, key: user.employee.position.key, employeePositionId: user.employee.position.employeePositionId, name: user.employee.position.name, isDelegate: user.employee.position.isDelegate, parentPositionId: user.employee.position.parentPositionId, permissions: positionPermissions, positionType, }, ] : [], }, ] : []; // `collectPermissionKeys` reads the raw token (position-level only), so // union the type-level grants in — the backoffice prefers this flat list // over the nested array and would otherwise still see none of them. const permissionKeys = [ ...new Set([ ...collectPermissionKeys(user), ...positionTypePermissionKeys, ]), ]; return { id: user.id, email: user.email, name: user.name, username: user.username, phoneNumber: user.phoneNumber, userType: user.userType, status: user.status, hasFinishedRegistration: user.hasFinishedRegistration, hasFinishedDMSOnboarding: user.hasFinishedDMSOnboarding, roles: user.roles, permissions: user.permissions, employee, permissionKeys, isSuperAdmin: isSuperAdmin(user), permissionsCatalog: PERMISSIONS_CATALOG, }; } }