import { CanActivate, ExecutionContext, ForbiddenException, Injectable, Type, UnauthorizedException, } from '@nestjs/common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { hasFreightPermission, isSuperAdmin } from './freight-permission.util'; // String literals on purpose (same reasoning as login-audience.middleware.ts): // the values are wire-format constants from iam.users.user_type, and importing // the vendored enum couples us to its package layout for no gain. const CUSTOMER_USER_TYPES = ['individual', 'external_organization']; const userTypeOf = (user: TCurrentUser): string | undefined => (user as { userType?: string }).userType; /** Staff routes are employee-only; a missing userType (stale session) also fails. */ const isEmployee = (user: TCurrentUser): boolean => userTypeOf(user) === 'employee' || isSuperAdmin(user); export function FreightPermissionGuard( permissions: string[], ): Type { @Injectable() class FreightPermissionsGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest<{ user?: TCurrentUser }>(); const user = request.user; if (!user) { throw new UnauthorizedException('Authentication required'); } if (!isEmployee(user)) { throw new ForbiddenException('Staff account required'); } if (!permissions?.length) return true; if (permissions.some((p) => hasFreightPermission(user, p))) { return true; } throw new ForbiddenException( `Missing permission. Required one of: ${permissions.join(', ')}`, ); } } return FreightPermissionsGuard; } /** Portal routes: customer accounts only (individual / external organization). */ @Injectable() export class PortalCustomerGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest<{ user?: TCurrentUser }>(); const user = request.user; if (!user) { throw new UnauthorizedException('Authentication required'); } if (!CUSTOMER_USER_TYPES.includes(userTypeOf(user) ?? '')) { throw new ForbiddenException('Customer account required'); } return true; } } /** * Routes both audiences legitimately call (contract sign, shared document * reads, warehouse handover). Staff callers must hold one of the given * permissions; customer callers pass here and are scoped by the service's * ownership checks. */ export function MixedAudienceGuard(permissions: string[]): Type { @Injectable() class MixedAudiencesGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest<{ user?: TCurrentUser }>(); const user = request.user; if (!user) { throw new UnauthorizedException('Authentication required'); } if (CUSTOMER_USER_TYPES.includes(userTypeOf(user) ?? '')) { return true; } if (!isEmployee(user)) { throw new ForbiddenException('Unrecognized account type'); } if ( !permissions?.length || permissions.some((p) => hasFreightPermission(user, p)) ) { return true; } throw new ForbiddenException( `Missing permission. Required one of: ${permissions.join(', ')}`, ); } } return MixedAudiencesGuard; }