Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Сервер наносит ответный удар #8

Merged
merged 14 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
543 changes: 351 additions & 192 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"devDependencies": {
"@types/convict": "6.1.4",
"@types/convict-format-with-validator": "6.0.3",
"@types/express": "4.17.18",
"@types/node": "18.17.15",
"@typescript-eslint/eslint-plugin": "6.7.0",
"@typescript-eslint/parser": "6.7.0",
Expand All @@ -43,10 +44,14 @@
},
"dependencies": {
"@typegoose/typegoose": "11.5.0",
"class-transformer": "0.5.1",
"convict": "6.2.4",
"convict-format-with-validator": "6.2.0",
"dotenv": "16.3.1",
"express": "4.18.2",
"express-async-handler": "1.2.0",
"got": "13.0.0",
"http-status-codes": "2.3.0",
"inversify": "6.0.1",
"mongoose": "7.5.3",
"pino": "8.15.1",
Expand Down
2 changes: 1 addition & 1 deletion specification/specification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ paths:
description: Предложение удалено.

/offers/premium:
post:
get:
tags:
- offers
summary: Список премиальных предложений
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/import.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export class ImportCommand implements Command {
hostId: user.id,
bedrooms: offer.bedrooms,
maxAdults: offer.maxAdults,
commentCount: offer.commentCount
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В shared/helpers/offer.ts ts ругается из-за того, что в типизации не почистил следом. Нужно до-урегулировать отличия. В конце концов можно вроде изначально прокидывать 0, потом пусть пересчитывается

});

}
Expand Down
53 changes: 44 additions & 9 deletions src/rest/rest.application.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import {inject, injectable} from 'inversify';
import express, {Express} from 'express';

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 {CommentService} from '../shared/modules/comment/index.js';
import {Controller, ExceptionFilter} from '../shared/libs/rest/index.js';

@injectable()
export class RestApplication {
private readonly server: Express;

constructor(
@inject(Component.Logger) private readonly logger: Logger,
@inject(Component.Config) private readonly config: Config<RestSchema>,
@inject(Component.DatabaseClient) private readonly databaseClient: DatabaseClient,
@inject(Component.CommentService) private readonly commentService: CommentService,
@inject(Component.CommentController) private readonly commentController: Controller,
@inject(Component.ExceptionFilter) private readonly appExceptionFilter: ExceptionFilter,
@inject(Component.UserController) private readonly userController: Controller,
@inject(Component.OfferController) private readonly offerController: Controller,

) {
this.server = express();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Express по умолчанию добавляет во все ответы заголовок X-Powered-By: Express обозначающий, какой сервер отвечает на запросы. И конечно же не следует предоставлять в публичных api неполезную (с точки зрения бизнес логики и пользователя) информацию. С точки зрения безопасности это может быть важный момент. Так как мы резко уменьшаем коридор поиска лазеек для потенциального злоумышленника, так как сразу говорим ему какой у нас веб-сервер.
Для того, чтобы отключить этот заголовок, следует сразу после создания инстанса express'а вызвать команду, думаю, что ты можешь это сделать прямо в конструкторе:

this.server = express();
this.server.disable('x-powered-by');

}

private async _initDb() {
Expand All @@ -29,6 +37,25 @@ export class RestApplication {
return this.databaseClient.connect(mongoUri);
}

private async _initServer() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_initServer название не отражает действительность. Всё-таки внутри этого метода происходит запуск сервера, а инициализация/создание в конструкторе класса. Я бы предложил название _startServer

const port = this.config.get('PORT');
this.server.listen(port);
}

private async _initControllers() {
this.server.use('/comments', this.commentController.router);
this.server.use('/users', this.userController.router);
this.server.use('/offers', this.offerController.router);
}

private async _initMiddleware() {
this.server.use(express.json());
}

private async _initExceptionFilters() {
this.server.use(this.appExceptionFilter.catch.bind(this.appExceptionFilter));
}

public async init() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В стартовых логах в терминале начинает сложно ориентироваться. Можно подумать о том, чтобы сообщение-заголовок группы, например Init controllers, перекрасить в отдельный цвет

this.logger.info('Application initialization');
this.logger.info(`Get value from env $PORT: ${this.config.get('PORT')}`);
Expand All @@ -37,12 +64,20 @@ export class RestApplication {
await this._initDb();
this.logger.info('Init database completed');

const rrr = await this.commentService.create({
text: 'fdfdfd',
rating: 3,
offerId: '66e554ee0c19a0f76db1b955',
authorId: '66e554ee0c19a0f76db1b953'
});
console.log(rrr);
this.logger.info('Init app-level middleware');
await this._initMiddleware();
this.logger.info('App-level middleware initialization completed');

this.logger.info('Init controllers');
await this._initControllers();
this.logger.info('Controller initialization completed');

this.logger.info('Init exception filters');
await this._initExceptionFilters();
this.logger.info('Exception filters initialization completed');

this.logger.info('Try to init server...');
await this._initServer();
this.logger.info(`Server started on http://localhost:${this.config.get('PORT')}`);
}
}
2 changes: 2 additions & 0 deletions src/rest/rest.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Component} from '../shared/types/index.js';
import {Logger, PinoLogger} from '../shared/libs/logger/index.js';
import {Config, RestConfig, RestSchema} from '../shared/libs/config/index.js';
import {DatabaseClient, MongoDatabaseClient} from '../shared/libs/database-client/index.js';
import {AppExceptionFilter, ExceptionFilter} from '../shared/libs/rest/index.js';

export function createRestApplicationContainer() {
const restApplicationContainer = new Container();
Expand All @@ -12,6 +13,7 @@ export function createRestApplicationContainer() {
restApplicationContainer.bind<Logger>(Component.Logger).to(PinoLogger).inSingletonScope();
restApplicationContainer.bind<Config<RestSchema>>(Component.Config).to(RestConfig).inSingletonScope();
restApplicationContainer.bind<DatabaseClient>(Component.DatabaseClient).to(MongoDatabaseClient).inSingletonScope();
restApplicationContainer.bind<ExceptionFilter>(Component.ExceptionFilter).to(AppExceptionFilter).inSingletonScope();

return restApplicationContainer;
}
3 changes: 3 additions & 0 deletions src/shared/const/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export const HOTEL_IMAGE_PATH = 'https://13.design.htmlacademy.pro/static/hotel/
export const HOST_AVATAR_PATH = 'https://13.design.htmlacademy.pro/static/avatar/';
export const MAX_IMAGES_AMOUNT = 20;
export const MAX_IMAGES_ARRAY_LENGTH = 6;
export const MAX_PREMIUM_OFFERS = 3;
export const MAX_COMMENTS_AMOUNT = 50;
export const DEFAULT_OFFERS_COUNT = 60;
11 changes: 11 additions & 0 deletions src/shared/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {HOST_AVATAR_PATH, HOTEL_IMAGE_PATH, MAX_IMAGES_AMOUNT, MAX_IMAGES_ARRAY_LENGTH} from '../const/index.js';
import {ClassConstructor, plainToInstance} from 'class-transformer';

export function generateRandomValue(min: number, max: number, numAfterDigit = 0) {
return +((Math.random() * (max - min)) + min).toFixed(numAfterDigit);
Expand Down Expand Up @@ -42,3 +43,13 @@ export function generateImagesPaths() {
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : '';
}

export function fillDTO<T, V>(someDto: ClassConstructor<T>, plainObject: V) {
return plainToInstance(someDto, plainObject, { excludeExtraneousValues: true });
}

export function createErrorObject(message: string) {
return {
error: message,
};
}
49 changes: 49 additions & 0 deletions src/shared/libs/rest/controller/base-controller.abstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { injectable } from 'inversify';
import asyncHandler from 'express-async-handler';
import { StatusCodes } from 'http-status-codes';
import { Response, Router } from 'express';
import { Controller } from './controller.interface.js';
import { Logger } from '../../logger/index.js';
import { Route } from '../types/route.interface.js';

const DEFAULT_CONTENT_TYPE = 'application/json';

@injectable()
export abstract class BaseController implements Controller {
private readonly _router: Router;

constructor(
protected readonly logger: Logger
) {
this._router = Router();
}

get router() {
return this._router;
}

public addRoute(route: Route) {
const wrapperAsyncHandler = asyncHandler(route.handler.bind(this));
this._router[route.method](route.path, wrapperAsyncHandler);
this.logger.info(`Route registered: ${route.method.toUpperCase()} ${route.path}`);
}

public send<T>(res: Response, statusCode: number, data: T): void {
res
.type(DEFAULT_CONTENT_TYPE)
.status(statusCode)
.json(data);
}

public created<T>(res: Response, data: T): void {
this.send(res, StatusCodes.CREATED, data);
}

public noContent<T>(res: Response, data: T): void {
this.send(res, StatusCodes.NO_CONTENT, data);
}

public ok<T>(res: Response, data: T): void {
this.send(res, StatusCodes.OK, data);
}
}
11 changes: 11 additions & 0 deletions src/shared/libs/rest/controller/controller.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Response, Router } from 'express';
import { Route } from '../types/route.interface.js';

export interface Controller {
readonly router: Router;
addRoute(route: Route): void;
send<T>(res: Response, statusCode: number, data: T): void;
ok<T>(res: Response, data: T): void;
created<T>(res: Response, data: T): void;
noContent<T>(res: Response, data: T): void;
}
12 changes: 12 additions & 0 deletions src/shared/libs/rest/errors/http-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class HttpError extends Error {
public httpStatusCode!: number;
public detail?: string;

constructor(httpStatusCode: number, message: string, detail?: string) {
super(message);

this.httpStatusCode = httpStatusCode;
this.message = message;
this.detail = detail;
}
}
1 change: 1 addition & 0 deletions src/shared/libs/rest/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './http-error.js';
38 changes: 38 additions & 0 deletions src/shared/libs/rest/exception-filter/app-exception-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {inject, injectable} from 'inversify';
import {StatusCodes} from 'http-status-codes';
import {NextFunction, Request, Response} from 'express';
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';

@injectable()
export class AppExceptionFilter implements ExceptionFilter {
constructor(
@inject(Component.Logger) private readonly logger: Logger
) {
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) {
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextFunction, Request, Response } from 'express';

export interface ExceptionFilter {
catch(error: Error, req: Request, res: Response, next:NextFunction): void;
}
9 changes: 9 additions & 0 deletions src/shared/libs/rest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './types/http-method.enum.js';
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 './types/request.params.type.js';
export * from './types/request-body.type.js';
export * from './errors/index.js';
7 changes: 7 additions & 0 deletions src/shared/libs/rest/types/http-method.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum HttpMethod {
Get = 'get',
Post = 'post',
Delete = 'delete',
Patch = 'patch',
Put = 'put',
}
1 change: 1 addition & 0 deletions src/shared/libs/rest/types/request-body.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type RequestBody = Record<string, unknown>;
1 change: 1 addition & 0 deletions src/shared/libs/rest/types/request.params.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type RequestParams = Record<string, unknown>;
8 changes: 8 additions & 0 deletions src/shared/libs/rest/types/route.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NextFunction, Request, Response } from 'express';
import { HttpMethod } from './http-method.enum.js';

export interface Route {
path: string;
method: HttpMethod;
handler: (req: Request, res: Response, next: NextFunction) => void;
}
11 changes: 7 additions & 4 deletions src/shared/modules/comment/comment.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {Component} from '../../types/index.js';
import {CommentService} from './comment-service.interface.js';
import {DefaultCommentService} from './default-comment.service.js';
import {CommentEntity, CommentModel} from './comment.entity.js';
import {CommentController} from './comment.controller.js';
import {Controller} from '../../libs/rest/index.js';

export function createCommentContainer() {
const offerContainer = new Container();
offerContainer.bind<CommentService>(Component.OfferService).to(DefaultCommentService).inSingletonScope();
offerContainer.bind<types.ModelType<CommentEntity>>(Component.CommentService).toConstantValue(CommentModel);
const commentContainer = new Container();
commentContainer.bind<CommentService>(Component.CommentService).to(DefaultCommentService).inSingletonScope();
commentContainer.bind<types.ModelType<CommentEntity>>(Component.CommentModel).toConstantValue(CommentModel);
commentContainer.bind<Controller>(Component.CommentController).to(CommentController).inSingletonScope();

return offerContainer;
return commentContainer;
}
38 changes: 38 additions & 0 deletions src/shared/modules/comment/comment.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {inject, injectable} from 'inversify';
import {Request, Response} from 'express';
import {BaseController, HttpMethod} from '../../libs/rest/index.js';
import {Logger} from '../../libs/logger/index.js';
import {Component} from '../../types/index.js';
import {CommentService} from './comment-service.interface.js';
import {CommentRdo} from './rdo/comment.rdo.js';
import {fillDTO} from '../../helpers/index.js';
import {CreateCommentDto} from './dto/create-comment.dto.js';

@injectable()
export class CommentController extends BaseController {
constructor(
@inject(Component.Logger) protected readonly logger: Logger,
@inject(Component.CommentService) private readonly commentService: CommentService,
) {
super(logger);

this.logger.info('Register routes for CommentController…');

this.addRoute({path: '/:offerId', method: HttpMethod.Get, handler: this.index});
this.addRoute({path: '/:offerId', method: HttpMethod.Post, handler: this.create});
}

public async index(req: Request, res: Response): Promise<void> {
const comments = await this.commentService.findByOfferId(req.params.id);
const responseData = fillDTO(CommentRdo, comments);
this.ok(res, responseData);
}

public async create(
{body}: Request<Record<string, unknown>, Record<string, unknown>, CreateCommentDto>,
res: Response
): Promise<void> {
const comment = await this.commentService.create(body);
this.created(res, fillDTO(CommentRdo, comment));
}
}
9 changes: 9 additions & 0 deletions src/shared/modules/comment/comment.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
POST http://localhost:4000/comments/6709458a0ba2080e9b726af3 HTTP/1.1
Content-Type: application/json

{
"text": "test",
"rating": "1",
"authorId": "6709458a0ba2080e9b726af1",
"offerId": "6717e369c934030b401bf05e"
}
Loading
Loading