import { CallHandler, ExecutionContext, HttpException, Inject, Injectable, Logger, NestInterceptor, RequestTimeoutException } from '@nestjs/common'; import { randomUUID } from 'crypto'; import { catchError, Observable, tap, throwError } from 'rxjs'; import { Request as ExpressRequest, Response } from 'express'; import * as Joi from 'joi'; import { PrismaService } from '../_prisma/prisma.service'; import { ActionHandlers } from '../app.constants'; import { Action } from '../action.enum'; import { Format } from '../abstract-action.handler'; import { AwsException, InternalFailure } from '../aws-shared-entities/aws-exceptions'; import { IRequest, RequestContext } from '../_context/request.context'; import { ConfigService } from '@nestjs/config'; @Injectable() export class AuditInterceptor implements NestInterceptor { private readonly logger = new Logger(AuditInterceptor.name); constructor( @Inject(ActionHandlers) private readonly handlers: ActionHandlers, private readonly prismaService: PrismaService, private readonly configService: ConfigService, ) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const awsProperties = { accountId: this.configService.get('AWS_ACCOUNT_ID'), region: this.configService.get('AWS_REGION'), host: `${this.configService.get('PROTO')}://${this.configService.get('HOST')}:${this.configService.get('PORT')}`, }; const requestContext: RequestContext = { requestId: randomUUID(), awsProperties, } const httpContext = context.switchToHttp(); const request = httpContext.getRequest(); request.context = requestContext; const hasTargetHeader = Object.keys(request.headers).some( k => k.toLocaleLowerCase() === 'x-amz-target'); const action = hasTargetHeader ? request.headers['x-amz-target'] : request.body.Action; const { value: resolvedAction } = Joi.string().required().valid(...Object.values(Action)).validate(action) as { value: Action | undefined }; requestContext.action = resolvedAction; const response = context.switchToHttp().getResponse(); response.header('x-amzn-RequestId', requestContext.requestId); if (!resolvedAction || !this.handlers[resolvedAction]?.audit) { return next.handle().pipe( catchError(async (error: Error) => { await this.prismaService.audit.create({ data: { id: requestContext.requestId, action, request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }), response: JSON.stringify(error), } }); this.logger.error(error.message); return error; }) ); } const handler = this.handlers[resolvedAction]; requestContext.format = handler.format; return next.handle().pipe( catchError((error: Error) => { return throwError(() => { if (error instanceof AwsException) { return error; } const defaultError = new InternalFailure('Unexpected local AWS exception...'); this.logger.error(error.message); defaultError.requestId = requestContext.requestId; return defaultError; }); }), tap({ next: async (data) => await this.prismaService.audit.create({ data: { id: requestContext.requestId, action, request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }), response: JSON.stringify(data), } }), error: async (error) => await this.prismaService.audit.create({ data: { id: requestContext.requestId, action, request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }), response: JSON.stringify(error), } }), }) ); } }