112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
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<T> implements NestInterceptor<T, Response> {
|
|
|
|
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<T>): Observable<any> {
|
|
|
|
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<IRequest>();
|
|
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>();
|
|
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),
|
|
}
|
|
}),
|
|
})
|
|
);
|
|
}
|
|
}
|