diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..55541f1 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +PORT=4000 +SALT=secret +DB_HOST=127.0.0.1 +DB_USER=admin +DB_PASSWORD=test +DB_NAME=six-cities +DB_MAX_RETRIES=3 +DB_RETRY_TIMEOUT=1000 +UPLOAD_DIRECTORY=upload +JWT_SECRET=super-secret! diff --git a/Workflow.md b/Workflow.md index 4977046..393d8ec 100644 --- a/Workflow.md +++ b/Workflow.md @@ -1,16 +1,82 @@ # Как работать над проектом -## Окружение +## Запуск проекта -Для удобства работы над проектом используются инструменты из **Node.js** и **npm**. Все необходимые настройки произведены. Убедитесь, что на рабочем компьютере установлен актуальный LTS релиз Node.js**. Актуальная версия **Node.js** указана в файле `package.json` в поле `node`. Затем, в терминале, перейдите в директорию с проектом и _единожды_ запустите команду: +Убедитесь, что на рабочем компьютере установлен актуальный LTS релиз **Node.js**. Актуальная версия **Node.js** указана в файле `package.json` в поле `node`. + +### Установите зависимости + +Выполните команду ```bash npm install ``` -Команда запустит процесс установки зависимостей проекта из **npm**. +### Разверните базу данных MongoDB + +В проекте используется база данных **MongoDB** версии 4.2. + +Если вы используете Docker, воспользуйтесь файлом `docker-compose.dev.yml`, расположенным в корне проекта, для быстрого разворачивания базы данных. + +### Наполните базу данных + +Запустите JSON-сервер с моковыми данными: + +```bash +npm run mock:server +``` + +Сгенерируйте нужное количество предложений об аренде: + +```bash +npm run cli -- --generate 100 ./mocks/mock-data.tsv http://localhost:3123/api +``` + +Импортируйте данные в базу данных, используя настройки подключения из файла `docker-compose`: + +```bash +npm run cli -- --import ./mocks/mock-data.tsv admin test 127.0.0.1 six-cities secret +``` + +### Настройте переменные окружения + +В корне проекта находится файл `.env.example`. Скопируйте его содержимое в файл `.env` и настройте значения переменных. + +#### Переменные окружения + +```bash +PORT=4000 - Порт для входящих соединений -### Сценарии +SALT=secret - Соль для хэширования паролей + +DB_HOST=127.0.0.1 - IP-адрес сервера базы данных (MongoDB) + +DB_USER=admin - Имя пользователя для подключения к базе данных + +DB_PASSWORD=test - Пароль для подключения к базе данных + +DB_PORT=27017 - Порт для подключения к базе данных (MongoDB) + +DB_NAME=six-cities - Имя базы данных (MongoDB) + +DB_MAX_RETRIES=3 - Максильмальное число попыток переподключения к БД + +DB_RETRY_TIMEOUT=1000 - Число миллисекунд между попытками подключения к БД + +UPLOAD_DIRECTORY=upload - Директория для хранения загружаемых пользователями файлов + +JWT_SECRET=super-secret! - Секрет для подписи JWT-токена +``` + +### Запустите проект + +Выполните команду + +```bash +npm start +``` + +## Сценарии В `package.json` предопределено несколько сценариев. @@ -66,25 +132,24 @@ npm start В процессе запуска проекта будет выполнен процесс «Сборки проекта» и запуска результирующего кода. -#### Запустить JSON-server с моковыми данными +#### Запустить проект в режиме разработки ```bash -npm run mock:server +npm run start:dev ``` -#### Запустить генерацию моковых данных +#### Запустить JSON-server с моковыми данными ```bash -npm run ts ./src/main.cli.ts -- --generate 100 ./mocks/mock-data.tsv http://localhost:3123/api +npm run mock:server ``` -#### Наполнить базу моковыми данными +#### Запустить консольную утилиту ```bash -npm run ts ./src/main.cli.ts -- --import ./mocks/mock-data.tsv admin test 127.0.0.1 six-cities secret +npm run cli ``` - ## Структура проекта ### Директория `src` diff --git a/package.json b/package.json index be3d408..c0ccbb7 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "compile": "tsc -p tsconfig.json", "clean": "rimraf dist", "ts": "ts-node", - "mock:server": "json-server ./mocks/mock-server-data.json --port 3123" + "mock:server": "json-server ./mocks/mock-server-data.json --port 3123", + "cli": "ts-node ./src/main.cli.ts" }, "devDependencies": { "@types/convict": "6.1.4", diff --git a/specification/project.spec.yml b/specification/project.spec.yml index 1099561..8c04734 100644 --- a/specification/project.spec.yml +++ b/specification/project.spec.yml @@ -11,6 +11,7 @@ tags: - name: offers - name: users - name: comments + - name: cities paths: /users/register: @@ -38,9 +39,13 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/validationError' + $ref: '#/components/schemas/validationError' + '403': + description: A logged in user can not register. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' '409': description: A user with this email already exists. content: @@ -59,7 +64,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/authorizeUser' + $ref: '#/components/schemas/loginUser' required: true responses: '200': @@ -68,8 +73,24 @@ paths: text/plain: schema: type: string + '400': + description: Some of the provided information is not correct. + content: + application/json: + schema: + $ref: '#/components/schemas/validationError' '401': + description: A user is not authorized. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' + '404': description: A user with specified credentials was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' get: tags: @@ -85,8 +106,12 @@ paths: $ref: '#/components/schemas/user' '401': description: User session token is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' - /users/{usedId}/avatar: + /users/avatar: post: tags: - users @@ -95,6 +120,72 @@ paths: responses: '201': description: Image has been uploaded. + content: + application/json: + schema: + type: object + properties: + filepath: + type: string + example: + upload/Uq-PzHXRZnm6NzYvfjFGA.png + '401': + description: A user is not authorized to upload an avatar. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' + + /users/favorites: + get: + tags: + - users + - offers + summary: List of favorite offers + description: Returns all the offers that have been added to favorites list by a current user. + responses: + '200': + description: List of offers. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/offerPreview' + '401': + description: User is not authorized to view favorite offers. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' + + put: + tags: + - users + - offers + summary: Favorite the offer + description: Adds/removes an offer to/from the favorites list. + requestBody: + description: Information to mark an offer as favorite. + content: + application/json: + schema: + $ref: '#/components/schemas/favoriteOffer' + responses: + '204': + description: Offer's `favorite` status has been successfully updated + '400': + description: Some of the provided information is not correct. + content: + application/json: + schema: + $ref: '#/components/schemas/validationError' + '401': + description: User is not authorized to favorite offers. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' /offers: get: @@ -143,11 +234,13 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/validationError' + $ref: '#/components/schemas/validationError' '401': description: User is not authorized to create offers. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' /offers/{offerId}: get: @@ -169,8 +262,18 @@ paths: application/json: schema: $ref: '#/components/schemas/offer' + '400': + description: Some of the provided information is not correct. + content: + application/json: + schema: + $ref: '#/components/schemas/validationError' '404': description: Offer with the given ID does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' patch: tags: @@ -203,15 +306,25 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/validationError' + $ref: '#/components/schemas/validationError' '401': description: User is not authorized to edit offers. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' '403': description: User is not authorized to edit this offer. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' '404': description: Offer with the given ID does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' delete: tags: @@ -228,12 +341,30 @@ paths: responses: '204': description: Offer has been successfully removed. + '400': + description: offerId is not correct. + content: + application/json: + schema: + $ref: '#/components/schemas/validationError' '401': description: User is not authorized to delete offers. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' '403': description: User is not authorized to delete this offer. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' '404': description: Offer with the given ID does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' /offers/{offerId}/comments: get: @@ -258,65 +389,73 @@ paths: type: array items: $ref: '#/components/schemas/comment' + '400': + description: offerId is not correct. + content: + application/json: + schema: + $ref: '#/components/schemas/validationError' '404': description: Offer with the given ID does not exist. - - /offers/favorites: - get: - tags: - - offers - summary: List of favorite offers - description: Returns all the offers that have been added to favorites list by a current user. - responses: - '200': - description: List of offers. content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/offerPreview' - '401': - description: User is not authorized to view favorite offers. + $ref: '#/components/schemas/generalError' - /offers/{offerId}/favorite: - put: + /comments: + post: tags: + - comments - offers - summary: Favorite the offer - description: Adds/removes an offer to/from the favorites list. - parameters: - - in: path - name: offerId - schema: - type: string - required: true - description: The ID of the offer. + summary: Comment creation + description: Adds a comments to a given offer. requestBody: + description: Information to create a new comment. content: - text/plain: + application/json: schema: - type: boolean + $ref: '#/components/schemas/createComment' + required: true responses: - '204': - description: Offer's `favorite` status has been successfully updated + '201': + description: Comments has been created. Comment object. + content: + application/json: + schema: + $ref: '#/components/schemas/comment' + '400': + description: Some of the provided information is not correct. + content: + application/json: + schema: + $ref: '#/components/schemas/validationError' '401': - description: User is not authorized to favorite offers. + description: User is not authorized to add comments. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' + '404': + description: Offer with the given ID does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/generalError' - /offers/premium: + /cities/:cityId/premium: get: tags: + - cities - offers summary: List of premium offers description: Returns a list of premium offers for a given city. parameters: - - in: query - name: city + - in: path + name: cityId schema: type: string required: true description: The city to get the premium offers for. - example: Paris responses: '200': description: List of premium offers. @@ -324,42 +463,20 @@ paths: application/json: schema: type: array - maxItems: 3 items: $ref: '#/components/schemas/offerPreview' - - /comments: - post: - tags: - - comments - summary: Comment creation - description: Adds a comments to a given offer. - requestBody: - description: Information to create a new comment. - content: - application/json: - schema: - $ref: '#/components/schemas/createComment' - required: true - responses: - '201': - description: Comments has been created. Comment object. + '400': + description: cityId is not correct. content: application/json: schema: - $ref: '#/components/schemas/comment' - '400': - description: Some of the provided information is not correct. + $ref: '#/components/schemas/validationError' + '404': + description: City with the given ID does not exist. content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/validationError' - '401': - description: User is not authorized to add comments. - '404': - description: Offer with the given ID does not exist. + $ref: '#/components/schemas/generalError' components: schemas: @@ -369,23 +486,24 @@ components: error: type: string description: A description of the error. - example: User already exists + example: Error description validationError: type: object properties: - message: - type: string - description: A description of the validation error. - example: Password too short - field: - type: string - description: The name of the field that failed validation. - example: password - code: - type: string - description: An error code associated with the validation error. - example: validation.createUser.password.tooShort + errors: + type: array + items: + type: object + properties: + field: + type: string + description: The name of the field that failed validation. + example: password + message: + type: string + description: A description of the validation error. + example: Password too short createUser: type: object @@ -395,6 +513,10 @@ components: format: email required: true example: keks@htmlacademy.ru + avatarPath: + type: string + required: false + example: upload/ketUeXWH-AxExEduf3lSp.png name: type: string required: true @@ -403,7 +525,7 @@ components: type: string format: password required: true - example: 123456 + example: 12345678 type: type: string enum: @@ -411,36 +533,31 @@ components: - Pro required: true example: Pro - avatarPath: - type: string - required: false - example: https://s3.amazonaws.com/uifaces/faces/twitter/hugocornejo/128.jpg - authorizeUser: + loginUser: type: object properties: login: type: string + required: true example: keks@htmlacademy.ru password: type: string - example: 123456 + required: true + example: 12345678 user: type: object properties: - id: - type: string - example: 6329c3d6a04ab1061c6425ea email: type: string example: keks@htmlacademy.ru + avatarPath: + type: string + example: upload/ketUeXWH-AxExEduf3lSp.png name: type: string example: Keks - avatarPath: - type: string - example: https://s3.amazonaws.com/uifaces/faces/twitter/hugocornejo/128.jpg type: type: string enum: @@ -448,6 +565,18 @@ components: - Pro example: Pro + favoriteOffer: + type: object + properties: + offerId: + type: string + required: true + example: 6329c3d6a04ab1061c6425ea + isFavorite: + type: boolean + required: true + example: true + createOffer: type: object properties: @@ -459,17 +588,11 @@ components: type: string required: true example: A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam. - city: + postDate: type: string - enum: - - Amsterdam - - Brussels - - Cologne - - Dusseldorf - - Hamburg - - Paris + format: date-time required: true - example: Amsterdam + example: "2023-01-30T08:30:00Z" imagePreview: type: string required: true @@ -487,14 +610,7 @@ components: required: true example: true housingType: - type: string - enum: - - Apartment - - House - - Room - - Hotel - required: true - example: Apartment + $ref: '#/components/schemas/housingType' roomAmount: type: integer required: true @@ -538,6 +654,10 @@ components: type: string example: 6329c3d6a04ab1061c6425ea required: true + cityId: + type: string + example: 6329c3d6a04ab1061c6425ea + required: true updateOffer: type: object @@ -545,26 +665,12 @@ components: title: type: string example: Beautiful & luxurious studio at great location - required: false description: type: string example: A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam. - required: false - city: - type: string - enum: - - Amsterdam - - Brussels - - Cologne - - Dusseldorf - - Hamburg - - Paris - example: Amsterdam - required: false imagePreview: type: string example: https://s3.amasonaws.com/image-preview.png - required: false images: type: array minItems: 6 @@ -572,63 +678,35 @@ components: items: type: string example: https://s3.amasonaws.com/full-image.png - required: false isPremium: type: boolean example: true - required: false housingType: - type: string - enum: - - Apartment - - House - - Room - - Hotel - example: Apartment - required: false + $ref: '#/components/schemas/housingType' roomAmount: type: integer example: 3 - required: false guestAmount: type: integer example: 5 - required: false price: type: number format: float example: 100 - required: false amenities: - type: array - items: - type: string - enum: - - Breakfast - - AirConditioning - - LaptopFriendlyWorkspace - - BabySeat - - Washer - - Towels - - Fridge - example: Fridge - required: false + $ref: '#/components/schemas/amenities' location: - type: object - properties: - latitude: - type: number - format: float - example: 48.85661 - longitude: - type: number - format: float - example: 2.351499 - required: false + $ref: '#/components/schemas/location' + cityId: + type: string + example: 6329c3d6a04ab1061c6425ea offer: type: object properties: + id: + type: string + example: 6329c3d6a04ab1061c6425ea title: type: string example: Beautiful & luxurious studio at great location @@ -640,15 +718,7 @@ components: format: date-time example: 2023-10-11T13:15:56.868Z city: - type: string - enum: - - Amsterdam - - Brussels - - Cologne - - Dusseldorf - - Hamburg - - Paris - example: Amsterdam + $ref: '#/components/schemas/city' imagePreview: type: string example: https://s3.amasonaws.com/image-preview.png @@ -670,13 +740,7 @@ components: format: float example: 4.2 housingType: - type: string - enum: - - Apartment - - House - - Room - - Hotel - example: Apartment + $ref: '#/components/schemas/housingType' roomAmount: type: integer example: 3 @@ -688,29 +752,9 @@ components: format: float example: 100 amenities: - type: array - items: - type: string - enum: - - Breakfast - - AirConditioning - - LaptopFriendlyWorkspace - - BabySeat - - Washer - - Towels - - Fridge - example: Fridge + $ref: '#/components/schemas/amenities' location: - type: object - properties: - latitude: - type: number - format: float - example: 48.85661 - longitude: - type: number - format: float - example: 2.351499 + $ref: '#/components/schemas/location' commentAmount: type: integer example: 5 @@ -723,48 +767,34 @@ components: id: type: string example: 6329c3d6a04ab1061c6425ea - price: - type: number - format: float - example: 100 title: type: string example: Beautiful & luxurious studio at great location housingType: - type: string - enum: - - Apartment - - House - - Room - - Hotel - example: Apartment - isFavorite: - type: boolean - example: true + $ref: '#/components/schemas/housingType' postDate: type: string format: date-time example: 2023-10-11T13:15:56.868Z city: - type: string - enum: - - Amsterdam - - Brussels - - Cologne - - Dusseldorf - - Hamburg - - Paris - example: Amsterdam + $ref: '#/components/schemas/city' imagePreview: type: string example: https://s3.amasonaws.com/image-preview.png isPremium: type: boolean example: false + isFavorite: + type: boolean + example: true rating: type: number format: float example: 4.2 + price: + type: number + format: float + example: 100 commentAmount: type: integer example: 5 @@ -774,8 +804,6 @@ components: properties: text: type: string - minLength: 5 - maxLength: 1024 required: true rating: type: integer @@ -785,10 +813,6 @@ components: type: string example: 6329c3d6a04ab1061c6425ea required: true - authorId: - type: string - example: 6329c3d6a04ab1061c6425ea - required: true comment: type: object @@ -799,12 +823,59 @@ components: text: type: string example: Nice property! + rating: + type: integer + example: 4 postDate: type: string format: date-time example: 2023-10-11T13:15:56.868Z - rating: - type: integer - example: 4 author: $ref: '#/components/schemas/user' + + amenities: + type: array + items: + type: string + enum: + - Breakfast + - AirConditioning + - LaptopFriendlyWorkspace + - BabySeat + - Washer + - Towels + - Fridge + example: Fridge + + housingType: + type: string + enum: + - Apartment + - House + - Room + - Hotel + example: Apartment + + location: + type: object + properties: + latitude: + type: number + format: float + example: 48.85661 + longitude: + type: number + format: float + example: 2.351499 + + city: + type: object + properties: + id: + type: string + example: 6329c3d6a04ab1061c6425ea + name: + type: string + example: Amsterdam + location: + $ref: '#/components/schemas/location' diff --git a/src/cli/commands/help.command.ts b/src/cli/commands/help.command.ts index ee4445c..d0cd573 100644 --- a/src/cli/commands/help.command.ts +++ b/src/cli/commands/help.command.ts @@ -10,20 +10,16 @@ export class HelpCommand implements Command { console.info(`Программа для подготовки данных для REST API сервера. ${chalk.bold('ПРИМЕР')} - ${chalk.bold('cli.js')} --${chalk.underline( - 'command', -)} [...${chalk.underline('arguments')}] + ${chalk.bold('npm run cli')} -- --${chalk.underline('command')} [...${chalk.underline('arguments')}] ${chalk.bold('КОМАНДЫ')} ${chalk.bold('--version')} Выводит номер версии ${chalk.bold('--help')} Печатает этот текст - ${chalk.bold('--import')} ${chalk.underline('path')} + ${chalk.bold('--import')} ${chalk.underline('path')} ${chalk.underline('db_login')} ${chalk.underline('db_pass')} ${chalk.underline('db_host')} ${chalk.underline('db_name')} ${chalk.underline('salt')} Импортирует данные из TSV - ${chalk.bold('--generate')} ${chalk.underline('n')} ${chalk.underline( - 'path', -)} ${chalk.underline('url')} + ${chalk.bold('--generate')} ${chalk.underline('n')} ${chalk.underline('path')} ${chalk.underline('url')} Генерирует произвольное количество тестовых данных `); } diff --git a/src/shared/helpers/parse.ts b/src/shared/helpers/parse.ts index f45bc69..742c3f5 100644 --- a/src/shared/helpers/parse.ts +++ b/src/shared/helpers/parse.ts @@ -7,7 +7,3 @@ export function parseAsInteger(param: unknown): number | null { return Number.isInteger(parsedParam) ? parsedParam : null; } - -export function parseAsString(param: unknown): string | null { - return typeof param === 'string' ? param : null; -} diff --git a/src/shared/libs/rest/middleware/validate-dto.middleware.ts b/src/shared/libs/rest/middleware/validate-dto.middleware.ts index 8915ebd..3250753 100644 --- a/src/shared/libs/rest/middleware/validate-dto.middleware.ts +++ b/src/shared/libs/rest/middleware/validate-dto.middleware.ts @@ -16,7 +16,12 @@ export class ValidateDtoMiddleware implements Middleware { const errors = await validate(dtoInstance); if (errors.length > 0) { - res.status(StatusCodes.BAD_REQUEST).send(errors); + res.status(StatusCodes.BAD_REQUEST).send({ + errors: errors.map((error) => ({ + field: error.property, + message: Object.values(error.constraints ?? {}).join(';') + })) + }); return; } diff --git a/src/shared/modules/auth/auth.exception-filter.ts b/src/shared/modules/auth/auth.exception-filter.ts index 771dd03..78c5193 100644 --- a/src/shared/modules/auth/auth.exception-filter.ts +++ b/src/shared/modules/auth/auth.exception-filter.ts @@ -4,6 +4,7 @@ import { ExceptionFilter } from '../../libs/rest/index.js'; import { Logger } from '../../libs/logger/index.js'; import { Component } from '../../types/index.js'; import { BaseUserException } from './errors/index.js'; +import { createErrorObject } from '../../helpers/index.js'; @injectable() export class AuthExceptionFilter implements ExceptionFilter { @@ -22,9 +23,6 @@ export class AuthExceptionFilter implements ExceptionFilter { } this.logger.error(`[AuthModule] ${error.message}`, error); - res.status(error.httpStatusCode).json({ - type: 'AUTHORIZATION', - error: error.message, - }); + res.status(error.httpStatusCode).json(createErrorObject(error.message)); } } diff --git a/src/shared/modules/user/rdo/logged-user.rdo.ts b/src/shared/modules/user/rdo/logged-user.rdo.ts deleted file mode 100644 index 543bd7d..0000000 --- a/src/shared/modules/user/rdo/logged-user.rdo.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Expose } from 'class-transformer'; - -export class LoggedUserRdo { - @Expose() - public token: string; - - @Expose() - public email: string; -} diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 7394955..0cf1c1d 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -19,7 +19,6 @@ import { UserService } from './user-service.interface.js'; import { CreateUserRequest } from './types/create-user-request.type.js'; import { LoginUserRequest } from './types/login-user-request.type.js'; import { UserRdo } from './rdo/user.rdo.js'; -import { LoggedUserRdo } from './rdo/logged-user.rdo.js'; import { CreateUserDto } from './dto/create-user.dto.js'; import { LoginUserDto } from './dto/login-user.dto.js'; import { FavoriteOfferDto } from './dto/favorite-offer.dto.js'; @@ -118,11 +117,7 @@ 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); + this.ok(res, token); } public async checkToken(