import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ConfigService } from '@nestjs/config'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; /** * IAM Adapter for @tria-plc corporate identity integration * * This adapter wraps the corporate IAM guards and provides a bridge * between the corporate identity system and the EDR passenger API. * * For back-office roles (agent, supervisor, admin, staff), this guard * validates tokens against the corporate IAM service. * * For passenger-facing routes, the existing JWT guard is used. */ export interface IamTokenPayload { sub: string; email: string; roles: string[]; permissions: string[]; organizationId?: string; exp: number; iat: number; } export interface IamValidationResponse { valid: boolean; payload?: IamTokenPayload; error?: string; } @Injectable() export class IamGuard implements CanActivate { private readonly iamApiUrl: string; private readonly iamEnabled: boolean; constructor( private readonly reflector: Reflector, private readonly config: ConfigService, private readonly http: HttpService, ) { this.iamApiUrl = this.config.get('IAM_API_URL') || 'https://iam.tria-plc.com/api'; this.iamEnabled = this.config.get('IAM_ENABLED') === 'true'; } async canActivate(context: ExecutionContext): Promise { if (!this.iamEnabled) { // IAM disabled - allow access (for development) return true; } const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (!token) { throw new UnauthorizedException('No authentication token provided'); } const validation = await this.validateToken(token); if (!validation.valid || !validation.payload) { throw new UnauthorizedException(validation.error || 'Invalid token'); } // Check required roles const requiredRoles = this.reflector.get('roles', context.getHandler()); if (requiredRoles && requiredRoles.length > 0) { const hasRole = requiredRoles.some((role) => validation.payload!.roles.includes(role)); if (!hasRole) { throw new ForbiddenException('Insufficient permissions'); } } // Attach user to request request.user = { userId: validation.payload.sub, email: validation.payload.email, roles: validation.payload.roles, permissions: validation.payload.permissions, organizationId: validation.payload.organizationId, }; return true; } private extractToken(request: any): string | null { const authHeader = request.headers.authorization; if (!authHeader) return null; const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') return null; return parts[1]; } private async validateToken(token: string): Promise { try { const response = await firstValueFrom( this.http.post( `${this.iamApiUrl}/v1/auth/validate`, { token }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.config.get('IAM_API_KEY') || '', }, timeout: 5000, }, ), ); return response.data; } catch (err) { return { valid: false, error: err instanceof Error ? err.message : 'Token validation failed', }; } } } /** * Decorator to mark routes as requiring IAM authentication */ export const UseIamAuth = () => { // This is a marker decorator that can be used with @UseGuards(IamGuard) return (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => { // Marker only - actual guard is applied via @UseGuards }; }; /** * Decorator to specify required roles for IAM-protected routes */ export const IamRoles = (...roles: string[]) => { return (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => { if (descriptor) { Reflect.defineMetadata('roles', roles, descriptor.value); } }; };