From 4ee6d0a6353c369133d66940412be56fc58eb70f Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 20:52:58 +0300 Subject: [PATCH 01/17] adds validation error --- src/shared/helpers/common.ts | 12 ++++++++++++ src/shared/libs/rest/errors/index.ts | 1 + src/shared/libs/rest/errors/validation.error.ts | 12 ++++++++++++ src/shared/libs/rest/index.ts | 1 + .../libs/rest/middleware/validate-dto.middleware.ts | 13 ++++++++----- .../libs/rest/types/validation-error-field.type.ts | 5 +++++ 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 src/shared/libs/rest/errors/validation.error.ts create mode 100644 src/shared/libs/rest/types/validation-error-field.type.ts diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index 7358069..7249548 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 getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : ''; @@ -15,3 +17,13 @@ export function fillDTO(someDto: ClassConstructor, plainObject: V) { excludeExtraneousValues: true, }); } + +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/errors/index.ts b/src/shared/libs/rest/errors/index.ts index 0c80068..12e21a7 100644 --- a/src/shared/libs/rest/errors/index.ts +++ b/src/shared/libs/rest/errors/index.ts @@ -1 +1,2 @@ export * from './http-error.js'; +export * from './validation.error.js'; diff --git a/src/shared/libs/rest/errors/validation.error.ts b/src/shared/libs/rest/errors/validation.error.ts new file mode 100644 index 0000000..d5dd8fc --- /dev/null +++ b/src/shared/libs/rest/errors/validation.error.ts @@ -0,0 +1,12 @@ +import { StatusCodes } from 'http-status-codes'; +import { ValidationErrorField } from '../types/validation-error-field.type.js'; +import { HttpError } from './http-error.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/libs/rest/index.ts b/src/shared/libs/rest/index.ts index d8a47e9..43a90c5 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -15,3 +15,4 @@ export * from './middleware/document-exists.middleware.js'; export * from './middleware/upload-file.middleware.js'; export * from './middleware/parse-token.middleware.js'; export * from './middleware/private-route.middleware.js'; +export * from './types/validation-error-field.type.js'; diff --git a/src/shared/libs/rest/middleware/validate-dto.middleware.ts b/src/shared/libs/rest/middleware/validate-dto.middleware.ts index 8915ebd..c629cbe 100644 --- a/src/shared/libs/rest/middleware/validate-dto.middleware.ts +++ b/src/shared/libs/rest/middleware/validate-dto.middleware.ts @@ -1,23 +1,26 @@ import { NextFunction, Request, Response } from 'express'; import { ClassConstructor, plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; -import { StatusCodes } from 'http-status-codes'; import { Middleware } from './middleware.interface.js'; +import { ValidationError } from '../errors/validation.error.js'; +import { reduceValidationErrors } from '../../../helpers/index.js'; export class ValidateDtoMiddleware implements Middleware { constructor(private dto: ClassConstructor) {} public async execute( - { body }: Request, - res: Response, + { 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/types/validation-error-field.type.ts b/src/shared/libs/rest/types/validation-error-field.type.ts new file mode 100644 index 0000000..155246d --- /dev/null +++ b/src/shared/libs/rest/types/validation-error-field.type.ts @@ -0,0 +1,5 @@ +export type ValidationErrorField = { + property: string; + value: string; + messages: string[]; +}; From 2e69ebf8f201bfc994a13d31d702c5abd22b648e Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 21:22:05 +0300 Subject: [PATCH 02/17] adds validation exception filter --- src/shared/helpers/common.ts | 12 ++++-- .../validation.exception-filter.ts | 43 +++++++++++++++++++ src/shared/libs/rest/index.ts | 5 ++- .../libs/rest/types/application-error.enum.ts | 5 +++ 4 files changed, 61 insertions(+), 4 deletions(-) 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 diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index 7249548..aadd736 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -1,14 +1,20 @@ 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 getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : ''; } -export function createErrorObject(message: string) { +export function createErrorObject( + errorType: ApplicationError, + error: string, + details: ValidationErrorField[] = [], +) { return { - error: message, + errorType, + error, + details, }; } 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..cd93721 --- /dev/null +++ b/src/shared/libs/rest/exception-filter/validation.exception-filter.ts @@ -0,0 +1,43 @@ +import { Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { inject, injectable } from 'inversify'; +import { ExceptionFilter } from './exception-filter.interface.js'; +import { Component } from '../../../types/index.js'; +import { Logger } from '../../logger/index.js'; +import { ValidationError } from '../errors/validation.error.js'; +import { createErrorObject } from '../../../helpers/index.js'; +import { ApplicationError } from '../types/application-error.enum.js'; + +@injectable() +export class ValidationExceptionFilter implements ExceptionFilter { + constructor(@inject(Component.Logger) private readonly logger: Logger) { + this.logger.info('Register ValidationExceptionFilter'); + } + + public catch( + error: Error, + _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 43a90c5..091054f 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -3,7 +3,7 @@ export * from './types/route.interface.js'; export * from './controller/controller.interface.js'; export * from './controller/base-controller.abstract.js'; export * from './exception-filter/exception-filter.interface.js'; -export * from './exception-filter/app-exception-filter.js'; +export * from './exception-filter/app.exception-filter.js'; export * from './types/request-params.type.js'; export * from './types/request-body.type.js'; export * from './types/request-query.type.js'; @@ -16,3 +16,6 @@ export * from './middleware/upload-file.middleware.js'; export * from './middleware/parse-token.middleware.js'; export * from './middleware/private-route.middleware.js'; export * from './types/validation-error-field.type.js'; +export * from './types/application-error.enum.js'; +export * from './exception-filter/validation.exception-filter.js'; +export * from './exception-filter/http.exception-filter.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', +} From 68dcc6091c73511aa55e37b95cb36381685962a9 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 21:22:20 +0300 Subject: [PATCH 03/17] adds http exception filter --- .../exception-filter/http.exception-filter.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/shared/libs/rest/exception-filter/http.exception-filter.ts diff --git a/src/shared/libs/rest/exception-filter/http.exception-filter.ts b/src/shared/libs/rest/exception-filter/http.exception-filter.ts new file mode 100644 index 0000000..eadf442 --- /dev/null +++ b/src/shared/libs/rest/exception-filter/http.exception-filter.ts @@ -0,0 +1,36 @@ +import { Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { inject, injectable } from 'inversify'; +import { ExceptionFilter } from './exception-filter.interface.js'; +import { Component } from '../../../types/index.js'; +import { Logger } from '../../logger/index.js'; +import { HttpError } from '../errors/http-error.js'; +import { createErrorObject } from '../../../helpers/index.js'; +import { ApplicationError } from '../types/application-error.enum.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)); + } +} From ef63fd7762bb0d611c2fee7871d0b658def17e3d Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 21:22:41 +0300 Subject: [PATCH 04/17] adds new exception filters to rest app --- src/rest/rest.application.ts | 10 ++++++ src/rest/rest.container.ts | 10 ++++++ ...tion-filter.ts => app.exception-filter.ts} | 34 +++---------------- src/shared/types/component.enum.ts | 2 ++ 4 files changed, 26 insertions(+), 30 deletions(-) rename src/shared/libs/rest/exception-filter/{app-exception-filter.ts => app.exception-filter.ts} (53%) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index f7a760d..f387bc4 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -30,6 +30,10 @@ export class RestApplication { 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(); } @@ -76,6 +80,12 @@ export class RestApplication { 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 052f80d..53a5fef 100644 --- a/src/rest/rest.container.ts +++ b/src/rest/rest.container.ts @@ -10,6 +10,8 @@ import { import { AppExceptionFilter, ExceptionFilter, + HttpErrorExceptionFilter, + ValidationExceptionFilter, } from '../shared/libs/rest/index.js'; export function createRestApplicationContainer() { @@ -35,6 +37,14 @@ export function createRestApplicationContainer() { .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/libs/rest/exception-filter/app-exception-filter.ts b/src/shared/libs/rest/exception-filter/app.exception-filter.ts similarity index 53% rename from src/shared/libs/rest/exception-filter/app-exception-filter.ts rename to src/shared/libs/rest/exception-filter/app.exception-filter.ts index 8458604..82c3f04 100644 --- a/src/shared/libs/rest/exception-filter/app-exception-filter.ts +++ b/src/shared/libs/rest/exception-filter/app.exception-filter.ts @@ -4,8 +4,8 @@ import { inject, injectable } from 'inversify'; import { ExceptionFilter } from './exception-filter.interface.js'; import { Component } from '../../../types/index.js'; import { Logger } from '../../logger/index.js'; -import { HttpError } from '../errors/index.js'; import { createErrorObject } from '../../../helpers/index.js'; +import { ApplicationError } from '../types/application-error.enum.js'; @injectable() export class AppExceptionFilter implements ExceptionFilter { @@ -13,41 +13,15 @@ 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( + 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); - } - - return this.handleOtherError(error, req, res, next); + .json(createErrorObject(ApplicationError.ServiceError, error.message)); } } diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index 1e11b8b..6b18698 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -17,4 +17,6 @@ export const Component = { AuthExceptionFilter: Symbol.for('AuthExceptionFilter'), FavoriteService: Symbol.for('FavoriteService'), FavoriteModel: Symbol.for('FavoriteModel'), + HttpExceptionFilter: Symbol.for('HttpExceptionFilter'), + ValidationExceptionFilter: Symbol.for('ValidationExceptionFilter'), } as const; From 6d0625caaae50b5fbb05e721e8e1b1df6f55cec2 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 21:28:07 +0300 Subject: [PATCH 05/17] adds default user avatar path --- src/shared/modules/user/default-user.service.ts | 6 +++++- src/shared/modules/user/dto/create-user.dto.ts | 3 --- src/shared/modules/user/dto/create-user.messages.ts | 3 --- src/shared/modules/user/index.ts | 1 + src/shared/modules/user/user.constant.ts | 1 + 5 files changed, 7 insertions(+), 7 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 e32447e..7348580 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 '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; import { UpdateUserDTO } from './dto/update-user.dto.js'; +import { DEFAULT_AVATAR_FILE_NAME } from './user.constant.js'; @injectable() export class DefaultUserService implements UserService { @@ -19,7 +20,10 @@ export class DefaultUserService implements UserService { dto: CreateUserDto, salt: string, ): Promise> { - const user = new UserEntity(dto); + const user = new UserEntity({ + ...dto, + avatarPath: DEFAULT_AVATAR_FILE_NAME, + }); user.setPassword(dto.password, salt); const result = await this.userModel.create(user); diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts index 4a44190..3064d59 100644 --- a/src/shared/modules/user/dto/create-user.dto.ts +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -6,9 +6,6 @@ export class CreateUserDto { @IsEmail({}, { message: CreateUserMessages.email.invalidFormat }) public email: string; - @IsString({ message: CreateUserMessages.avatarPath.invalidFormat }) - public avatarPath: string; - @IsString({ message: CreateUserMessages.firstname.invalidFormat }) @Length(1, 15, { message: CreateUserMessages.firstname.lengthField }) public firstname: string; diff --git a/src/shared/modules/user/dto/create-user.messages.ts b/src/shared/modules/user/dto/create-user.messages.ts index c7f2925..e28658f 100644 --- a/src/shared/modules/user/dto/create-user.messages.ts +++ b/src/shared/modules/user/dto/create-user.messages.ts @@ -2,9 +2,6 @@ export const CreateUserMessages = { email: { invalidFormat: 'email must be a valid address', }, - avatarPath: { - invalidFormat: 'avatarPath is required', - }, firstname: { invalidFormat: 'firstname is required', lengthField: 'min length is 1, max is 15', diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index 9fcf0b8..7484818 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -5,3 +5,4 @@ export * from './default-user.service.js'; export * from './user.container.js'; export * from './user-service.interface.js'; export * from './user.controller.js'; +export * 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'; From ac451eaf5e292464f12b6842ec45a3d57d253830 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 21:32:54 +0300 Subject: [PATCH 06/17] adds HOST to config --- src/rest/rest.application.ts | 4 ++-- src/shared/helpers/common.ts | 4 ++++ src/shared/libs/config/rest.schema.ts | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index f387bc4..97b56ab 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -4,7 +4,7 @@ import { Logger } from '../shared/libs/logger/index.js'; import { Config, RestSchema } from '../shared/libs/config/index.js'; import { Component } from '../shared/types/index.js'; import { DatabaseClient } from '../shared/libs/database-client/index.js'; -import { getMongoURI } from '../shared/helpers/index.js'; +import { getFullServerPath, getMongoURI } from '../shared/helpers/index.js'; import { Controller, ExceptionFilter, @@ -113,7 +113,7 @@ 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')}`, + `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 aadd736..8e11cba 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -33,3 +33,7 @@ export function reduceValidationErrors( 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 151c9f4..80b7041 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -27,6 +27,7 @@ export type RestSchema = { DB_NAME: string; UPLOAD_DIRECTORY: string; JWT_SECRET: string; + HOST: string; }; export const configRestSchema = convict({ @@ -84,4 +85,10 @@ export const configRestSchema = convict({ env: 'JWT_SECRET', default: null, }, + HOST: { + doc: 'Host where service started', + format: String, + env: 'HOST', + default: 'localhost', + }, }); From 07e5d38818b83dd3fa8b65ead1d27b1fc7e17fb6 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 21:55:33 +0300 Subject: [PATCH 07/17] adds static directory --- src/rest/rest.application.ts | 4 ++++ src/shared/libs/config/rest.schema.ts | 7 +++++++ static/default-avatar.jpg | Bin 0 -> 5912 bytes 3 files changed, 11 insertions(+) create mode 100644 static/default-avatar.jpg diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 97b56ab..982a124 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -71,6 +71,10 @@ export class RestApplication { '/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 80b7041..74c52db 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -28,6 +28,7 @@ export type RestSchema = { UPLOAD_DIRECTORY: string; JWT_SECRET: string; HOST: string; + STATIC_DIRECTORY_PATH: string; }; export const configRestSchema = convict({ @@ -91,4 +92,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', + }, }); diff --git a/static/default-avatar.jpg b/static/default-avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a5ae95a57dd0a33228237197e86eb24025b736de GIT binary patch literal 5912 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 Date: Thu, 14 May 2026 21:58:15 +0300 Subject: [PATCH 08/17] adds static constants --- src/rest/index.ts | 1 + src/rest/rest.application.ts | 5 +++-- src/rest/rest.constant.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/rest/rest.constant.ts diff --git a/src/rest/index.ts b/src/rest/index.ts index 2758071..47e67c6 100644 --- a/src/rest/index.ts +++ b/src/rest/index.ts @@ -1,2 +1,3 @@ export * from './rest.application.js'; export * from './rest.container.js'; +export * from './rest.constant.js'; diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 982a124..bd2be33 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -10,6 +10,7 @@ import { ExceptionFilter, ParseTokenMiddleware, } from '../shared/libs/rest/index.js'; +import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from './rest.constant.js'; @injectable() export class RestApplication { @@ -68,11 +69,11 @@ export class RestApplication { this.server.use(express.json()); this.server.use( - '/upload', + STATIC_UPLOAD_ROUTE, express.static(this.config.get('UPLOAD_DIRECTORY')), ); this.server.use( - '/static', + STATIC_FILES_ROUTE, express.static(this.config.get('STATIC_DIRECTORY_PATH')), ); this.server.use( 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 3a51d00b08837e6157f2af7ed20b90143b615118 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 22:48:47 +0300 Subject: [PATCH 09/17] adds path transformer for static --- src/rest/rest.container.ts | 5 ++ .../controller/base-controller.abstract.ts | 13 +++- .../transform/path-transformer.constant.ts | 3 + .../libs/rest/transform/path-transformer.ts | 69 +++++++++++++++++++ src/shared/types/component.enum.ts | 1 + 5 files changed, 89 insertions(+), 2 deletions(-) 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 53a5fef..a8808a6 100644 --- a/src/rest/rest.container.ts +++ b/src/rest/rest.container.ts @@ -13,6 +13,7 @@ import { HttpErrorExceptionFilter, ValidationExceptionFilter, } from '../shared/libs/rest/index.js'; +import { PathTransformer } from '../shared/libs/rest/transform/path-transformer.js'; export function createRestApplicationContainer() { const restApplicationContainer = new Container(); @@ -45,6 +46,10 @@ export function createRestApplicationContainer() { .bind(Component.ValidationExceptionFilter) .to(ValidationExceptionFilter) .inSingletonScope(); + restApplicationContainer + .bind(Component.PathTransformer) + .to(PathTransformer) + .inSingletonScope(); return restApplicationContainer; } diff --git a/src/shared/libs/rest/controller/base-controller.abstract.ts b/src/shared/libs/rest/controller/base-controller.abstract.ts index 0e077b7..a5572d5 100644 --- a/src/shared/libs/rest/controller/base-controller.abstract.ts +++ b/src/shared/libs/rest/controller/base-controller.abstract.ts @@ -1,11 +1,13 @@ import { Response, Router } from 'express'; import { ParamsDictionary } from 'express-serve-static-core'; import asyncHandler from 'express-async-handler'; -import { injectable } from 'inversify'; +import { injectable, inject } from 'inversify'; import { Controller } from './controller.interface.js'; import { Logger } from '../../logger/index.js'; import { Route } from '../types/route.interface.js'; import { StatusCodes } from 'http-status-codes'; +import { Component } from '../../../types/index.js'; +import { PathTransformer } from '../transform/path-transformer.js'; const DEFAULT_CONTENT_TYPE = 'application/json'; @@ -13,6 +15,9 @@ const DEFAULT_CONTENT_TYPE = 'application/json'; export abstract class BaseController implements Controller { private readonly _router: Router; + @inject(Component.PathTransformer) + private pathTransformer: PathTransformer; + constructor(protected readonly logger: Logger) { this._router = Router(); } @@ -39,7 +44,11 @@ export abstract class BaseController implements Controller { } public send(res: Response, statusCode: number, data: T): void { - res.type(DEFAULT_CONTENT_TYPE).status(statusCode).json(data); + const modifiedData = this.pathTransformer.execute( + data as Record, + ); + + res.type(DEFAULT_CONTENT_TYPE).status(statusCode).json(modifiedData); } public created(res: Response, data: T): void { 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..fc17149 --- /dev/null +++ b/src/shared/libs/rest/transform/path-transformer.constant.ts @@ -0,0 +1,3 @@ +export const DEFAULT_STATIC_IMAGES = ['default-avatar.jpg']; + +export const STATIC_RESOURCE_FIELDS = ['avatarPath', 'image']; 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..bbf7a40 --- /dev/null +++ b/src/shared/libs/rest/transform/path-transformer.ts @@ -0,0 +1,69 @@ +import { inject, injectable } from 'inversify'; +import { Component } from '../../../types/component.enum.js'; +import { Logger } from '../../logger/index.js'; +import { Config, RestSchema } from '../../config/index.js'; +import { + DEFAULT_STATIC_IMAGES, + STATIC_RESOURCE_FIELDS, +} from './path-transformer.constant.js'; +import { + STATIC_FILES_ROUTE, + STATIC_UPLOAD_ROUTE, +} from '../../../../rest/index.js'; +import { getFullServerPath } from '../../../helpers/index.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; + } +} diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index 6b18698..3bf1049 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -19,4 +19,5 @@ export const Component = { FavoriteModel: Symbol.for('FavoriteModel'), HttpExceptionFilter: Symbol.for('HttpExceptionFilter'), ValidationExceptionFilter: Symbol.for('ValidationExceptionFilter'), + PathTransformer: Symbol.for('PathTransformer'), } as const; From 41b35d27ba83a201f9de44b3ba0a7e8cb289e959 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Thu, 14 May 2026 22:53:34 +0300 Subject: [PATCH 10/17] updates logged user rdo --- src/shared/modules/user/rdo/logged-user.rdo.ts | 9 +++++++++ src/shared/modules/user/user.controller.ts | 7 ++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/shared/modules/user/rdo/logged-user.rdo.ts b/src/shared/modules/user/rdo/logged-user.rdo.ts index 543bd7d..fba093a 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 avatarPath: string; + + @Expose() + public firstname: string; + + @Expose() + public lastname: string; } diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 21d0d89..1b1e558 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -88,11 +88,8 @@ export class UserController extends BaseController { public async login({ body }: LoginUserRequest, res: Response): 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 cdadb983bb3c5dbdec5a23a7678789959218428c Mon Sep 17 00:00:00 2001 From: Alexey Zhila Date: Fri, 15 May 2026 15:40:31 +0300 Subject: [PATCH 11/17] adds offer images and preview upload --- src/shared/libs/rest/index.ts | 1 + .../middleware/upload-files.middleware.ts | 35 ++++++++ .../modules/offer/default-offer.service.ts | 2 +- .../modules/offer/dto/create-offer.dto.ts | 15 ---- .../modules/offer/dto/update-offer.dto.ts | 36 +++------ src/shared/modules/offer/index.ts | 2 + .../{offer.constants.ts => offer.constant.ts} | 1 + src/shared/modules/offer/offer.controller.ts | 81 +++++++++++++++++++ .../modules/offer/rdo/upload-images.rdo.ts | 6 ++ .../modules/offer/rdo/upload-preview.rdo.ts | 6 ++ 10 files changed, 146 insertions(+), 39 deletions(-) create mode 100644 src/shared/libs/rest/middleware/upload-files.middleware.ts rename src/shared/modules/offer/{offer.constants.ts => offer.constant.ts} (69%) create mode 100644 src/shared/modules/offer/rdo/upload-images.rdo.ts create mode 100644 src/shared/modules/offer/rdo/upload-preview.rdo.ts diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index 091054f..ce504c7 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -19,3 +19,4 @@ export * from './types/validation-error-field.type.js'; export * from './types/application-error.enum.js'; export * from './exception-filter/validation.exception-filter.js'; export * from './exception-filter/http.exception-filter.js'; +export * from './middleware/upload-files.middleware.js'; diff --git a/src/shared/libs/rest/middleware/upload-files.middleware.ts b/src/shared/libs/rest/middleware/upload-files.middleware.ts new file mode 100644 index 0000000..6f83cfe --- /dev/null +++ b/src/shared/libs/rest/middleware/upload-files.middleware.ts @@ -0,0 +1,35 @@ +import { NextFunction, Request, Response } from 'express'; +import multer, { diskStorage } from 'multer'; +import { extension } from 'mime-types'; +import { nanoid } from 'nanoid'; +import { Middleware } from './middleware.interface.js'; + +export class UploadFilesMiddleware implements Middleware { + constructor( + private uploadDirectory: string, + private fieldName: string, + private maxCount?: number, + ) {} + + public async execute( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + const storage = diskStorage({ + destination: this.uploadDirectory, + filename: (_req, file, callback) => { + const fileExtention = extension(file.mimetype); + const filename = nanoid(); + callback(null, `${filename}.${fileExtention}`); + }, + }); + + const uploadFilesMiddleware = multer({ storage }).array( + this.fieldName, + this.maxCount, + ); + + uploadFilesMiddleware(req, res, next); + } +} diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index 16d9b82..d40dbf3 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -9,7 +9,7 @@ import { UpdateOfferDTO } from './dto/update-offer.dto.js'; import { DEFAULT_OFFER_COUNT, DEFAULT_PREMIUM_OFFER_COUNT, -} from './offer.constants.js'; +} from './offer.constant.js'; import { CommentEntity } from '../comment/index.js'; import { Types } from 'mongoose'; import { FavoriteEntity } from '../favorite/index.js'; diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index 011ed7e..4035045 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -48,21 +48,6 @@ export class CreateOfferDTO { @IsEnum(City, { message: CreateOfferValidationMessage.city.invalid }) city: City; - @IsUrl( - {}, - { message: CreateOfferValidationMessage.previewImage.invalidFormat }, - ) - previewImage: string; - - @IsArray({ message: CreateOfferValidationMessage.images.invalidFormat }) - @ArrayMinSize(6, { - message: CreateOfferValidationMessage.images.invalidLength, - }) - @ArrayMaxSize(6, { - message: CreateOfferValidationMessage.images.invalidLength, - }) - images: string[]; - @IsBoolean({ message: CreateOfferValidationMessage.isPremium.invalidFormat }) isPremium: boolean; diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts index 1fd63a3..7453e78 100644 --- a/src/shared/modules/offer/dto/update-offer.dto.ts +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -10,7 +10,6 @@ import { ArrayMinSize, ArrayMaxSize, IsBoolean, - IsNumber, Min, Max, IsIn, @@ -32,7 +31,7 @@ export class UpdateOfferDTO { @IsOptional() @MinLength(10, { message: CreateUpdateOfferMessage.title.minLength }) @MaxLength(100, { message: CreateUpdateOfferMessage.title.maxLength }) - title: string; + public title?: string; @IsOptional() @MinLength(20, { @@ -41,22 +40,22 @@ export class UpdateOfferDTO { @MaxLength(1024, { message: CreateUpdateOfferMessage.description.maxLength, }) - description: string; + public description?: string; @IsOptional() @IsDateString( {}, { message: CreateUpdateOfferMessage.publishDate.invalidFormat }, ) - publishDate: Date; + public publishDate?: Date; @IsOptional() @IsEnum(City, { message: CreateUpdateOfferMessage.city.invalid }) - city: City; + public city?: City; @IsOptional() @IsUrl({}, { message: CreateUpdateOfferMessage.previewImage.invalidFormat }) - previewImage: string; + public previewImage?: string; @IsOptional() @IsArray({ message: CreateUpdateOfferMessage.images.invalidFormat }) @@ -66,42 +65,33 @@ export class UpdateOfferDTO { @ArrayMaxSize(6, { message: CreateUpdateOfferMessage.images.invalidLength, }) - images: string[]; + public images?: string[]; @IsOptional() @IsBoolean({ message: CreateUpdateOfferMessage.isPremium.invalidFormat }) - isPremium: boolean; - - @IsOptional() - @IsNumber( - { maxDecimalPlaces: 1 }, - { message: CreateUpdateOfferMessage.rating.invalidFormat }, - ) - @Min(0, { message: CreateUpdateOfferMessage.rating.minValue }) - @Max(5, { message: CreateUpdateOfferMessage.rating.maxValue }) - rating: number; + public isPremium?: boolean; @IsOptional() @IsIn(HousingValues, { message: CreateUpdateOfferMessage.type.invalid }) - type: Housing; + public type?: Housing; @IsOptional() @IsInt({ message: CreateUpdateOfferMessage.rooms.invalidFormat }) @Min(1, { message: CreateUpdateOfferMessage.rooms.minValue }) @Max(8, { message: CreateUpdateOfferMessage.rooms.maxValue }) - rooms: number; + public rooms?: number; @IsOptional() @IsInt({ message: CreateUpdateOfferMessage.guests.invalidFormat }) @Min(1, { message: CreateUpdateOfferMessage.guests.minValue }) @Max(10, { message: CreateUpdateOfferMessage.guests.maxValue }) - guests: number; + public guests?: number; @IsOptional() @IsInt({ message: CreateUpdateOfferMessage.price.invalidFormat }) @Min(100, { message: CreateUpdateOfferMessage.price.minValue }) @Max(100000, { message: CreateUpdateOfferMessage.price.maxValue }) - price: number; + public price?: number; @IsOptional() @IsArray({ message: CreateUpdateOfferMessage.facilities.invalidFormat }) @@ -112,10 +102,10 @@ export class UpdateOfferDTO { each: true, message: CreateUpdateOfferMessage.facilities.invalidValue, }) - facilities: Facility[]; + public facilities?: Facility[]; @IsOptional() @ValidateNested({ message: CreateUpdateOfferMessage.location.invalid }) @Type(() => CoordinatesDto) - location: CoordinatesDto; + public location?: CoordinatesDto; } diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts index 22f87bc..394962b 100644 --- a/src/shared/modules/offer/index.ts +++ b/src/shared/modules/offer/index.ts @@ -6,3 +6,5 @@ export * from './offer.container.js'; export * from './offer.controller.js'; export * from './rdo/offer.rdo.js'; export * from './dto/create-offer.messages.js'; +export * from './rdo/upload-images.rdo.js'; +export * from './rdo/upload-preview.rdo.js'; diff --git a/src/shared/modules/offer/offer.constants.ts b/src/shared/modules/offer/offer.constant.ts similarity index 69% rename from src/shared/modules/offer/offer.constants.ts rename to src/shared/modules/offer/offer.constant.ts index e02f6cb..ddbcd56 100644 --- a/src/shared/modules/offer/offer.constants.ts +++ b/src/shared/modules/offer/offer.constant.ts @@ -1,2 +1,3 @@ export const DEFAULT_OFFER_COUNT = 60; export const DEFAULT_PREMIUM_OFFER_COUNT = 3; +export const OFFER_IMAGES_LENGTH = 6; diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 3011793..08afae7 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -8,6 +8,8 @@ import { HttpMethod, PrivateRouteMiddleware, RequestQuery, + UploadFileMiddleware, + UploadFilesMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware, } from '../../libs/rest/index.js'; @@ -23,6 +25,10 @@ import { ParamsDictionary } from 'express-serve-static-core'; import { CommentRdo, CommentService } from '../comment/index.js'; import { CreateOfferDTO } from './dto/create-offer.dto.js'; import { FavoriteService } from '../favorite/index.js'; +import { Config, RestSchema } from '../../libs/config/index.js'; +import { OFFER_IMAGES_LENGTH } from './offer.constant.js'; +import { UploadImagesRdo } from './rdo/upload-images.rdo.js'; +import { UploadPreviewRdo } from './rdo/upload-preview.rdo.js'; @injectable() export class OfferController extends BaseController { @@ -34,6 +40,8 @@ export class OfferController extends BaseController { private readonly commentService: CommentService, @inject(Component.FavoriteService) private readonly favoriteService: FavoriteService, + @inject(Component.Config) + private readonly configService: Config, ) { super(logger); @@ -122,6 +130,35 @@ export class OfferController extends BaseController { new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), ], }); + this.addRoute({ + path: '/:offerId/preview', + method: HttpMethod.Post, + handler: this.uploadPreview, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + new UploadFileMiddleware( + this.configService.get('UPLOAD_DIRECTORY'), + 'preview', + ), + ], + }); + this.addRoute({ + path: '/:offerId/images', + method: HttpMethod.Post, + handler: this.uploadImages, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + new UploadFilesMiddleware( + this.configService.get('UPLOAD_DIRECTORY'), + 'images', + OFFER_IMAGES_LENGTH, + ), + ], + }); } public async show( @@ -246,4 +283,48 @@ export class OfferController extends BaseController { this.ok(res, fillDTO(OfferRdo, offers)); } + + public async uploadPreview( + { params, file }: Request, + res: Response, + ): Promise { + const { offerId } = params; + + if (!file) { + throw new HttpError( + StatusCodes.UNPROCESSABLE_ENTITY, + 'Preview image is required', + 'OfferController', + ); + } + + const updatedOffer = await this.offerService.updateById(offerId, { + previewImage: file.filename, + }); + const previewRdo = fillDTO(UploadPreviewRdo, updatedOffer); + this.created(res, previewRdo); + } + + public async uploadImages( + { params, files }: Request, + res: Response, + ): Promise { + const { offerId } = params; + const filesToUpload = Array.isArray(files) ? files : undefined; + + if (!filesToUpload || filesToUpload.length !== OFFER_IMAGES_LENGTH) { + throw new HttpError( + StatusCodes.UNPROCESSABLE_ENTITY, + `Images must contain exactly ${OFFER_IMAGES_LENGTH} items`, + 'OfferController', + ); + } + + const images = filesToUpload.map((file) => file.filename); + const updatedOffer = await this.offerService.updateById(offerId, { + images, + }); + const imagesRdo = fillDTO(UploadImagesRdo, updatedOffer); + this.created(res, imagesRdo); + } } diff --git a/src/shared/modules/offer/rdo/upload-images.rdo.ts b/src/shared/modules/offer/rdo/upload-images.rdo.ts new file mode 100644 index 0000000..ba1b417 --- /dev/null +++ b/src/shared/modules/offer/rdo/upload-images.rdo.ts @@ -0,0 +1,6 @@ +import { Expose } from 'class-transformer'; + +export class UploadImagesRdo { + @Expose() + public images: string[]; +} diff --git a/src/shared/modules/offer/rdo/upload-preview.rdo.ts b/src/shared/modules/offer/rdo/upload-preview.rdo.ts new file mode 100644 index 0000000..c69ca34 --- /dev/null +++ b/src/shared/modules/offer/rdo/upload-preview.rdo.ts @@ -0,0 +1,6 @@ +import { Expose } from 'class-transformer'; + +export class UploadPreviewRdo { + @Expose() + public previewImage: string; +} From cf079be5b657706eed9dcf9afccee15c1e4e745e Mon Sep 17 00:00:00 2001 From: Alexey Zhila Date: Fri, 15 May 2026 15:51:01 +0300 Subject: [PATCH 12/17] updates user avatar upload --- specification/project.spec.yml | 11 ----------- .../modules/offer/types/param-offerid.type.ts | 2 -- .../modules/user/rdo/upload-user-avatar.rdo.ts | 6 ++++++ src/shared/modules/user/rdo/user.rdo.ts | 3 +++ .../modules/user/types/param-userid.type.ts | 3 +++ src/shared/modules/user/user.controller.ts | 17 +++++++++++++---- 6 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 src/shared/modules/user/rdo/upload-user-avatar.rdo.ts create mode 100644 src/shared/modules/user/types/param-userid.type.ts diff --git a/specification/project.spec.yml b/specification/project.spec.yml index 1d882f0..aa785a4 100644 --- a/specification/project.spec.yml +++ b/specification/project.spec.yml @@ -75,17 +75,6 @@ paths: '401': $ref: '#/components/responses/Unauthorized' - /users/logout: - post: - tags: [users] - summary: Выход пользователя - description: Завершает пользовательскую сессию - responses: - '204': - description: Успешный выход - '401': - $ref: '#/components/responses/Unauthorized' - /users/{userId}/avatar: post: tags: [users] diff --git a/src/shared/modules/offer/types/param-offerid.type.ts b/src/shared/modules/offer/types/param-offerid.type.ts index ad18197..a2bcc32 100644 --- a/src/shared/modules/offer/types/param-offerid.type.ts +++ b/src/shared/modules/offer/types/param-offerid.type.ts @@ -1,5 +1,3 @@ -// import { ParamsDictionary } from 'express-serve-static-core'; - export type ParamOfferId = { offerId: string; }; 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 7de404d..1f7aa85 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 email: string; diff --git a/src/shared/modules/user/types/param-userid.type.ts b/src/shared/modules/user/types/param-userid.type.ts new file mode 100644 index 0000000..4c63ac7 --- /dev/null +++ b/src/shared/modules/user/types/param-userid.type.ts @@ -0,0 +1,3 @@ +export type ParamUserId = { + userId: string; +}; diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 1b1e558..5df9d17 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -21,6 +21,8 @@ import { CreateUserDto } from './dto/create-user.dto.js'; import { LoginUserDto } from './dto/login-user.dto.js'; import { AuthService } from '../auth/index.js'; import { LoggedUserRdo } from './rdo/logged-user.rdo.js'; +import { ParamUserId } from './types/param-userid.type.js'; +import { UploadUserAvatarRdo } from './rdo/upload-user-avatar.rdo.js'; @injectable() export class UserController extends BaseController { @@ -92,10 +94,17 @@ 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 = { avatarPath: file?.filename }; + await this.userService.updateById(userId, uploadFile); + this.created( + res, + fillDTO(UploadUserAvatarRdo, { filepath: uploadFile.avatarPath }), + ); } public async checkAuth({ tokenPayload: { email } }: Request, res: Response) { From 0cf0ac066cde2c75312179ac2fac428c18faf160 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Fri, 15 May 2026 18:19:41 +0300 Subject: [PATCH 13/17] installs cors --- package-lock.json | 49 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 ++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4bd236..1b5cac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "six-cities", "version": "5.0.0", "dependencies": { + "@types/cors": "^2.8.19", "chalk": "5.6.2", "class-transformer": "0.5.1", "class-validator": "0.15.1", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", + "cors": "^2.8.6", "dayjs": "1.11.19", "express": "5.2.1", "express-async-handler": "1.2.0", @@ -809,6 +811,15 @@ "@types/convict": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -1739,6 +1750,23 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4515,7 +4543,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7112,6 +7139,14 @@ "@types/convict": "*" } }, + "@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -7742,6 +7777,15 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" }, + "cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -9613,8 +9657,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.13.4", diff --git a/package.json b/package.json index d574872..5ddbee6 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,13 @@ "npm": ">=8" }, "dependencies": { + "@types/cors": "^2.8.19", "chalk": "5.6.2", "class-transformer": "0.5.1", "class-validator": "0.15.1", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", + "cors": "^2.8.6", "dayjs": "1.11.19", "express": "5.2.1", "express-async-handler": "1.2.0", From d575c50d9a28ac6aa6a0421a590cce5f9e7c4a4e Mon Sep 17 00:00:00 2001 From: Adonariz Date: Fri, 15 May 2026 18:28:15 +0300 Subject: [PATCH 14/17] adds cors to app, fixes lint errors --- src/rest/rest.application.ts | 2 ++ src/shared/modules/offer/dto/create-offer.dto.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index bd2be33..71a1944 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -1,3 +1,4 @@ +import cors from 'cors'; import express, { Express } from 'express'; import { inject, injectable } from 'inversify'; import { Logger } from '../shared/libs/logger/index.js'; @@ -79,6 +80,7 @@ export class RestApplication { this.server.use( authenticateMiddleware.execute.bind(authenticateMiddleware), ); + this.server.use(cors()); } private async _initExceptionFilters() { diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index 4035045..0b2eaa1 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -1,6 +1,5 @@ /* eslint-disable indent */ import { - ArrayMaxSize, ArrayMinSize, IsArray, IsBoolean, @@ -8,7 +7,6 @@ import { IsEnum, IsIn, IsInt, - IsUrl, Max, MaxLength, Min, From 7e4730ac0eed2beac01dfea433aed1e07d7aa761 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Tue, 19 May 2026 20:12:12 +0300 Subject: [PATCH 15/17] moves rest constants to shared --- src/rest/index.ts | 1 - src/rest/rest.application.ts | 3 ++- src/shared/libs/rest/index.ts | 1 + src/{ => shared/libs}/rest/rest.constant.ts | 0 src/shared/libs/rest/transform/path-transformer.ts | 5 +---- 5 files changed, 4 insertions(+), 6 deletions(-) rename src/{ => shared/libs}/rest/rest.constant.ts (100%) diff --git a/src/rest/index.ts b/src/rest/index.ts index 47e67c6..2758071 100644 --- a/src/rest/index.ts +++ b/src/rest/index.ts @@ -1,3 +1,2 @@ export * from './rest.application.js'; export * from './rest.container.js'; -export * from './rest.constant.js'; diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 71a1944..34125e3 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -10,8 +10,9 @@ import { Controller, ExceptionFilter, ParseTokenMiddleware, + STATIC_FILES_ROUTE, + STATIC_UPLOAD_ROUTE, } from '../shared/libs/rest/index.js'; -import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from './rest.constant.js'; @injectable() export class RestApplication { diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index ce504c7..729af2f 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -20,3 +20,4 @@ export * from './types/application-error.enum.js'; export * from './exception-filter/validation.exception-filter.js'; export * from './exception-filter/http.exception-filter.js'; export * from './middleware/upload-files.middleware.js'; +export * from './rest.constant.js'; diff --git a/src/rest/rest.constant.ts b/src/shared/libs/rest/rest.constant.ts similarity index 100% rename from src/rest/rest.constant.ts rename to src/shared/libs/rest/rest.constant.ts diff --git a/src/shared/libs/rest/transform/path-transformer.ts b/src/shared/libs/rest/transform/path-transformer.ts index bbf7a40..a9d483c 100644 --- a/src/shared/libs/rest/transform/path-transformer.ts +++ b/src/shared/libs/rest/transform/path-transformer.ts @@ -6,11 +6,8 @@ import { DEFAULT_STATIC_IMAGES, STATIC_RESOURCE_FIELDS, } from './path-transformer.constant.js'; -import { - STATIC_FILES_ROUTE, - STATIC_UPLOAD_ROUTE, -} from '../../../../rest/index.js'; import { getFullServerPath } from '../../../helpers/index.js'; +import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from '../rest.constant.js'; function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null; From c2dc25c9bf41c9341496e89733d910ed352b2201 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Tue, 19 May 2026 20:20:04 +0300 Subject: [PATCH 16/17] moves cors middleware init --- src/rest/rest.application.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 34125e3..2bdaf83 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -65,10 +65,7 @@ export class RestApplication { } private async _initMiddleware() { - const authenticateMiddleware = new ParseTokenMiddleware( - this.config.get('JWT_SECRET'), - ); - + this.server.use(cors()); this.server.use(express.json()); this.server.use( STATIC_UPLOAD_ROUTE, @@ -78,10 +75,13 @@ export class RestApplication { STATIC_FILES_ROUTE, express.static(this.config.get('STATIC_DIRECTORY_PATH')), ); + + const authenticateMiddleware = new ParseTokenMiddleware( + this.config.get('JWT_SECRET'), + ); this.server.use( authenticateMiddleware.execute.bind(authenticateMiddleware), ); - this.server.use(cors()); } private async _initExceptionFilters() { From 57b53cd746467a60d103cb0311762fe452bdaac7 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Tue, 19 May 2026 21:07:28 +0300 Subject: [PATCH 17/17] adds file validation to upload files middleware --- .../rest/middleware/upload-file.middleware.ts | 46 +++++++++++++-- .../middleware/upload-files.middleware.ts | 59 +++++++++++++++++-- src/shared/libs/rest/rest.constant.ts | 2 + src/shared/modules/offer/offer.controller.ts | 6 ++ src/shared/modules/user/user.controller.ts | 4 ++ 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/shared/libs/rest/middleware/upload-file.middleware.ts b/src/shared/libs/rest/middleware/upload-file.middleware.ts index 8f8e7cb..3dff54d 100644 --- a/src/shared/libs/rest/middleware/upload-file.middleware.ts +++ b/src/shared/libs/rest/middleware/upload-file.middleware.ts @@ -3,11 +3,15 @@ import multer, { diskStorage } from 'multer'; import { extension } from 'mime-types'; import { nanoid } from 'nanoid'; import { Middleware } from './middleware.interface.js'; +import { StatusCodes } from 'http-status-codes'; +import { HttpError } from '../errors/index.js'; export class UploadFileMiddleware implements Middleware { constructor( private uploadDirectory: string, private fieldName: string, + private readonly allowedMimeTypes: string[], + private readonly maxFileSize: number, ) {} public async execute( @@ -24,10 +28,44 @@ export class UploadFileMiddleware implements Middleware { }, }); - const uploadSingleFileMiddleware = multer({ storage }).single( - this.fieldName, - ); + const uploadSingleFileMiddleware = multer({ + storage, + limits: { + fileSize: this.maxFileSize, + }, + fileFilter: (_req, file, callback) => { + if (!this.allowedMimeTypes.includes(file.mimetype)) { + return callback( + new HttpError( + StatusCodes.BAD_REQUEST, + 'Invalid file type', + 'UploadFileMiddleware', + ), + ); + } + + callback(null, true); + }, + }).single(this.fieldName); - uploadSingleFileMiddleware(req, res, next); + uploadSingleFileMiddleware(req, res, (error) => { + if (error instanceof multer.MulterError) { + if (error.code === 'LIMIT_FILE_SIZE') { + return next( + new HttpError( + StatusCodes.BAD_REQUEST, + 'File is too large', + 'UploadFileMiddleware', + ), + ); + } + } + + if (error) { + return next(error); + } + + return next(); + }); } } diff --git a/src/shared/libs/rest/middleware/upload-files.middleware.ts b/src/shared/libs/rest/middleware/upload-files.middleware.ts index 6f83cfe..018cb78 100644 --- a/src/shared/libs/rest/middleware/upload-files.middleware.ts +++ b/src/shared/libs/rest/middleware/upload-files.middleware.ts @@ -3,12 +3,16 @@ import multer, { diskStorage } from 'multer'; import { extension } from 'mime-types'; import { nanoid } from 'nanoid'; import { Middleware } from './middleware.interface.js'; +import { HttpError } from '../errors/index.js'; +import { StatusCodes } from 'http-status-codes'; export class UploadFilesMiddleware implements Middleware { constructor( private uploadDirectory: string, private fieldName: string, - private maxCount?: number, + private readonly allowedMimeTypes: string[], + private readonly maxFileSize: number, + private readonly maxCount?: number, ) {} public async execute( @@ -25,11 +29,54 @@ export class UploadFilesMiddleware implements Middleware { }, }); - const uploadFilesMiddleware = multer({ storage }).array( - this.fieldName, - this.maxCount, - ); + const uploadFilesMiddleware = multer({ + storage, + limits: { + fileSize: this.maxFileSize, + }, + fileFilter: (_req, file, callback) => { + if (!this.allowedMimeTypes.includes(file.mimetype)) { + return callback( + new HttpError( + StatusCodes.BAD_REQUEST, + 'Invalid file type', + 'UploadFilesMiddleware', + ), + ); + } + + callback(null, true); + }, + }).array(this.fieldName, this.maxCount); + + uploadFilesMiddleware(req, res, (error) => { + if (error instanceof multer.MulterError) { + if (error.code === 'LIMIT_FILE_SIZE') { + return next( + new HttpError( + StatusCodes.BAD_REQUEST, + 'File is too large', + 'UploadFilesMiddleware', + ), + ); + } - uploadFilesMiddleware(req, res, next); + if (error.code === 'LIMIT_UNEXPECTED_FILE') { + return next( + new HttpError( + StatusCodes.BAD_REQUEST, + 'Too many files uploaded', + 'UploadFilesMiddleware', + ), + ); + } + } + + if (error) { + return next(error); + } + + return next(); + }); } } diff --git a/src/shared/libs/rest/rest.constant.ts b/src/shared/libs/rest/rest.constant.ts index 77ff89d..4dcffcf 100644 --- a/src/shared/libs/rest/rest.constant.ts +++ b/src/shared/libs/rest/rest.constant.ts @@ -1,2 +1,4 @@ export const STATIC_UPLOAD_ROUTE = '/upload'; export const STATIC_FILES_ROUTE = '/static'; +export const MAX_SINGLE_FILE_SIZE = 5 * 1024 * 1024; // 5 MB +export const ALLOWED_IMAGES = ['image/jpeg', 'image/png', 'image/webp']; diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 08afae7..f7d52d0 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -2,10 +2,12 @@ import { StatusCodes } from 'http-status-codes'; import { inject, injectable } from 'inversify'; import { Request, Response } from 'express'; import { + ALLOWED_IMAGES, BaseController, DocumentExistsMiddleware, HttpError, HttpMethod, + MAX_SINGLE_FILE_SIZE, PrivateRouteMiddleware, RequestQuery, UploadFileMiddleware, @@ -141,6 +143,8 @@ export class OfferController extends BaseController { new UploadFileMiddleware( this.configService.get('UPLOAD_DIRECTORY'), 'preview', + ALLOWED_IMAGES, + MAX_SINGLE_FILE_SIZE, ), ], }); @@ -155,6 +159,8 @@ export class OfferController extends BaseController { new UploadFilesMiddleware( this.configService.get('UPLOAD_DIRECTORY'), 'images', + ALLOWED_IMAGES, + MAX_SINGLE_FILE_SIZE, OFFER_IMAGES_LENGTH, ), ], diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 5df9d17..4cd416c 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -2,9 +2,11 @@ import { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; import { inject, injectable } from 'inversify'; import { + ALLOWED_IMAGES, BaseController, HttpError, HttpMethod, + MAX_SINGLE_FILE_SIZE, UploadFileMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware, @@ -61,6 +63,8 @@ export class UserController extends BaseController { new UploadFileMiddleware( this.configService.get('UPLOAD_DIRECTORY'), 'avatar', + ALLOWED_IMAGES, + MAX_SINGLE_FILE_SIZE, ), ], });