From 96a97dfb95bb798c1552f9a85390c53ea1d151ae Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 20:40:01 +0300 Subject: [PATCH 01/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=82=D1=80=D0=B0=D0=BD=D1=81=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/helpers/common.ts | 10 ++++++++++ src/shared/libs/rest/index.ts | 1 + src/shared/libs/rest/validation-error-field.type.ts | 5 +++++ 3 files changed, 16 insertions(+) create mode 100644 src/shared/libs/rest/validation-error-field.type.ts diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index f949c61..64f0d57 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -1,4 +1,6 @@ import { ClassConstructor, plainToInstance } from 'class-transformer'; +import { ValidationError } from 'class-validator'; +import { ValidationErrorField } from '../libs/rest/index.js'; export function getRandomNumber(a: number, b: number, rank: number = 0) { return +(Math.random() * (b - a) + a).toFixed(rank); @@ -30,3 +32,11 @@ export function createErrorObject(message: string) { error: message, }; } + +export function reduceValidationErrors(errors: ValidationError[]): ValidationErrorField[] { + return errors.map(({ property, value, constraints }) => ({ + property, + value, + messages: constraints ? Object.values(constraints) : [] + })); +} diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index 930aa80..d6cd1b5 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -11,3 +11,4 @@ export { DocumentExistsMiddleware } from './middleware/document-exists.middlewar export { UploadFileMiddleware } from './middleware/upload-file.middleware.js'; export { ParseTokenMiddleware } from './middleware/parse-token.middleware.js'; export { PrivateRouteMiddleware } from './middleware/private-route.middleware.js'; +export { ValidationErrorField } from './validation-error-field.type.js'; diff --git a/src/shared/libs/rest/validation-error-field.type.ts b/src/shared/libs/rest/validation-error-field.type.ts new file mode 100644 index 0000000..155246d --- /dev/null +++ b/src/shared/libs/rest/validation-error-field.type.ts @@ -0,0 +1,5 @@ +export type ValidationErrorField = { + property: string; + value: string; + messages: string[]; +}; From 50ffe6777689fd68610f7701616d456058cecb6d Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 20:45:27 +0300 Subject: [PATCH 02/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20ValidationError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../middleware/validate-dto.middleware.ts | 8 +-- src/shared/libs/rest/validation-error.ts | 12 ++++ src/shared/modules/user/user.controller.ts | 66 +++++++++---------- 3 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 src/shared/libs/rest/validation-error.ts diff --git a/src/shared/libs/rest/middleware/validate-dto.middleware.ts b/src/shared/libs/rest/middleware/validate-dto.middleware.ts index acbf815..df8c22e 100644 --- a/src/shared/libs/rest/middleware/validate-dto.middleware.ts +++ b/src/shared/libs/rest/middleware/validate-dto.middleware.ts @@ -2,18 +2,18 @@ import { Request, Response, NextFunction } from 'express'; import { ClassConstructor, plainToInstance } from 'class-transformer'; import { Middleware } from './middleware.interface.js'; import { validate } from 'class-validator'; -import { StatusCodes } from 'http-status-codes'; +import { reduceValidationErrors } from '../../../helpers/common.js'; +import { ValidationError } from '../validation-error.js'; export class ValidateDtoMiddleware implements Middleware { constructor(private dto: ClassConstructor) { } - public async execute({ body }: Request, res: Response, next: NextFunction): Promise { + public async execute({ body, path }: Request, _res: Response, next: NextFunction): Promise { const dtoInstance = plainToInstance(this.dto, body); const errors = await validate(dtoInstance); if (errors.length > 0) { - res.status(StatusCodes.BAD_REQUEST).send(errors); - return; + throw new ValidationError(`Validation error: ${path}`, reduceValidationErrors(errors)); } next(); diff --git a/src/shared/libs/rest/validation-error.ts b/src/shared/libs/rest/validation-error.ts new file mode 100644 index 0000000..53ec9c4 --- /dev/null +++ b/src/shared/libs/rest/validation-error.ts @@ -0,0 +1,12 @@ +import { StatusCodes } from 'http-status-codes'; +import { HttpError } from './http-error.js'; +import { ValidationErrorField } from './validation-error-field.type.js'; + +export class ValidationError extends HttpError { + public details: ValidationErrorField[] = []; + + constructor(message: string, errors: ValidationErrorField[]) { + super(StatusCodes.BAD_REQUEST, message); + this.details = errors; + } +} diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 70fd510..4f9451a 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -9,11 +9,11 @@ import { fillDTO } from '../../helpers/common.js'; import { UserRdo } from './rdo/user.rdo.js'; import { LoginUserRequest } from './types/login-user-request.type.js'; import { CreateUserRequest } from './types/create-user-request.type.js'; -import { BaseController, HttpError, PrivateRouteMiddleware, UploadFileMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; +import { BaseController, HttpError, UploadFileMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; import { LoginUserDto } from './dto/login-user.dto.js'; import { AuthService } from '../auth/index.js'; import { LoggedUserRdo } from './rdo/logged-user.rdo.js'; -import { OfferService } from '../offer/index.js'; +// import { OfferService } from '../offer/index.js'; @injectable() export class UserController extends BaseController { @@ -22,7 +22,7 @@ export class UserController extends BaseController { @inject(Component.UserService) private readonly userService: UserService, @inject(Component.Config) private readonly configService: Config, @inject(Component.AuthService) private readonly authService: AuthService, - @inject(Component.OfferService) private readonly offerService: OfferService, + // @inject(Component.OfferService) private readonly offerService: OfferService, ) { super(logger); this.logger.info('Register routes for UserController'); @@ -53,30 +53,30 @@ export class UserController extends BaseController { new UploadFileMiddleware(this.configService.get('UPLOAD_DIRECTORY'), 'avatar'), ] }); - this.addRoute({ - path: '/favorites', - method: 'get', - handler: this.showFavorite, - middlewares: [ - new PrivateRouteMiddleware(), - ] - }); - this.addRoute({ - path: '/favorites', - method: 'post', - handler: this.addFavorite, - middlewares: [ - new PrivateRouteMiddleware(), - ] - }); - this.addRoute({ - path: '/favorites', - method: 'delete', - handler: this.deleteFavorite, - middlewares: [ - new PrivateRouteMiddleware(), - ] - }); + // this.addRoute({ + // path: '/favorites', + // method: 'get', + // handler: this.showFavorite, + // middlewares: [ + // new PrivateRouteMiddleware(), + // ] + // }); + // this.addRoute({ + // path: '/favorites', + // method: 'post', + // handler: this.addFavorite, + // middlewares: [ + // new PrivateRouteMiddleware(), + // ] + // }); + // this.addRoute({ + // path: '/favorites', + // method: 'delete', + // handler: this.deleteFavorite, + // middlewares: [ + // new PrivateRouteMiddleware(), + // ] + // }); } public async create( @@ -121,15 +121,15 @@ export class UserController extends BaseController { this.ok(res, fillDTO(LoggedUserRdo, foundedUser)); } - public async showFavorite({ tokenPayload: { id } }: Request, _res: Response) { + // public async showFavorite({ tokenPayload: { id } }: Request, _res: Response) { - } + // } - public async addFavorite({ body, tokenPayload: { id } }: Request, _res: Response) { + // public async addFavorite({ body, tokenPayload: { id } }: Request, _res: Response) { - } + // } - public async deleteFavorite({ body, tokenPayload: { id } }: Request, _res: Response) { + // public async deleteFavorite({ body, tokenPayload: { id } }: Request, _res: Response) { - } + // } } From 1ecc7c1fe4703f00fa1ff74893ec6be219af9132 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 20:50:55 +0300 Subject: [PATCH 03/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20ValidationExceptionFilter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/helpers/common.ts | 8 ++--- ...tion-filter.ts => app.exception-filter.ts} | 0 .../validation.exception-filter.ts | 34 +++++++++++++++++++ src/shared/libs/rest/index.ts | 5 +-- .../libs/rest/types/application-error.enum.ts | 5 +++ .../libs/rest/{ => types}/request.type.ts | 0 .../validation-error-field.type.ts | 0 src/shared/libs/rest/validation-error.ts | 2 +- .../types/create-comment-request.type.ts | 2 +- .../offer/types/create-offer-request.type.ts | 2 +- .../user/types/create-user-request.type.ts | 2 +- .../user/types/login-user-request.type.ts | 2 +- 12 files changed, 50 insertions(+), 12 deletions(-) rename src/shared/libs/rest/exception-filter/{app-exception-filter.ts => app.exception-filter.ts} (100%) create mode 100644 src/shared/libs/rest/exception-filter/validation.exception-filter.ts create mode 100644 src/shared/libs/rest/types/application-error.enum.ts rename src/shared/libs/rest/{ => types}/request.type.ts (100%) rename src/shared/libs/rest/{ => types}/validation-error-field.type.ts (100%) diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index 64f0d57..4867759 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -1,6 +1,6 @@ import { ClassConstructor, plainToInstance } from 'class-transformer'; import { ValidationError } from 'class-validator'; -import { ValidationErrorField } from '../libs/rest/index.js'; +import { ApplicationError, ValidationErrorField } from '../libs/rest/index.js'; export function getRandomNumber(a: number, b: number, rank: number = 0) { return +(Math.random() * (b - a) + a).toFixed(rank); @@ -27,10 +27,8 @@ export function fillDTO(someDto: ClassConstructor, plainObject: V) { return plainToInstance(someDto, plainObject, { excludeExtraneousValues: true }); } -export function createErrorObject(message: string) { - return { - error: message, - }; +export function createErrorObject(errorType: ApplicationError, error: string, details: ValidationErrorField[] = []) { + return { errorType, error, details }; } export function reduceValidationErrors(errors: ValidationError[]): ValidationErrorField[] { diff --git a/src/shared/libs/rest/exception-filter/app-exception-filter.ts b/src/shared/libs/rest/exception-filter/app.exception-filter.ts similarity index 100% rename from src/shared/libs/rest/exception-filter/app-exception-filter.ts rename to src/shared/libs/rest/exception-filter/app.exception-filter.ts diff --git a/src/shared/libs/rest/exception-filter/validation.exception-filter.ts b/src/shared/libs/rest/exception-filter/validation.exception-filter.ts new file mode 100644 index 0000000..6adf16c --- /dev/null +++ b/src/shared/libs/rest/exception-filter/validation.exception-filter.ts @@ -0,0 +1,34 @@ +import { Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { injectable, inject } from 'inversify'; +import { Component } from '../../../const.js'; +import { createErrorObject } from '../../../helpers/common.js'; +import { ApplicationError } from '../types/application-error.enum.js'; +import { ExceptionFilter } from './exception-filter.interface.js'; +import { Logger } from '../../logger/logger.interface.js'; +import { ValidationError } from '../validation-error.js'; + +@injectable() +export class ValidationExceptionFilter implements ExceptionFilter { + constructor( + @inject(Component.Logger) private readonly logger: Logger + ) { + this.logger.info('Register ValidationExceptionFilter'); + } + + public catch(error: unknown, _req: Request, res: Response, next: NextFunction): void { + if (!(error instanceof ValidationError)) { + return next(error); + } + + this.logger.error(`[ValidationException]: ${error.message}`, error); + + error.details.forEach( + (errorField) => this.logger.warn(`[${errorField.property}] — ${errorField.messages}`) + ); + + res + .status(StatusCodes.BAD_REQUEST) + .json(createErrorObject(ApplicationError.ValidationError, error.message, error.details)); + } +} diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index d6cd1b5..5f95038 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -2,7 +2,7 @@ export { ValidateObjectIdMiddleware } from './middleware/validate-objectid.middl export { ValidateDtoMiddleware } from './middleware/validate-dto.middleware.js'; export { Middleware } from './middleware/middleware.interface.js'; export { ExceptionFilter } from './exception-filter/exception-filter.interface.js'; -export { AppExceptionFilter } from './exception-filter/app-exception-filter.js'; +export { AppExceptionFilter } from './exception-filter/app.exception-filter.js'; export { Controller } from './controller/controller.interface.js'; export { BaseController } from './controller/base-controller.abstract.js'; export { Route } from './route.interface.js'; @@ -11,4 +11,5 @@ export { DocumentExistsMiddleware } from './middleware/document-exists.middlewar export { UploadFileMiddleware } from './middleware/upload-file.middleware.js'; export { ParseTokenMiddleware } from './middleware/parse-token.middleware.js'; export { PrivateRouteMiddleware } from './middleware/private-route.middleware.js'; -export { ValidationErrorField } from './validation-error-field.type.js'; +export { ValidationErrorField } from './types/validation-error-field.type.js'; +export { ApplicationError } from './types/application-error.enum.js'; diff --git a/src/shared/libs/rest/types/application-error.enum.ts b/src/shared/libs/rest/types/application-error.enum.ts new file mode 100644 index 0000000..1a2e3ee --- /dev/null +++ b/src/shared/libs/rest/types/application-error.enum.ts @@ -0,0 +1,5 @@ +export enum ApplicationError { + ValidationError = 'VALIDATION_ERROR', + CommonError = 'COMMON_ERROR', + ServiceError = 'SERVICE_ERROR', +} diff --git a/src/shared/libs/rest/request.type.ts b/src/shared/libs/rest/types/request.type.ts similarity index 100% rename from src/shared/libs/rest/request.type.ts rename to src/shared/libs/rest/types/request.type.ts diff --git a/src/shared/libs/rest/validation-error-field.type.ts b/src/shared/libs/rest/types/validation-error-field.type.ts similarity index 100% rename from src/shared/libs/rest/validation-error-field.type.ts rename to src/shared/libs/rest/types/validation-error-field.type.ts diff --git a/src/shared/libs/rest/validation-error.ts b/src/shared/libs/rest/validation-error.ts index 53ec9c4..c5e2bb3 100644 --- a/src/shared/libs/rest/validation-error.ts +++ b/src/shared/libs/rest/validation-error.ts @@ -1,6 +1,6 @@ import { StatusCodes } from 'http-status-codes'; import { HttpError } from './http-error.js'; -import { ValidationErrorField } from './validation-error-field.type.js'; +import { ValidationErrorField } from './types/validation-error-field.type.js'; export class ValidationError extends HttpError { public details: ValidationErrorField[] = []; diff --git a/src/shared/modules/comment/types/create-comment-request.type.ts b/src/shared/modules/comment/types/create-comment-request.type.ts index 94b4d26..b3cf25f 100644 --- a/src/shared/modules/comment/types/create-comment-request.type.ts +++ b/src/shared/modules/comment/types/create-comment-request.type.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { RequestBody, RequestParams } from '../../../libs/rest/request.type.js'; +import { RequestBody, RequestParams } from '../../../libs/rest/types/request.type.js'; import { CreateCommentDto } from '../dto/create-comment.dto.js'; export type CreateCommentRequest = Request; diff --git a/src/shared/modules/offer/types/create-offer-request.type.ts b/src/shared/modules/offer/types/create-offer-request.type.ts index 73a068f..d811bcf 100644 --- a/src/shared/modules/offer/types/create-offer-request.type.ts +++ b/src/shared/modules/offer/types/create-offer-request.type.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { RequestBody, RequestParams } from '../../../libs/rest/request.type.js'; +import { RequestBody, RequestParams } from '../../../libs/rest/types/request.type.js'; import { CreateOfferDto } from '../dto/create-offer.dto.js'; export type CreateOfferRequest = Request; diff --git a/src/shared/modules/user/types/create-user-request.type.ts b/src/shared/modules/user/types/create-user-request.type.ts index 6c5bc5a..d4ccdcf 100644 --- a/src/shared/modules/user/types/create-user-request.type.ts +++ b/src/shared/modules/user/types/create-user-request.type.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { RequestBody, RequestParams } from '../../../libs/rest/request.type.js'; +import { RequestBody, RequestParams } from '../../../libs/rest/types/request.type.js'; import { CreateUserDto } from '../dto/create-user.dto.js'; export type CreateUserRequest = Request; diff --git a/src/shared/modules/user/types/login-user-request.type.ts b/src/shared/modules/user/types/login-user-request.type.ts index 8a39677..e8b74bd 100644 --- a/src/shared/modules/user/types/login-user-request.type.ts +++ b/src/shared/modules/user/types/login-user-request.type.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { RequestBody, RequestParams } from '../../../libs/rest/request.type.js'; +import { RequestBody, RequestParams } from '../../../libs/rest/types/request.type.js'; import { LoginUserDto } from '../dto/login-user.dto.js'; export type LoginUserRequest = Request; From db9042eda5d81e3ad52a9143ae4a93f01d4e9fa0 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 20:57:09 +0300 Subject: [PATCH 04/14] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3.=20=D0=A0=D0=B0=D0=B7=D0=B1?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=20app.exception-filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/rest.application.ts | 4 +++ src/rest/rest.container.ts | 4 ++- src/shared/const.ts | 2 ++ .../exception-filter/app.exception-filter.ts | 27 ++++------------- .../http-error.exception-filter.ts | 30 +++++++++++++++++++ src/shared/libs/rest/index.ts | 2 ++ 6 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 src/shared/libs/rest/exception-filter/http-error.exception-filter.ts diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index da4a9d9..ce44d76 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -23,6 +23,8 @@ export class RESTApplication { @inject(Component.OfferController) private readonly offerController: Controller, @inject(Component.CommentController) private readonly commentController: Controller, @inject(Component.AuthExceptionFilter) private readonly authExceptionFilter: ExceptionFilter, + @inject(Component.HttpExceptionFilter) private readonly httpExceptionFilter: ExceptionFilter, + @inject(Component.ValidationExceptionFilter) private readonly validationExceptionFilter: ExceptionFilter, ) { this.server = express(); } @@ -59,6 +61,8 @@ export class RESTApplication { private async initExceptionFilters() { this.server.use(this.authExceptionFilter.catch.bind(this.authExceptionFilter)); + this.server.use(this.validationExceptionFilter.catch.bind(this.validationExceptionFilter)); + this.server.use(this.httpExceptionFilter.catch.bind(this.httpExceptionFilter)); this.server.use(this.appExceptionFilter.catch.bind(this.appExceptionFilter)); } diff --git a/src/rest/rest.container.ts b/src/rest/rest.container.ts index 2a12cab..a9a7014 100644 --- a/src/rest/rest.container.ts +++ b/src/rest/rest.container.ts @@ -6,7 +6,7 @@ import { Config, RestConfig, RestSchema } from '../shared/libs/config/index.js'; import { DatabaseClient } from '../shared/libs/database-client/database-client.interface.js'; import { MongoDatabaseClient } from '../shared/libs/database-client/mongo.database-client.js'; import { RESTApplication } from './rest.application.js'; -import { AppExceptionFilter, ExceptionFilter } from '../shared/libs/rest/index.js'; +import { AppExceptionFilter, ExceptionFilter, HttpErrorExceptionFilter, ValidationExceptionFilter } from '../shared/libs/rest/index.js'; export function createRestApplicationContainer() { const restApplicationContainer = new Container(); @@ -16,6 +16,8 @@ export function createRestApplicationContainer() { restApplicationContainer.bind>(Component.Config).to(RestConfig).inSingletonScope(); restApplicationContainer.bind(Component.DatabaseClient).to(MongoDatabaseClient).inSingletonScope(); restApplicationContainer.bind(Component.ExceptionFilter).to(AppExceptionFilter).inSingletonScope(); + restApplicationContainer.bind(Component.HttpExceptionFilter).to(HttpErrorExceptionFilter).inSingletonScope(); + restApplicationContainer.bind(Component.ValidationExceptionFilter).to(ValidationExceptionFilter).inSingletonScope(); return restApplicationContainer; } diff --git a/src/shared/const.ts b/src/shared/const.ts index 25fc8ed..90b04e0 100644 --- a/src/shared/const.ts +++ b/src/shared/const.ts @@ -64,6 +64,8 @@ export const Component = { CommentController: Symbol.for('CommentController'), AuthService: Symbol.for('AuthService'), AuthExceptionFilter: Symbol.for('AuthExceptionFilter'), + HttpExceptionFilter: Symbol.for('HttpExceptionFilter'), + ValidationExceptionFilter: Symbol.for('ValidationExceptionFilter'), } as const; export enum SortType { diff --git a/src/shared/libs/rest/exception-filter/app.exception-filter.ts b/src/shared/libs/rest/exception-filter/app.exception-filter.ts index c15e734..65af91b 100644 --- a/src/shared/libs/rest/exception-filter/app.exception-filter.ts +++ b/src/shared/libs/rest/exception-filter/app.exception-filter.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from 'express'; -import { ExceptionFilter } from './exception-filter.interface.js'; import { StatusCodes } from 'http-status-codes'; -import { Logger } from '../../logger/logger.interface.js'; +import { injectable, inject } from 'inversify'; import { Component } from '../../../const.js'; -import { inject, injectable } from 'inversify'; import { createErrorObject } from '../../../helpers/common.js'; -import { HttpError } from '../http-error.js'; +import { ApplicationError } from '../types/application-error.enum.js'; +import { ExceptionFilter } from './exception-filter.interface.js'; +import { Logger } from '../../logger/logger.interface.js'; @injectable() export class AppExceptionFilter implements ExceptionFilter { @@ -15,25 +15,10 @@ export class AppExceptionFilter implements ExceptionFilter { this.logger.info('Register AppExceptionFilter'); } - private handleHttpError(error: HttpError, _req: Request, res: Response, _next: NextFunction) { - this.logger.error(`[${error.detail}]: ${error.httpStatusCode} — ${error.message}`, error); - res - .status(error.httpStatusCode) - .json(createErrorObject(error.message)); - } - - private handleOtherError(error: Error, _req: Request, res: Response, _next: NextFunction) { + public catch(error: Error, _req: Request, res: Response, _next: NextFunction): void { this.logger.error(error.message, error); res .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json(createErrorObject(error.message)); - } - - public catch(error: Error | HttpError, req: Request, res: Response, next: NextFunction): void { - if (error instanceof HttpError) { - return this.handleHttpError(error, req, res, next); - } - - this.handleOtherError(error, req, res, next); + .json(createErrorObject(ApplicationError.ServiceError, error.message)); } } diff --git a/src/shared/libs/rest/exception-filter/http-error.exception-filter.ts b/src/shared/libs/rest/exception-filter/http-error.exception-filter.ts new file mode 100644 index 0000000..06d3520 --- /dev/null +++ b/src/shared/libs/rest/exception-filter/http-error.exception-filter.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { injectable, inject } from 'inversify'; +import { Component } from '../../../const.js'; +import { createErrorObject } from '../../../helpers/common.js'; +import { Logger } from '../../logger/logger.interface.js'; +import { HttpError } from '../http-error.js'; +import { ApplicationError } from '../types/application-error.enum.js'; +import { ExceptionFilter } from './exception-filter.interface.js'; + +@injectable() +export class HttpErrorExceptionFilter implements ExceptionFilter { + constructor( + @inject(Component.Logger) private readonly logger: Logger + ) { + this.logger.info('Register HttpErrorExceptionFilter'); + } + + public catch(error: unknown, req: Request, res: Response, next: NextFunction): void { + if (!(error instanceof HttpError)) { + return next(error); + } + + this.logger.error(`[HttpErrorException]: ${req.path} # ${error.message}`, error); + + res + .status(StatusCodes.BAD_REQUEST) + .json(createErrorObject(ApplicationError.CommonError, error.message)); + } +} diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index 5f95038..abde708 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -13,3 +13,5 @@ export { ParseTokenMiddleware } from './middleware/parse-token.middleware.js'; export { PrivateRouteMiddleware } from './middleware/private-route.middleware.js'; export { ValidationErrorField } from './types/validation-error-field.type.js'; export { ApplicationError } from './types/application-error.enum.js'; +export { ValidationExceptionFilter } from './exception-filter/validation.exception-filter.js'; +export { HttpErrorExceptionFilter } from './exception-filter/http-error.exception-filter.js'; From b681428ba60b225e491c15dca992f0cb63e3786e Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:03:43 +0300 Subject: [PATCH 05/14] =?UTF-8?q?=D0=A3=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8F=20avatar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/modules/user/default-user.service.ts | 3 ++- src/shared/modules/user/index.ts | 1 + src/shared/modules/user/user.constant.ts | 1 + src/shared/modules/user/user.entity.ts | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/shared/modules/user/user.constant.ts diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index 5899986..94ee358 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -6,6 +6,7 @@ import { inject, injectable } from 'inversify'; import { Component } from '../../const.js'; import { Logger } from '../../libs/logger/logger.interface.js'; import { UpdateUserDto } from './dto/update-user.dto.js'; +import { DEFAULT_AVATAR_FILE_NAME } from './user.constant.js'; @injectable() export class DefaultUserService implements UserService { @@ -15,7 +16,7 @@ export class DefaultUserService implements UserService { ) { } public async create(dto: CreateUserDto, salt: string): Promise> { - const user = new UserEntity(dto); + const user = new UserEntity({ ...dto, avatar: DEFAULT_AVATAR_FILE_NAME }); user.setPassword(dto.password, salt); const result = await this.userModel.create(user); diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index cffbcf9..9fb6ec4 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -4,3 +4,4 @@ export { CreateUserDto } from './dto/create-user.dto.js'; export { UserService } from './user-service.interface.js'; export { DefaultUserService } from './default-user.service.js'; export { createUserContainer } from './user.container.js'; +export { DEFAULT_AVATAR_FILE_NAME } from './user.constant.js'; diff --git a/src/shared/modules/user/user.constant.ts b/src/shared/modules/user/user.constant.ts new file mode 100644 index 0000000..29633af --- /dev/null +++ b/src/shared/modules/user/user.constant.ts @@ -0,0 +1 @@ +export const DEFAULT_AVATAR_FILE_NAME = 'default-avatar.jpg'; diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index 74a4a38..ac5d37d 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -1,6 +1,7 @@ import { defaultClasses, getModelForClass, modelOptions, prop } from '@typegoose/typegoose'; import { User } from '../../types/index.js'; import { createSHA256 } from '../../helpers/hash.js'; +import { DEFAULT_AVATAR_FILE_NAME } from './user.constant.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface UserEntity extends defaultClasses.Base { } @@ -20,7 +21,7 @@ export class UserEntity extends defaultClasses.TimeStamps { @prop({ unique: true, required: true }) public email: string; - @prop({ required: false, default: 'basic-avatar.jpg' }) + @prop({ required: false, default: DEFAULT_AVATAR_FILE_NAME }) public avatar?: string; @prop({ required: true }) From b3c61dcf0bdc4e5b68a71cee0978141eac68f010 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:09:23 +0300 Subject: [PATCH 06/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=83=D1=8E=20HOST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/rest.application.ts | 3 ++- src/shared/helpers/common.ts | 4 ++++ src/shared/libs/config/rest.schema.ts | 7 +++++++ src/shared/modules/offer/offer.http | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index ce44d76..26f29b0 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -9,6 +9,7 @@ import express, { Express } from 'express'; import { ExceptionFilter } from '../shared/libs/rest/exception-filter/exception-filter.interface.js'; import { Controller } from '../shared/libs/rest/controller/controller.interface.js'; import { ParseTokenMiddleware } from '../shared/libs/rest/middleware/parse-token.middleware.js'; +import { getFullServerPath } from '../shared/helpers/common.js'; @injectable() export class RESTApplication { @@ -87,6 +88,6 @@ export class RESTApplication { this.logger.info('Try to init server...'); await this.initServer(); - this.logger.info(`🚀 Server started on http://localhost:${this.config.get('PORT')}`); + this.logger.info(`🚀 Server started on ${getFullServerPath(this.config.get('HOST'), this.config.get('PORT'))}`); } } diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index 4867759..2782ad4 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -38,3 +38,7 @@ export function reduceValidationErrors(errors: ValidationError[]): ValidationErr messages: constraints ? Object.values(constraints) : [] })); } + +export function getFullServerPath(host: string, port: number) { + return `http://${host}:${port}`; +} diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 179284b..d349197 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -13,6 +13,7 @@ export type RestSchema = { DB_NAME: string; UPLOAD_DIRECTORY: string; JWT_SECRET: string; + HOST: string; } export const configRestSchema = convict({ @@ -70,4 +71,10 @@ export const configRestSchema = convict({ env: 'JWT_SECRET', default: null }, + HOST: { + doc: 'Host where started service', + format: String, + env: 'HOST', + default: 'localhost' + }, }); diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http index facc04f..136aa3e 100644 --- a/src/shared/modules/offer/offer.http +++ b/src/shared/modules/offer/offer.http @@ -13,7 +13,7 @@ GET http://localhost:5000/offers HTTP/1.1 POST http://localhost:5000/offers HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJpZCI6IjY3Mjg5OTY1ZTIyNzVmNDkwMWFiZmIzMiIsImlhdCI6MTczMDcxODIzMCwiZXhwIjoxNzMwODkxMDMwfQ.xQysRewYUOEhCUWWM8Og2KCnn4gy1eZR4hXjGAixir8 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJpZCI6IjY3MjhlNmZmYWM5MzJiOWJlOTU1ZTAxOCIsImlhdCI6MTczMDc0MzcyMywiZXhwIjoxNzMwOTE2NTIzfQ.ha6f0ddup9k_F6km-laFAn36AwGvRpbV4b9DmoMfb7g { "title": "Proposal some", From 0d9a0f5b658230ccc9a79d7deefa87cfe9111df4 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:14:05 +0300 Subject: [PATCH 07/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=B4=D0=B8=D1=80=D0=B5=D0=BA=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B4=D0=BB=D1=8F=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/rest.application.ts | 1 + src/shared/libs/config/rest.schema.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 26f29b0..25b1b09 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -57,6 +57,7 @@ export class RESTApplication { const authenticateMiddleware = new ParseTokenMiddleware(this.config.get('JWT_SECRET')); this.server.use(express.json()); this.server.use('/upload', express.static(this.config.get('UPLOAD_DIRECTORY'))); + this.server.use('/static', express.static(this.config.get('STATIC_DIRECTORY_PATH'))); this.server.use(authenticateMiddleware.execute.bind(authenticateMiddleware)); } diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index d349197..5756193 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -14,6 +14,7 @@ export type RestSchema = { UPLOAD_DIRECTORY: string; JWT_SECRET: string; HOST: string; + STATIC_DIRECTORY_PATH: string; } export const configRestSchema = convict({ @@ -77,4 +78,10 @@ export const configRestSchema = convict({ env: 'HOST', default: 'localhost' }, + STATIC_DIRECTORY_PATH: { + doc: 'Path to directory with static resources', + format: String, + env: 'STATIC_DIRECTORY_PATH', + default: 'static' + }, }); From cfe6053a4d507d046563fa44206c369ec1360fef Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:15:49 +0300 Subject: [PATCH 08/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20/static,=20/upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/rest.application.ts | 5 +++-- src/rest/rest.constant.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 src/rest/rest.constant.ts diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 25b1b09..948025e 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -10,6 +10,7 @@ import { ExceptionFilter } from '../shared/libs/rest/exception-filter/exception- import { Controller } from '../shared/libs/rest/controller/controller.interface.js'; import { ParseTokenMiddleware } from '../shared/libs/rest/middleware/parse-token.middleware.js'; import { getFullServerPath } from '../shared/helpers/common.js'; +import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from './rest.constant.js'; @injectable() export class RESTApplication { @@ -56,8 +57,8 @@ export class RESTApplication { private async initMiddleware() { const authenticateMiddleware = new ParseTokenMiddleware(this.config.get('JWT_SECRET')); this.server.use(express.json()); - this.server.use('/upload', express.static(this.config.get('UPLOAD_DIRECTORY'))); - this.server.use('/static', express.static(this.config.get('STATIC_DIRECTORY_PATH'))); + this.server.use(STATIC_UPLOAD_ROUTE, express.static(this.config.get('UPLOAD_DIRECTORY'))); + this.server.use(STATIC_FILES_ROUTE, express.static(this.config.get('STATIC_DIRECTORY_PATH'))); this.server.use(authenticateMiddleware.execute.bind(authenticateMiddleware)); } diff --git a/src/rest/rest.constant.ts b/src/rest/rest.constant.ts new file mode 100644 index 0000000..77ff89d --- /dev/null +++ b/src/rest/rest.constant.ts @@ -0,0 +1,2 @@ +export const STATIC_UPLOAD_ROUTE = '/upload'; +export const STATIC_FILES_ROUTE = '/static'; From 9c6d80b6c3832ac079e6a1750504d5f533587887 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:19:44 +0300 Subject: [PATCH 09/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20PathTransformer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/rest.container.ts | 3 +- src/shared/const.ts | 1 + src/shared/libs/rest/index.ts | 1 + .../transform/path-transformer.constant.ts | 8 +++ .../libs/rest/transform/path-transformer.ts | 60 +++++++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/shared/libs/rest/transform/path-transformer.constant.ts create mode 100644 src/shared/libs/rest/transform/path-transformer.ts diff --git a/src/rest/rest.container.ts b/src/rest/rest.container.ts index a9a7014..b19cbb6 100644 --- a/src/rest/rest.container.ts +++ b/src/rest/rest.container.ts @@ -6,7 +6,7 @@ import { Config, RestConfig, RestSchema } from '../shared/libs/config/index.js'; import { DatabaseClient } from '../shared/libs/database-client/database-client.interface.js'; import { MongoDatabaseClient } from '../shared/libs/database-client/mongo.database-client.js'; import { RESTApplication } from './rest.application.js'; -import { AppExceptionFilter, ExceptionFilter, HttpErrorExceptionFilter, ValidationExceptionFilter } from '../shared/libs/rest/index.js'; +import { AppExceptionFilter, ExceptionFilter, HttpErrorExceptionFilter, PathTransformer, ValidationExceptionFilter } from '../shared/libs/rest/index.js'; export function createRestApplicationContainer() { const restApplicationContainer = new Container(); @@ -18,6 +18,7 @@ export function createRestApplicationContainer() { restApplicationContainer.bind(Component.ExceptionFilter).to(AppExceptionFilter).inSingletonScope(); restApplicationContainer.bind(Component.HttpExceptionFilter).to(HttpErrorExceptionFilter).inSingletonScope(); restApplicationContainer.bind(Component.ValidationExceptionFilter).to(ValidationExceptionFilter).inSingletonScope(); + restApplicationContainer.bind(Component.PathTransformer).to(PathTransformer).inSingletonScope(); return restApplicationContainer; } diff --git a/src/shared/const.ts b/src/shared/const.ts index 90b04e0..4e680ee 100644 --- a/src/shared/const.ts +++ b/src/shared/const.ts @@ -66,6 +66,7 @@ export const Component = { AuthExceptionFilter: Symbol.for('AuthExceptionFilter'), HttpExceptionFilter: Symbol.for('HttpExceptionFilter'), ValidationExceptionFilter: Symbol.for('ValidationExceptionFilter'), + PathTransformer: Symbol.for('PathTransformer'), } as const; export enum SortType { diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index abde708..190b5fe 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -15,3 +15,4 @@ export { ValidationErrorField } from './types/validation-error-field.type.js'; export { ApplicationError } from './types/application-error.enum.js'; export { ValidationExceptionFilter } from './exception-filter/validation.exception-filter.js'; export { HttpErrorExceptionFilter } from './exception-filter/http-error.exception-filter.js'; +export { PathTransformer } from './transform/path-transformer.js'; diff --git a/src/shared/libs/rest/transform/path-transformer.constant.ts b/src/shared/libs/rest/transform/path-transformer.constant.ts new file mode 100644 index 0000000..9e24908 --- /dev/null +++ b/src/shared/libs/rest/transform/path-transformer.constant.ts @@ -0,0 +1,8 @@ +export const DEFAULT_STATIC_IMAGES = [ + 'default-avatar.jpg', +]; + +export const STATIC_RESOURCE_FIELDS = [ + 'avatar', + 'preview' +]; diff --git a/src/shared/libs/rest/transform/path-transformer.ts b/src/shared/libs/rest/transform/path-transformer.ts new file mode 100644 index 0000000..2514c62 --- /dev/null +++ b/src/shared/libs/rest/transform/path-transformer.ts @@ -0,0 +1,60 @@ +import { Config } from 'convict'; +import { injectable, inject } from 'inversify'; +import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from '../../../../rest/rest.constant.js'; +import { Component } from '../../../const.js'; +import { getFullServerPath } from '../../../helpers/common.js'; +import { RestSchema } from '../../config/rest.schema.js'; +import { Logger } from '../../logger/logger.interface.js'; +import { DEFAULT_STATIC_IMAGES, STATIC_RESOURCE_FIELDS } from './path-transformer.constant.js'; + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +@injectable() +export class PathTransformer { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.Config) private readonly config: Config, + ) { + this.logger.info('PathTranformer created!'); + } + + private hasDefaultImage(value: string) { + return DEFAULT_STATIC_IMAGES.includes(value); + } + + private isStaticProperty(property: string) { + return STATIC_RESOURCE_FIELDS.includes(property); + } + + public execute(data: Record): Record { + const stack = [data]; + while (stack.length > 0) { + const current = stack.pop(); + + for (const key in current) { + if (Object.hasOwn(current, key)) { + const value = current[key]; + + if (isObject(value)) { + stack.push(value); + continue; + } + + if (this.isStaticProperty(key) && typeof value === 'string') { + const staticPath = STATIC_FILES_ROUTE; + const uploadPath = STATIC_UPLOAD_ROUTE; + const serverHost = this.config.get('HOST'); + const serverPort = this.config.get('PORT'); + + const rootPath = this.hasDefaultImage(value) ? staticPath : uploadPath; + current[key] = `${getFullServerPath(serverHost, serverPort)}${rootPath}/${value}`; + } + } + } + } + + return data; + } +} From 3cd7f19db25960633b6fd1bf65d5da41878d5967 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:22:20 +0300 Subject: [PATCH 10/14] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B8=D1=82=20PathTransformer=20=D0=B2=20BaseController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../libs/rest/controller/base-controller.abstract.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/shared/libs/rest/controller/base-controller.abstract.ts b/src/shared/libs/rest/controller/base-controller.abstract.ts index 36f64bd..38d6389 100644 --- a/src/shared/libs/rest/controller/base-controller.abstract.ts +++ b/src/shared/libs/rest/controller/base-controller.abstract.ts @@ -1,15 +1,18 @@ -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { Controller } from './controller.interface.js'; import { Response, Router } from 'express'; import { Logger } from '../../logger/logger.interface.js'; import { Route } from '../route.interface.js'; import { StatusCodes } from 'http-status-codes'; import asyncHandler from 'express-async-handler'; +import { PathTransformer } from '../transform/path-transformer.js'; +import { Component } from '../../../const.js'; @injectable() export abstract class BaseController implements Controller { private readonly DEFAULT_CONTENT_TYPE = 'application/json'; private readonly _router: Router; + @inject(Component.PathTransformer) private pathTranformer: PathTransformer; constructor( protected readonly logger: Logger @@ -30,10 +33,11 @@ export abstract class BaseController implements Controller { } public send(res: Response, statusCode: number, data: T): void { + const modifiedData = this.pathTranformer.execute(data as Record); res .type(this.DEFAULT_CONTENT_TYPE) .status(statusCode) - .json(data); + .json(modifiedData); } public created(res: Response, data: T): void { From c445260e64d8623e81a5f38f1e35c8a2fda0065c Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:39:15 +0300 Subject: [PATCH 11/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D1=83?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/comment/comment.controller.ts | 4 +-- src/shared/modules/offer/index.ts | 1 + src/shared/modules/offer/offer.controller.ts | 33 +++++++++++++++---- src/shared/modules/offer/offer.entity.ts | 2 +- src/shared/modules/offer/offer.http | 16 +++++++++ .../modules/offer/rdo/upload-image.rdo.ts | 6 ++++ .../modules/user/rdo/logged-user.rdo.ts | 9 +++++ src/shared/modules/user/user.controller.ts | 4 +-- 8 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 src/shared/modules/offer/rdo/upload-image.rdo.ts diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index 0d8fda6..f60b0a6 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -35,7 +35,7 @@ export class CommentController extends BaseController { this.addRoute({ path: '/:offerId', method: 'get', - handler: this.show, + handler: this.getById, middlewares: [ new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') @@ -56,7 +56,7 @@ export class CommentController extends BaseController { await this.offerService.updateById(body.offerId, { rating, commentCount }); } - public async show({ params }: Request, res: Response): Promise { + public async getById({ params }: Request, res: Response): Promise { const { offerId } = params; const comments = await this.commentService.findByOfferId(offerId); this.ok(res, fillDTO(CommentRdo, comments)); diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts index 86e042a..2390976 100644 --- a/src/shared/modules/offer/index.ts +++ b/src/shared/modules/offer/index.ts @@ -3,3 +3,4 @@ export { CreateOfferDto } from './dto/create-offer.dto.js'; export { OfferService } from './offer-service.interface.js'; export { DefaultOfferService } from './default-offer.service.js'; export { createOfferContainer } from './offer.container.js'; +export { UploadImageRdo } from './rdo/upload-image.rdo.js'; diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index b375464..308e570 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -2,22 +2,24 @@ import { Request, Response } from 'express'; import { inject, injectable } from 'inversify'; import { Logger } from '../../libs/logger/logger.interface.js'; import { CITIES_LIST, Component } from '../../const.js'; -import { CreateOfferDto, OfferService } from './index.js'; +import { CreateOfferDto, OfferService, UploadImageRdo } from './index.js'; import { ParamOfferId } from './types/param-offerid.type.js'; import { fillDTO } from '../../helpers/common.js'; import { OfferRdo } from './rdo/offer.rdo.js'; import { CreateOfferRequest } from './types/create-offer-request.type.js'; import { UpdateOfferDto } from './dto/update-offer.dto.js'; import { CommentService } from '../comment/index.js'; -import { BaseController, DocumentExistsMiddleware, PrivateRouteMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; +import { BaseController, DocumentExistsMiddleware, PrivateRouteMiddleware, UploadFileMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; import { ParamCityName } from './types/param-cityname.type.js'; +import { Config, RestSchema } from '../../libs/config/index.js'; @injectable() export class OfferController extends BaseController { constructor( @inject(Component.Logger) protected readonly logger: Logger, @inject(Component.OfferService) private readonly offerService: OfferService, - @inject(Component.CommentService) private readonly commentService: CommentService + @inject(Component.CommentService) private readonly commentService: CommentService, + @inject(Component.Config) private readonly configService: Config, ) { super(logger); this.logger.info('Register routes for OfferController'); @@ -35,7 +37,7 @@ export class OfferController extends BaseController { this.addRoute({ path: '/:offerId', method: 'get', - handler: this.show, + handler: this.getById, middlewares: [ new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') @@ -65,11 +67,21 @@ export class OfferController extends BaseController { this.addRoute({ path: '/premium/:cityName', method: 'get', - handler: this.showPremium + handler: this.getPremium + }); + this.addRoute({ + path: '/:offerId/preview', + method: 'post', + handler: this.uploadImage, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new UploadFileMiddleware(this.configService.get('UPLOAD_DIRECTORY'), 'preview'), + ] }); } - public async show({ params }: Request, res: Response): Promise { + public async getById({ params }: Request, res: Response): Promise { const { offerId } = params; const offer = await this.offerService.findById(offerId); this.ok(res, fillDTO(OfferRdo, offer)); @@ -98,7 +110,7 @@ export class OfferController extends BaseController { this.ok(res, fillDTO(OfferRdo, updatedOffer)); } - public async showPremium({ params }: Request, res: Response) { + public async getPremium({ params }: Request, res: Response) { const { cityName } = params; if (!CITIES_LIST.includes(cityName)) { throw new Error('Нет такого города!'); @@ -107,4 +119,11 @@ export class OfferController extends BaseController { const offers = await this.offerService.findPremiumByCity(cityName); this.ok(res, fillDTO(OfferRdo, offers)); } + + public async uploadImage({ params, file }: Request, res: Response) { + const { offerId } = params; + const updateDto = { preview: file?.filename }; + await this.offerService.updateById(offerId, updateDto); + this.created(res, fillDTO(UploadImageRdo, updateDto)); + } } diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index bd0b536..8381b53 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -27,7 +27,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop({ required: true }) public cityName: string; - @prop({ required: true }) + @prop({ required: false }) public preview: string; @prop({ required: true }) diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http index 136aa3e..852dcdf 100644 --- a/src/shared/modules/offer/offer.http +++ b/src/shared/modules/offer/offer.http @@ -69,4 +69,20 @@ Content-Type: application/json ## Получить список премиальных предложений для города GET http://localhost:5000/offers/premium/Paris HTTP/1.1 + +### + +## Загрузить изображение для объявления + +POST http://localhost:5000/offers/65258514a30fc6ef77c0edf7/preview HTTP/1.1 +Authorization: Bearer "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJpZCI6IjY3MjhlNmZmYWM5MzJiOWJlOTU1ZTAxOCIsImlhdCI6MTczMDc0NTQ2OCwiZXhwIjoxNzMwOTE4MjY4fQ.tTpVbE-z75nKe95Btu0sudHgLODq0W3mIBQlWTq8rAI +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="image"; filename="screen.png" +Content-Type: image/png + +< \Users\Ирина-ПК\Desktop\35x45.jpg +------WebKitFormBoundary7MA4YWxkTrZu0gW-- + ### diff --git a/src/shared/modules/offer/rdo/upload-image.rdo.ts b/src/shared/modules/offer/rdo/upload-image.rdo.ts new file mode 100644 index 0000000..b78f5b1 --- /dev/null +++ b/src/shared/modules/offer/rdo/upload-image.rdo.ts @@ -0,0 +1,6 @@ +import { Expose } from 'class-transformer'; + +export class UploadImageRdo { + @Expose() + public preview: string; +} diff --git a/src/shared/modules/user/rdo/logged-user.rdo.ts b/src/shared/modules/user/rdo/logged-user.rdo.ts index 543bd7d..1fafb19 100644 --- a/src/shared/modules/user/rdo/logged-user.rdo.ts +++ b/src/shared/modules/user/rdo/logged-user.rdo.ts @@ -6,4 +6,13 @@ export class LoggedUserRdo { @Expose() public email: string; + + @Expose() + public name: string; + + @Expose() + public avatar: string; + + @Expose() + public isPro: boolean; } diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 4f9451a..20fe5d5 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -99,8 +99,8 @@ export class UserController extends BaseController { ): Promise { const user = await this.authService.verify(body); const token = await this.authService.authenticate(user); - const responseData = fillDTO(LoggedUserRdo, { email: user.email, token }); - this.ok(res, responseData); + const responseData = fillDTO(LoggedUserRdo, user); + this.ok(res, Object.assign(responseData, { token })); } public async uploadAvatar(req: Request, res: Response) { From 2e414588d95e1d8858311d94fcc459149e087733 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:42:40 +0300 Subject: [PATCH 12/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80=D0=B0?= =?UTF-8?q?=20=D0=B2=20=D0=B1=D0=B0=D0=B7=D1=83=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/modules/user/rdo/upload-user-avatar.rdo.ts | 6 ++++++ src/shared/modules/user/rdo/user.rdo.ts | 3 +++ src/shared/modules/user/user.controller.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/shared/modules/user/rdo/upload-user-avatar.rdo.ts diff --git a/src/shared/modules/user/rdo/upload-user-avatar.rdo.ts b/src/shared/modules/user/rdo/upload-user-avatar.rdo.ts new file mode 100644 index 0000000..8514657 --- /dev/null +++ b/src/shared/modules/user/rdo/upload-user-avatar.rdo.ts @@ -0,0 +1,6 @@ +import { Expose } from 'class-transformer'; + +export class UploadUserAvatarRdo { + @Expose() + public filepath: string; +} diff --git a/src/shared/modules/user/rdo/user.rdo.ts b/src/shared/modules/user/rdo/user.rdo.ts index 3ad72a9..4c6ed50 100644 --- a/src/shared/modules/user/rdo/user.rdo.ts +++ b/src/shared/modules/user/rdo/user.rdo.ts @@ -1,6 +1,9 @@ import { Expose } from 'class-transformer'; export class UserRdo { + @Expose() + public id: string; + @Expose() public name: string; diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 20fe5d5..667b226 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -13,6 +13,7 @@ import { BaseController, HttpError, UploadFileMiddleware, ValidateDtoMiddleware, import { LoginUserDto } from './dto/login-user.dto.js'; import { AuthService } from '../auth/index.js'; import { LoggedUserRdo } from './rdo/logged-user.rdo.js'; +import { UploadUserAvatarRdo } from './rdo/upload-user-avatar.rdo.js'; // import { OfferService } from '../offer/index.js'; @injectable() @@ -103,8 +104,11 @@ export class UserController extends BaseController { this.ok(res, Object.assign(responseData, { token })); } - public async uploadAvatar(req: Request, res: Response) { - this.created(res, { filepath: req.file?.path }); + public async uploadAvatar({ params, file }: Request, res: Response) { + const { userId } = params; + const uploadFile = { avatar: file?.filename }; + await this.userService.updateById(userId, uploadFile); + this.created(res, fillDTO(UploadUserAvatarRdo, { filepath: uploadFile.avatar })); } public async checkAuthenticate({ tokenPayload: { email } }: Request, res: Response) { From 7986c61566c3f89517d3bcecfbbf019c7eafc505 Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:43:43 +0300 Subject: [PATCH 13/14] =?UTF-8?q?=D0=A3=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20CORS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 ++++++++++++++++++++-- package.json | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f022b40..fd1c973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "class-validator": "0.14.1", "convict": "6.2.4", "convict-format-with-validator": "6.2.0", + "cors": "2.8.5", "dayjs": "1.11.13", "dotenv": "16.4.5", "express": "4.21.1", @@ -32,6 +33,7 @@ "devDependencies": { "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.5", + "@types/cors": "2.8.17", "@types/express": "5.0.0", "@types/mime-types": "2.1.4", "@types/multer": "1.4.12", @@ -504,6 +506,15 @@ "@types/convict": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", @@ -1752,7 +1763,6 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, "dependencies": { "object-assign": "^4", "vary": "^1" @@ -7788,6 +7798,15 @@ "@types/convict": "*" } }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", @@ -8705,7 +8724,6 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, "requires": { "object-assign": "^4", "vary": "^1" diff --git a/package.json b/package.json index e187339..4f64392 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.5", + "@types/cors": "2.8.17", "@types/express": "5.0.0", "@types/mime-types": "2.1.4", "@types/multer": "1.4.12", @@ -53,6 +54,7 @@ "class-validator": "0.14.1", "convict": "6.2.4", "convict-format-with-validator": "6.2.0", + "cors": "2.8.5", "dayjs": "1.11.13", "dotenv": "16.4.5", "express": "4.21.1", From 895e43561654f3f71ac6afc3dca52127efc2e44d Mon Sep 17 00:00:00 2001 From: Irina Lagutina Date: Mon, 4 Nov 2024 21:45:33 +0300 Subject: [PATCH 14/14] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B8=D1=82=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20CORS=20?= =?UTF-8?q?=D0=B2=20=D0=BA=D0=B0=D1=87=D0=B5=D1=81=D1=82=D0=B2=D0=B5=20mid?= =?UTF-8?q?dleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/rest.application.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 948025e..cb2a1d9 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -11,6 +11,7 @@ import { Controller } from '../shared/libs/rest/controller/controller.interface. import { ParseTokenMiddleware } from '../shared/libs/rest/middleware/parse-token.middleware.js'; import { getFullServerPath } from '../shared/helpers/common.js'; import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from './rest.constant.js'; +import cors from 'cors'; @injectable() export class RESTApplication { @@ -60,6 +61,7 @@ export class RESTApplication { this.server.use(STATIC_UPLOAD_ROUTE, express.static(this.config.get('UPLOAD_DIRECTORY'))); this.server.use(STATIC_FILES_ROUTE, express.static(this.config.get('STATIC_DIRECTORY_PATH'))); this.server.use(authenticateMiddleware.execute.bind(authenticateMiddleware)); + this.server.use(cors()); } private async initExceptionFilters() {