feat: add authorization

This commit is contained in:
2026-08-13 15:03:51 +03:00
parent ab21426478
commit 4cbfc2621c
25 changed files with 952 additions and 114 deletions
@@ -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',
});
});
});
+48 -7
View File
@@ -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,
})),
);
}
}