feat: add authorization
This commit is contained in:
@@ -4,25 +4,22 @@ import { HttpExceptionFilter } from './http-exception.filter';
|
||||
describe('HttpExceptionFilter', () => {
|
||||
it('does not expose internal error details', () => {
|
||||
const status = jest.fn().mockReturnThis();
|
||||
const type = jest.fn().mockReturnThis();
|
||||
const json = jest.fn();
|
||||
const filter = new HttpExceptionFilter();
|
||||
const host = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
path: '/api/v1/health',
|
||||
url: '/api/v1/health?access_token=secret',
|
||||
}),
|
||||
getResponse: () => ({ status, json }),
|
||||
getResponse: () => ({ status, type, json }),
|
||||
}),
|
||||
} as unknown as ArgumentsHost;
|
||||
|
||||
filter.catch(new Error('database password: secret'), host);
|
||||
|
||||
expect(status).toHaveBeenCalledWith(500);
|
||||
expect(type).toHaveBeenCalledWith('application/problem+json');
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
statusCode: 500,
|
||||
message: 'Internal server error',
|
||||
path: '/api/v1/health',
|
||||
status: 500,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,24 +4,65 @@ import {
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
ValidationError,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import type { Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const context = host.switchToHttp();
|
||||
const response = context.getResponse<Response>();
|
||||
const request = context.getRequest<Request>();
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
message: status === 500 ? 'Internal server error' : 'Request failed',
|
||||
path: request.path,
|
||||
});
|
||||
const payload =
|
||||
exception instanceof HttpException ? exception.getResponse() : undefined;
|
||||
const isValidation = Array.isArray(
|
||||
(payload as { message?: unknown })?.message,
|
||||
);
|
||||
response
|
||||
.status(status)
|
||||
.type('application/problem+json')
|
||||
.json({
|
||||
status,
|
||||
code: isValidation ? 'VALIDATION_ERROR' : this.codeFor(status, payload),
|
||||
...(isValidation
|
||||
? {
|
||||
errors: this.validationErrors(
|
||||
(payload as { message: ValidationError[] }).message,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
private codeFor(status: number, payload: unknown): string {
|
||||
if (typeof payload === 'object' && payload !== null && 'code' in payload) {
|
||||
const code = (payload as { code?: unknown }).code;
|
||||
if (typeof code === 'string') return code;
|
||||
}
|
||||
return (
|
||||
{
|
||||
400: 'BAD_REQUEST',
|
||||
401: 'UNAUTHORIZED',
|
||||
404: 'NOT_FOUND',
|
||||
415: 'UNSUPPORTED_MEDIA_TYPE',
|
||||
500: 'INTERNAL_SERVER_ERROR',
|
||||
}[status] ?? 'INTERNAL_SERVER_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
private validationErrors(
|
||||
errors: ValidationError[],
|
||||
): { pointer: string; detail: string }[] {
|
||||
return errors.flatMap((error) =>
|
||||
Object.values(error.constraints ?? {}).map((detail) => ({
|
||||
pointer: `#/${error.property}`,
|
||||
detail,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user