diff --git a/.env.example b/.env.example index ce6a3bb..e897a06 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,14 @@ -PORT=5000 +PORT=3000 SALT=secret -DB_USER = test -DB_PASSWORD = test +ME_USER=test +ME_PASSWORD=test +MONGO_EXPRESS_PORT=8081 +DB_USER=test +DB_PASSWORD=test DB_HOST=127.0.0.2 DB_PORT=27017 -DB_NAME=test_db -JWT_SECRET = 'secret key' +DB_NAME=db +JWT_SECRET='secret key' UPLOAD_FILE_DIRECTORY='\src\uploads' HOST='localhost' STATIC_DIRECTORY_PATH = '\src\static' \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index f119e27..4145b7a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,12 @@ -version: '3.9' - +# docker-compose.yaml services: db: image: mongo:4.2 restart: always container_name: six-cities-mongodb environment: - MONGO_INITDB_ROOT_USERNAME: ${DB_USER:-admin} - MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-test} + MONGO_INITDB_ROOT_USERNAME: ${DB_USER} + MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD} ports: - "${DB_PORT:-27017}:27017" volumes: @@ -20,17 +19,21 @@ services: restart: always container_name: six-cities-mongo-express ports: - - "${MONGO_EXPRESS_PORT:-8081}:8081" + - "${MONGO_EXPRESS_PORT}:8081" environment: - ME_CONFIG_BASICAUTH_USERNAME: ${ME_USER:-admin} - ME_CONFIG_BASICAUTH_PASSWORD: ${ME_PASSWORD:-test} - ME_CONFIG_MONGODB_ADMINUSERNAME: ${DB_USER:-admin} - ME_CONFIG_MONGODB_ADMINPASSWORD: ${DB_PASSWORD:-test} - ME_CONFIG_MONGODB_URL: mongodb://${DB_USER:-admin}:${DB_PASSWORD:-test}@db:27017/ + ME_CONFIG_BASICAUTH_USERNAME: ${ME_USER} + ME_CONFIG_BASICAUTH_PASSWORD: ${ME_PASSWORD} + ME_CONFIG_MONGODB_ADMINUSERNAME: ${DB_USER} + ME_CONFIG_MONGODB_ADMINPASSWORD: ${DB_PASSWORD} + ME_CONFIG_MONGODB_URL: mongodb://${DB_USER}:${DB_PASSWORD}@db:27017/ depends_on: - db networks: - app-network +networks: + app-network: + driver: bridge + volumes: - six-cities_data: + six-cities_data: \ No newline at end of file diff --git a/package.json b/package.json index b41e93b..3b1e621 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "dev:run": "node dist/main.rest.js", "compile": "tsc -p tsconfig.json", "clean": "rimraf dist", - "cli": "tsx ./src/cli.ts", + "cli": "node --loader ts-node/esm ./src/cli.ts", + "cli:generate": "node --loader ts-node/esm ./src/cli.ts --generate", + "cli:import": "node --loader ts-node/esm ./src/cli.ts --import", + "cli:version": "node --loader ts-node/esm ./src/cli.ts --version", "ts": "ts-node", "mock:server": "json-server ./src/mock/mock-server-data.json --port 3123", "prepare": "husky" @@ -68,4 +71,4 @@ "pino": "^10.3.1", "reflect-metadata": "^0.2.2" } -} +} \ No newline at end of file diff --git a/specification/specification.yml b/specification/specification.yml index bd3c6da..dcdad11 100644 --- a/specification/specification.yml +++ b/specification/specification.yml @@ -14,7 +14,7 @@ tags: description: Комментарии paths: - /register: + /users/register: post: summary: Создать пользователя tags: [Users] @@ -26,9 +26,16 @@ paths: required: true responses: '201': - $ref: '#/components/responses/Created' + $ref: '#/components/responses/UserResponse' - '422': + '409': + description: Пользователь с таким email уже существует + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '400': description: Некорректные данные content: application/json: @@ -42,7 +49,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /login: + /users/login: post: summary: Авторизация пользователя tags: [Users] @@ -50,7 +57,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Authorization' + $ref: '#/components/schemas/LoginUser' required: true responses: '200': @@ -58,7 +65,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthResponse' + $ref: '#/components/schemas/LoggedUser' '401': description: Неверный email или пароль @@ -85,13 +92,68 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserResponse' '401': description: Пользователь не авторизован content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + + /users/{userId}/favorite: + get: + summary: Получить список избранных предложений + parameters: + - in: path + name: userId + required: true + tags: [Users] + bearerAuth: [] + responses: + '200': + description: Предложения успешно получены + content: + application/json: + schema: + $ref: '#/components/schemas/UserFavoriteOffers' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /users/{userId}/avatar: + post: + summary: Загрузка аватара + parameters: + - in: path + name: userId + required: true + tags: [Users] + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + avatar: + type: string + format: binary + responses: + '201': + description: Аватар успешно загружен + content: + application/json: + type: string + description: Путь к загруженному файлу + example: 'https://example.com/avatar.jpg' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /offers: get: summary: Получить список предложений @@ -111,30 +173,64 @@ paths: $ref: '#/components/schemas/ErrorResponse' post: summary: Добавление нового предложения + bearerAuth: [] tags: [Offers] requestBody: content: application/json: schema: - $ref: '#/components/schemas/Offer' + $ref: '#/components/schemas/CreateOffer' responses: '201': description: Заказ успешно добавлен content: application/json: schema: - $ref: '#/components/schemas/Offer' - '422': + $ref: '#/components/schemas/OfferRdo' + '400': description: Некорректные данные content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /offers/{offerId}: + get: + summary: Получение предложения + tags: [Offers] + parameters: + - in: path + name: offerId + required: true + responses: + '200': + description: Предложение успешно получено + content: + application/json: + schema: + $ref: '#/components/schemas/OfferRdo' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' put: summary: Редактирование предложения tags: [Offers] + bearerAuth: [] parameters: - in: path name: offerId @@ -145,14 +241,20 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Offer' + $ref: '#/components/schemas/UpdateOffer' responses: '200': description: Предложение успешно обновлено content: application/json: schema: - $ref: '#/components/schemas/Offer' + $ref: '#/components/schemas/OfferRdo' + '403': + description: Пользователь не является владельцем предложения + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' '404': description: Предложение не найдено @@ -161,7 +263,7 @@ paths: schema: $ref: '#/components/schemas/NotFound' - '422': + '400': description: Некорректные данные content: application/json: @@ -176,6 +278,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: summary: Удаление предложения + bearerAuth: [] tags: [Offers] parameters: - in: path @@ -184,14 +287,111 @@ paths: schema: type: string responses: - '200': + '204': description: Предложение успешно удалено + '404': + description: Ресурс не найден или доступ запрещён content: application/json: schema: - $ref: '#/components/schemas/Offer' - + $ref: '#/components/schemas/NotFound' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /offers/{offerId}/favorite + post: + summary: Добавление предложения в избранное + bearerAuth: [] + parameters: + - in: path + name: offerId + required: true + tags: [Users] + responses: + '200': + description: Предложение успешно добавлено в избранное + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + '403': + description: Пользователь не авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + summary: Удаление предложения из избранного + bearerAuth: [] + parameters: + - in: path + name: offerId + required: true + tags: [Users] + responses: + '200': + description: Предложение успешно удалено из избранного + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + '403': + description: Пользователь не авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /offers/premium/{city}: + get: + summary: Получить список премиум предложений + tags: [Offers] + parameters: + - in: path + name: city + required: true + schema: + type: string + responses: + '200': + description: Предложения успешно получены + content: + application/json: + schema: + $ref: '#/components/schemas/Offers' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /offers/{offerId}/comments/: get: summary: Получить список комментариев @@ -209,20 +409,29 @@ paths: application/json: schema: $ref: '#/components/schemas/Comments' + '500': + description: Произошла ошибка на сервере + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /comments/{offerId}: post: summary: Добавление нового комментария + tags: [Comments] parameters: - in: path name: offerId - required: true schema: type: string - tags: [Comments] + example: '6dcfd4433f8d3f6d4f3d4f3d4f3d4f3d' + required: true requestBody: content: application/json: schema: - $ref: '#/components/schemas/Comment' + $ref: '#/components/schemas/CreateComment' required: true responses: '201': @@ -230,8 +439,8 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Comment' - '422': + $ref: '#/components/schemas/CommentRdo' + '400': description: Некорректные данные content: application/json: @@ -253,28 +462,35 @@ components: ErrorResponse: type: object properties: - message: + error: type: string example: 'Error message' - AuthResponse: + LoginUser: type: object + required: [email, password] properties: - accessToken: + email: type: string - example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' + format: email + example: 'john.doe@example.com' + password: + type: string + example: 'qwerty123' + minLength: 6 + maxLength: 12 - Authorization: + LoggedUser: type: object - required: [email, password] properties: email: type: string format: email - example: 'john.doe@example' - password: + example: 'john.doe@example.com' + token: type: string - example: 'qwerty123' + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' + User: type: object required: [name, email, password, isPro] @@ -287,7 +503,7 @@ components: email: type: string format: email - example: 'john.doe@example' + example: 'john.doe@example.com' avatar: type: string example: 'https://example.com/avatar.jpg' @@ -299,24 +515,79 @@ components: isPro: type: boolean example: false - Offer: + + UserResponse: + type: object + properties: + name: + type: string + example: 'John Doe' + email: + type: string + format: email + example: 'john.doe@example.com' + avatar: + type: string + example: 'https://example.com/avatar.jpg' + isPro: + type: boolean + example: false + favorites: + type: array + items: + type: string + example: '6dcfd4433f8d3f6d4f3d4f3d4f3d4f3d' + + Location: + type: object + required: [latitude, longitude] + properties: + latitude: + type: number + example: 48.8566 + longitude: + type: number + example: 2.3522 + + CreateOffer: type: object - required: [title, description, date, city, previewImage, images, isPremium, isFavorite, rating, type, rooms, guests, price, goods, host, commentsCount, location] + required: + - title + - description + - date + - city + - imagePreview + - images + - isPremium + - isFavorite + - rating + - type + - bedrooms + - maxAdults + - price + - goods + - commentsCount + - location properties: title: type: string + minLength: 10 + maxLength: 100 example: 'Apartment in the center of the city' description: type: string - example: 'Apartment in the center of the city' + minLength: 20 + maxLength: 1024 + example: 'Apartment in the center of the city with a great view' date: type: string format: date example: '2026-01-20' city: type: string - example: 'New York' - previewImage: + enum: [Paris, Cologne, Brussels, Amsterdam, Hamburg, Dusseldorf] + example: 'Paris' + imagePreview: type: string example: 'https://example.com/preview.jpg' images: @@ -329,76 +600,272 @@ components: example: true isFavorite: type: boolean - example: true + example: false rating: type: number - example: 5 minimum: 1 maximum: 5 + example: 4.5 type: type: string enum: [apartment, house, room, hotel] example: 'apartment' - rooms: - type: number - example: 3 + bedrooms: + type: integer + minimum: 1 + maximum: 8 + example: 2 + maxAdults: + type: integer minimum: 1 maximum: 10 - guests: + example: 4 + price: + type: integer + minimum: 100 + maximum: 100000 + example: 5000 + goods: + type: array + items: + type: string + enum: + - Breakfast + - Air conditioning + - Laptop friendly workspace + - Baby seat + - Washer + - Towels + - Fridge + example: ['Breakfast', 'Air conditioning'] + commentsCount: + type: integer + example: 0 + location: + $ref: '#/components/schemas/Location' + + UpdateOffer: + type: object + properties: + title: + type: string + minLength: 10 + maxLength: 100 + example: 'Apartment in the center of the city' + description: + type: string + minLength: 20 + maxLength: 1024 + example: 'Apartment in the center of the city with a great view' + date: + type: string + format: date + example: '2026-01-20' + city: + type: string + enum: [Paris, Cologne, Brussels, Amsterdam, Hamburg, Dusseldorf] + example: 'Paris' + imagePreview: + type: string + example: 'https://example.com/preview.jpg' + images: + type: array + items: + type: string + example: 'https://example.com/image.jpg' + isPremium: + type: boolean + example: true + isFavorite: + type: boolean + example: false + rating: type: number - example: 2 + minimum: 1 + maximum: 5 + example: 4.5 + type: + type: string + enum: [apartment, house, room, hotel] + example: 'apartment' + bedrooms: + type: integer minimum: 1 maximum: 8 + example: 2 + maxAdults: + type: integer + minimum: 1 + maximum: 10 + example: 4 price: - type: number - example: 1000 - features: + type: integer + minimum: 100 + maximum: 100000 + example: 5000 + goods: type: array - enum: [Breakfast, Air conditioning, Laptop friendly workspace, Baby seat, Washer, Towels, Towels, Fridge] items: type: string - example: 'Breakfast' - comments: - type: number - example: 5 + enum: + - Breakfast + - Air conditioning + - Laptop friendly workspace + - Baby seat + - Washer + - Towels + - Fridge + example: ['Breakfast', 'Air conditioning'] + commentsCount: + type: integer + example: 0 location: - type: object - properties: - latitude: - type: number - example: 50.4501 - longitude: - type: number - example: 30.5234 + $ref: '#/components/schemas/Location' + + OfferRdo: + type: object + properties: + title: + type: string + example: 'Apartment in the center of the city' + description: + type: string + example: 'Apartment in the center of the city with a great view' + date: + type: string + format: date-time + example: '2026-01-20T00:00:00.000Z' + city: + type: string + enum: [Paris, Cologne, Brussels, Amsterdam, Hamburg, Dusseldorf] + example: 'Paris' + imagePreview: + type: string + example: 'https://example.com/preview.jpg' + images: + type: array + items: + type: string + example: 'https://example.com/image.jpg' + isPremium: + type: boolean + example: true + isFavorite: + type: boolean + example: false + rating: + type: number + minimum: 1 + maximum: 5 + example: 4.5 + type: + type: string + enum: [apartment, house, room, hotel] + example: 'apartment' + price: + type: integer + example: 5000 + commentsCount: + type: integer + example: 12 + Offers: type: array items: - $ref: '#/components/schemas/Offer' + $ref: '#/components/schemas/OfferRdo' - Comment: + CreateComment: type: object - required: [text, date, rating, user] + required: [text, author, rating, date] properties: text: type: string + minLength: 5 + maxLength: 1024 example: 'Comment text' + author: + type: string + example: '6dcfd4433f8d3f6d4f3d4f3d4f3d4f3d' + rating: + type: number + minimum: 1 + maximum: 5 + example: 5 date: type: string format: date-time example: '2022-01-01T00:00:00.000Z' + + CommentRdo: + type: object + properties: + text: + type: string + example: 'Comment text' + offerId: + type: string + example: '6dcfd4433f8d3f6d4f3d4f3d4f3d4f3d' + author: + type: string + example: '6dcfd4433f8d3f6d4f3d4f3d4f3d4f3d' rating: type: number - example: 5 minimum: 1 maximum: 5 - user: + example: 5 + date: type: string - example: 'John Doe' + format: date-time + example: '2022-01-01T00:00:00.000Z' + + UserFavoriteOffer: + type: object + properties: + title: + type: string + example: 'Apartment in the center of the city' + date: + type: string + format: date-time + example: '2026-01-20T00:00:00.000Z' + city: + type: string + enum: [Paris, Cologne, Brussels, Amsterdam, Hamburg, Dusseldorf] + example: 'Paris' + type: + type: string + enum: [apartment, house, room, hotel] + example: 'apartment' + price: + type: integer + example: 5000 + isFavorite: + type: boolean + example: true + imagePreview: + type: string + example: 'https://example.com/preview.jpg' + isPremium: + type: boolean + example: true + rating: + type: number + minimum: 1 + maximum: 5 + example: 4.5 + commentsCount: + type: integer + example: 12 + + UserFavoriteOffers: + type: array + items: + $ref: '#/components/schemas/UserFavoriteOffer' Comments: type: array items: - $ref: '#/components/schemas/Comment' + $ref: '#/components/schemas/CommentRdo' NotFound: type: object @@ -414,10 +881,10 @@ components: application/json: schema: $ref: '#/components/schemas/NotFound' - Created: - description: Created + UserResponse: + description: Response with user content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserResponse' diff --git a/src/cli.ts b/src/cli.ts index 012f2ba..64c674b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node --loader ts-node/esm import 'reflect-metadata'; import { CliApplication } from './cli/cli-application.js'; import { Command } from './cli/command.interface.js'; diff --git a/src/cli/import.command.ts b/src/cli/import.command.ts index 348a647..3bdfda0 100644 --- a/src/cli/import.command.ts +++ b/src/cli/import.command.ts @@ -52,6 +52,7 @@ export class ImportCommand implements Command { this.logger, this.offerModel, this.commentModel, + this.userModel, ); } @@ -71,7 +72,7 @@ export class ImportCommand implements Command { { ...host, password: DEFAULT_USER_PASSWORD }, this.salt, ); - await this.offerService.createOffer({ ...offerData, host: user.id }); + await this.offerService.create({ ...offerData, host: user.id }); } public async execute( diff --git a/src/mock/mock.tsv b/src/mock/mock.tsv index e92201f..65c1310 100644 --- a/src/mock/mock.tsv +++ b/src/mock/mock.tsv @@ -1,5 +1,4 @@ -1 Cozy apartment Уютная квартира в центре с прекрасным видом на город и удобной планировкой 2026-01-20 Paris https://example.com/preview1.jpg https://example.com/img1.jpg|https://example.com/img2.jpg|https://example.com/img3.jpg|https://example.com/img4.jpg|https://example.com/img5.jpg|https://example.com/img6.jpg true false 4.7 apartment 2 4 5000 Breakfast|Air conditioning|Laptop friendly workspace 1 John Doe john@example.com https://example.com/avatar1.jpg john12 true 48.8566 2.3522 12 -2 Luxury villa Премиум вилла с бассейном и садом для больших компаний и длительного отдыха 2026-01-18 Amsterdam https://example.com/preview2.jpg https://example.com/img7.jpg|https://example.com/img8.jpg|https://example.com/img9.jpg|https://example.com/img10.jpg|https://example.com/img11.jpg|https://example.com/img12.jpg true true 4.9 house 4 8 25000 Breakfast|Washer|Towels|Fridge 2 Maria Schmidt maria@example.com https://example.com/avatar2.jpg maria12 false 52.3702 4.8952 8 -3 Student room Бюджетная комната для студентов рядом с университетом и общественным транспортом 2026-01-22 Cologne https://example.com/preview3.jpg https://example.com/img13.jpg|https://example.com/img14.jpg|https://example.com/img15.jpg|https://example.com/img16.jpg|https://example.com/img17.jpg|https://example.com/img18.jpg false false 4.0 room 1 2 1200 Laptop friendly workspace 3 Ahmed Ali ahmed@example.com https://example.com/avatar3.jpg ahmed12 true 50.9375 6.9603 5 -4 Hotel suite Современный номер в отеле с видом на канал и завтраком по расписанию 2026-01-15 Brussels https://example.com/preview4.jpg https://example.com/img19.jpg|https://example.com/img20.jpg|https://example.com/img21.jpg|https://example.com/img22.jpg|https://example.com/img23.jpg|https://example.com/img24.jpg false true 4.2 hotel 1 3 8000 Air conditioning|Baby seat|Towels 4 Emma Dubois emma@example.com https://example.com/avatar4.jpg emma12 true 50.8503 4.3517 23 -5 Family house Семейный дом в тихом районе с детской комнатой и просторной кухней 2026-01-19 Hamburg https://example.com/preview5.jpg https://example.com/img25.jpg|https://example.com/img26.jpg|https://example.com/img27.jpg|https://example.com/img28.jpg|https://example.com/img29.jpg|https://example.com/img30.jpg true false 4.5 house 3 6 15000 Breakfast|Baby seat|Washer|Fridge 5 Sophie Müller sophie@example.com https://example.com/avatar5.jpg soph12 false 53.5511 9.9937 17 +1 Luxury villa Премиум вилла с бассейном и садом для больших компаний и длительного отдыха 2026-01-18 Amsterdam https://example.com/preview2.jpg https://example.com/img7.jpg|https://example.com/img8.jpg|https://example.com/img9.jpg|https://example.com/img10.jpg|https://example.com/img11.jpg|https://example.com/img12.jpg true true 4.9 house 4 8 25000 Breakfast|Washer|Towels|Fridge 2 Maria Schmidt maria@example.com https://example.com/avatar2.jpg maria12 false 52.3702 4.8952 8 +2 Student room Бюджетная комната для студентов рядом с университетом и общественным транспортом 2026-01-22 Cologne https://example.com/preview3.jpg https://example.com/img13.jpg|https://example.com/img14.jpg|https://example.com/img15.jpg|https://example.com/img16.jpg|https://example.com/img17.jpg|https://example.com/img18.jpg false false 4.0 room 1 2 1200 Laptop friendly workspace 3 Ahmed Ali ahmed@example.com https://example.com/avatar3.jpg ahmed12 true 50.9375 6.9603 5 +3 Hotel suite Современный номер в отеле с видом на канал и завтраком по расписанию 2026-01-15 Brussels https://example.com/preview4.jpg https://example.com/img19.jpg|https://example.com/img20.jpg|https://example.com/img21.jpg|https://example.com/img22.jpg|https://example.com/img23.jpg|https://example.com/img24.jpg false true 4.2 hotel 1 3 8000 Air conditioning|Baby seat|Towels 4 Emma Dubois emma@example.com https://example.com/avatar4.jpg emma12 true 50.8503 4.3517 23 +4 Family house Семейный дом в тихом районе с детской комнатой и просторной кухней 2026-01-19 Hamburg https://example.com/preview5.jpg https://example.com/img25.jpg|https://example.com/img26.jpg|https://example.com/img27.jpg|https://example.com/img28.jpg|https://example.com/img29.jpg|https://example.com/img30.jpg true false 4.5 house 3 6 15000 Breakfast|Baby seat|Washer|Fridge 5 Sophie Müller sophie@example.com https://example.com/avatar5.jpg soph12 false 53.5511 9.9937 17 diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 7f43bc3..0ffe00e 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -58,6 +58,15 @@ export class RestApplication { '/upload', express.static(this.config.get('UPLOAD_FILE_DIRECTORY')), ); + this.server.use( + '/upload', + express.static(this.config.get('UPLOAD_FILE_DIRECTORY')), + ); + + this.server.use( + '/static', + express.static(this.config.get('STATIC_DIRECTORY_PATH')), + ); this.server.use( '/static', diff --git a/src/shared/libs/rest/exception-filter/app-exception-filter.ts b/src/shared/libs/rest/exception-filter/app-exception-filter.ts index 7ba3a5f..c320a1e 100644 --- a/src/shared/libs/rest/exception-filter/app-exception-filter.ts +++ b/src/shared/libs/rest/exception-filter/app-exception-filter.ts @@ -48,7 +48,5 @@ export class AppExceptionFilter implements ExceptionFilter { } this.handleCustomError(err, req, res, next); - - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: err.message }); } } diff --git a/src/shared/libs/rest/middleware/offer-owner.middleware.ts b/src/shared/libs/rest/middleware/offer-owner.middleware.ts index 5f412f6..91c6e6a 100644 --- a/src/shared/libs/rest/middleware/offer-owner.middleware.ts +++ b/src/shared/libs/rest/middleware/offer-owner.middleware.ts @@ -15,7 +15,7 @@ export class IsOfferOwnerMiddleware implements Middleware { const userId = req.tokenPayload.id; const { offerId } = req.params; - const offer = await this.service.findOfferById(offerId as string); + const offer = await this.service.findById(offerId as string); if (!offer) { return next( @@ -37,7 +37,7 @@ export class IsOfferOwnerMiddleware implements Middleware { ); } - if (offer.host.toString() !== userId) { + if (offer.host.id !== userId) { return next( new HttpError( StatusCodes.FORBIDDEN, diff --git a/src/shared/modules/comment/comment-controller.ts b/src/shared/modules/comment/comment-controller.ts index 8e67cec..df3cd34 100644 --- a/src/shared/modules/comment/comment-controller.ts +++ b/src/shared/modules/comment/comment-controller.ts @@ -20,7 +20,7 @@ export class CommentController extends BaseController { super(logger); this.addRoute({ - path: '/', + path: '/:offerId', method: HttpMethod.Post, handler: this.create, middlewares: [ @@ -31,7 +31,12 @@ export class CommentController extends BaseController { } public async create(req: Request, res: Response) { - const result = await this.commentService.createComment(req.body); + const { offerId } = req.params; + + const result = await this.commentService.create( + req.body, + offerId as string, + ); this.logger.info(`Comment created: ${req.body.text}`); this.created(res, fillDTO(CommentRDO, result)); diff --git a/src/shared/modules/comment/comment-service.interface.ts b/src/shared/modules/comment/comment-service.interface.ts index 331dee3..54e11e4 100644 --- a/src/shared/modules/comment/comment-service.interface.ts +++ b/src/shared/modules/comment/comment-service.interface.ts @@ -2,7 +2,7 @@ import { CommentEntity } from './comment.entity.js'; import { CommentDto } from './dto/comment-dto.js'; export interface CommentServiceInterface { - createComment(dto: CommentDto): Promise - findByOfferId(offerId: string): Promise - deleteByOfferId(offerId: string): Promise + create(dto: CommentDto, offerId: string): Promise; + findById(offerId: string): Promise; + deleteById(offerId: string): Promise; } diff --git a/src/shared/modules/comment/comment-service.ts b/src/shared/modules/comment/comment-service.ts index a9bd51f..5fa8d35 100644 --- a/src/shared/modules/comment/comment-service.ts +++ b/src/shared/modules/comment/comment-service.ts @@ -18,18 +18,21 @@ export class CommentService implements CommentServiceInterface { private readonly offerService: OfferServiceInterface, ) {} - public async createComment(dto: CommentDto): Promise { - const result = await this.commentModel.create(dto); + public async create( + dto: CommentDto, + offerId: string, + ): Promise { + const result = await this.commentModel.create({ ...dto, offerId }); this.logger.info(`Comment created: ${dto.text}`); - await this.offerService.incCommentCount(dto.offerId); + await this.offerService.incCommentCount(offerId); - await this.offerService.recalcRating(dto.offerId); + await this.offerService.recalcRating(offerId); return result.populate('author'); } - public async findByOfferId( + public async findById( offerId: string, ): Promise[]> { return this.commentModel @@ -39,7 +42,7 @@ export class CommentService implements CommentServiceInterface { .exec(); } - public async deleteByOfferId(offerId: string): Promise { + public async deleteById(offerId: string): Promise { const result = await this.commentModel.deleteMany({ offerId }).exec(); await this.offerService.decCommentCount(offerId); diff --git a/src/shared/modules/comment/dto/comment-dto.ts b/src/shared/modules/comment/dto/comment-dto.ts index cdf2c7b..b073d7d 100644 --- a/src/shared/modules/comment/dto/comment-dto.ts +++ b/src/shared/modules/comment/dto/comment-dto.ts @@ -13,9 +13,6 @@ export class CommentDto { @MaxLength(1024, { message: CommentDTOValidationMessage.text.max }) text!: string; - @IsMongoId({ message: CommentDTOValidationMessage.offerId.isMongoId }) - offerId!: string; - @IsMongoId({ message: CommentDTOValidationMessage.author.isMongoId }) author!: string; diff --git a/src/shared/modules/comment/rdo/comment-rdo.ts b/src/shared/modules/comment/rdo/comment-rdo.ts index 1845110..f91f35e 100644 --- a/src/shared/modules/comment/rdo/comment-rdo.ts +++ b/src/shared/modules/comment/rdo/comment-rdo.ts @@ -5,14 +5,11 @@ export class CommentRDO { text: string; @Expose() - offerId!: string; - - @Expose() - author!: string; + date!: Date; @Expose() rating!: number; @Expose() - date!: Date; + author!: string; } diff --git a/src/shared/modules/offer/dto/offer-dto.messages.ts b/src/shared/modules/offer/dto/offer-dto.messages.ts index 4adbce4..a23e205 100644 --- a/src/shared/modules/offer/dto/offer-dto.messages.ts +++ b/src/shared/modules/offer/dto/offer-dto.messages.ts @@ -61,7 +61,7 @@ export const OfferDTOValidationMessage = { }, host: { - isObject: 'Host must be an object.', + isMongoId: 'Host must be a valid mongo id.', }, commentsCount: { diff --git a/src/shared/modules/offer/dto/offer-dto.ts b/src/shared/modules/offer/dto/offer-dto.ts index f630359..7781e8c 100644 --- a/src/shared/modules/offer/dto/offer-dto.ts +++ b/src/shared/modules/offer/dto/offer-dto.ts @@ -72,7 +72,7 @@ export class CreateOfferDto { @IsArray({ message: OfferDTOValidationMessage.goods.isArray }) goods!: Amenity[]; - @IsString({ message: OfferDTOValidationMessage.host.isObject }) + @IsString({ message: OfferDTOValidationMessage.host.isMongoId }) host!: string; @IsInt({ message: OfferDTOValidationMessage.commentsCount.isInt }) diff --git a/src/shared/modules/offer/dto/update-dto.ts b/src/shared/modules/offer/dto/update-dto.ts index 8a2f463..ffaa034 100644 --- a/src/shared/modules/offer/dto/update-dto.ts +++ b/src/shared/modules/offer/dto/update-dto.ts @@ -4,6 +4,7 @@ import { IsDateString, IsEnum, IsInt, + IsMongoId, IsObject, IsString, Max, @@ -66,7 +67,7 @@ export class UpdateOfferDto { ['Paris', 'Cologne', 'Brussels', 'Amsterdam', 'Hamburg', 'Dusseldorf'], { message: OfferDTOValidationMessage.city.isEnum }, ) - city!: City; + city?: City; @IsString({ message: OfferDTOValidationMessage.imagePreview.isString }) imagePreview?: string; @@ -104,7 +105,7 @@ export class UpdateOfferDto { @IsArray({ message: OfferDTOValidationMessage.goods.isArray }) goods?: Amenity[]; - @IsObject({ message: OfferDTOValidationMessage.host.isObject }) + @IsMongoId({ message: OfferDTOValidationMessage.host.isMongoId }) host?: string; @IsInt({ message: OfferDTOValidationMessage.commentsCount.isInt }) diff --git a/src/shared/modules/offer/offer-controller.ts b/src/shared/modules/offer/offer-controller.ts index 8a43e22..327da8a 100644 --- a/src/shared/modules/offer/offer-controller.ts +++ b/src/shared/modules/offer/offer-controller.ts @@ -21,6 +21,7 @@ import { UserServiceInterface } from '../user/user-service.interface.js'; import { CommentRDO } from '../comment/rdo/comment-rdo.js'; import { MAX_DISPLAY_OFFERS_COUNT } from './offer.const.js'; import { IsOfferOwnerMiddleware } from '../../libs/rest/middleware/offer-owner.middleware.js'; +import { HttpError } from '../../libs/rest/errors/index.js'; export class OfferController extends BaseController { constructor( @@ -50,9 +51,9 @@ export class OfferController extends BaseController { handler: this.update, middlewares: [ new PrivateRouteMiddleware(), - new IsOfferOwnerMiddleware(this.offerService), new ValidateObjectIdMiddleware('offerId'), new ValidateDTOMiddleware(UpdateOfferDto), + new IsOfferOwnerMiddleware(this.offerService), new DocumentExistsMiddleware('offerId', 'offer', this.offerService), ], }); @@ -62,8 +63,8 @@ export class OfferController extends BaseController { handler: this.delete, middlewares: [ new PrivateRouteMiddleware(), - new IsOfferOwnerMiddleware(this.offerService), new ValidateObjectIdMiddleware('offerId'), + new IsOfferOwnerMiddleware(this.offerService), new DocumentExistsMiddleware('offerId', 'offer', this.offerService), ], }); @@ -90,6 +91,20 @@ export class OfferController extends BaseController { new DocumentExistsMiddleware('offerId', 'offer', this.offerService), ], }); + + this.addRoute({ + path: '/:offerId/favorite', + method: HttpMethod.Post, + handler: this.addFavoriteOffers, + middlewares: [new PrivateRouteMiddleware()], + }); + + this.addRoute({ + path: '/:offerId/favorite', + method: HttpMethod.Delete, + handler: this.deleteFavoriteOffers, + middlewares: [new PrivateRouteMiddleware()], + }); } public async index(req: Request, res: Response) { @@ -107,12 +122,14 @@ export class OfferController extends BaseController { ...offer.toObject(), isFavorite: favoritesIds.includes(offer.id), })); + const responseData = fillDTO(OfferRDO, offersWithStatus); + this.ok(res, responseData); } public async create(req: Request, res: Response): Promise { - const existOffer = await this.offerService.findOfferById(req.body.id); + const existOffer = await this.offerService.findById(req.body.id); if (existOffer) { const error = new Error(`Offer with id ${req.body.id} already exists`); @@ -123,7 +140,7 @@ export class OfferController extends BaseController { return this.logger.error(error.message, error); } - const result = await this.offerService.createOffer({ + const result = await this.offerService.create({ ...req.body, host: req.tokenPayload.id, }); @@ -131,10 +148,18 @@ export class OfferController extends BaseController { } public async getDetailOffers(req: Request, res: Response) { - const offer = await this.offerService.findOfferById( + const offer = await this.offerService.findById( req.params.offerId as string, ); - this.ok(res, fillDTO(OfferRDO, offer)); + + let isFavorite = false; + + if (req.tokenPayload) { + const user = await this.userService.findById(req.tokenPayload.id); + isFavorite = + user?.favorites.some((id) => id.toString() === offer?.id) ?? false; + } + this.ok(res, fillDTO(OfferRDO, { ...offer?.toObject(), isFavorite })); } public async update(req: Request, res: Response) { @@ -147,9 +172,54 @@ export class OfferController extends BaseController { this.ok(res, fillDTO(OfferRDO, updateOffer)); } + public async addFavoriteOffers(req: Request, res: Response) { + const { offerId } = req.params; + const user = await this.userService.findById(req.tokenPayload.id); + if (!user) { + throw new HttpError( + StatusCodes.UNAUTHORIZED, + 'Unauthorized', + 'UserController', + ); + } + const offer = await this.offerService.findById(offerId as string); + if (!offer) { + throw new HttpError( + StatusCodes.NOT_FOUND, + 'Offer not found', + 'UserController', + ); + } + const response = await this.offerService.addFavorite(offer.id, user.id); + this.ok(res, response); + } + + async deleteFavoriteOffers(req: Request, res: Response) { + const { offerId } = req.params; + const user = await this.userService.findById(req.tokenPayload.id); + if (!user) { + throw new HttpError( + StatusCodes.UNAUTHORIZED, + 'Unauthorized', + 'UserController', + ); + } + const offer = await this.offerService.findById(offerId as string); + if (!offer) { + throw new HttpError( + StatusCodes.NOT_FOUND, + 'Offer not found', + 'UserController', + ); + } + await this.offerService.deleteFavorite(offer.id, user.id); + const updatedUser = await this.userService.findById(user.id); + this.ok(res, updatedUser); + } + public async delete(req: Request, res: Response) { const { offerId } = req.params; - const offer = await this.offerService.findOfferById(offerId as string); + const offer = await this.offerService.findById(offerId as string); if (!offer) { const error = new Error(`Offer with id ${offerId} not found`); @@ -158,7 +228,7 @@ export class OfferController extends BaseController { return this.logger.error(error.message, error); } const result = await this.offerService.deleteById(offer.id); - await this.commentService.deleteByOfferId(offer.id); + await this.commentService.deleteById(offer.id); this.noContent(res, fillDTO(OfferRDO, result)); } @@ -169,13 +239,26 @@ export class OfferController extends BaseController { city as City, ); - this.ok(res, fillDTO(OfferRDO, premiumOffers)); + let favoritesIds: string[] = []; + + if (req.tokenPayload) { + const user = await this.userService.findById(req.tokenPayload.id); + favoritesIds = + user?.favorites.map((offer) => offer.toString()) ?? ([] as string[]); + } + + const offersWithStatus = premiumOffers.map((offer) => ({ + ...offer.toObject(), + isFavorite: favoritesIds.includes(offer.id), + })); + + this.ok(res, fillDTO(OfferRDO, offersWithStatus)); } public async getComments(req: Request, res: Response) { const { offerId } = req.params; - const comments = await this.commentService.findByOfferId(offerId as string); + const comments = await this.commentService.findById(offerId as string); this.ok(res, fillDTO(CommentRDO, comments)); } diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index 7cc5e9b..073e09b 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -2,10 +2,11 @@ import { DocumentType } from '@typegoose/typegoose'; import { CreateOfferDto } from './dto/offer-dto.js'; import { OfferEntity } from './offer.entity.js'; import { City, UpdateOfferDto } from './dto/update-dto.js'; +import { UserEntity } from '../user/index.js'; export interface OfferServiceInterface { - createOffer(dto: CreateOfferDto): Promise>; - findOfferById(id: string): Promise | null>; + create(dto: CreateOfferDto): Promise>; + findById(id: string): Promise | null>; updateById( offerId: string, dto: UpdateOfferDto, @@ -15,5 +16,13 @@ export interface OfferServiceInterface { incCommentCount(offerId: string): Promise | null>; decCommentCount(offerId: string): Promise | null>; findPremiumOffersByCity(city: City): Promise[]>; + deleteFavorite( + offerId: string, + userId: string, + ): Promise | null>; + addFavorite( + offerId: string, + userId: string, + ): Promise | null>; find(limit: number): Promise[]>; } diff --git a/src/shared/modules/offer/offer-service.ts b/src/shared/modules/offer/offer-service.ts index e6d2a63..b2ae901 100644 --- a/src/shared/modules/offer/offer-service.ts +++ b/src/shared/modules/offer/offer-service.ts @@ -5,11 +5,12 @@ import { OfferEntity } from './offer.entity.js'; import { Component } from '../../types/container.js'; import { Logger } from '../../libs/logger/index.js'; import { City, UpdateOfferDto } from './dto/update-dto.js'; -import { DocumentType, types } from '@typegoose/typegoose'; +import { DocumentType, mongoose, types } from '@typegoose/typegoose'; import { CommentEntity } from '../comment/comment.entity.js'; import { Types } from 'mongoose'; import { DocumentExists } from '../../libs/rest/types/document-exists.interface.js'; import { DISPLAY_PREMIUM_OFFERS_COUNT } from './offer.const.js'; +import { UserEntity } from '../user/index.js'; @injectable() export class OfferService implements OfferServiceInterface, DocumentExists { @@ -19,15 +20,17 @@ export class OfferService implements OfferServiceInterface, DocumentExists { private readonly offerModel: types.ModelType, @inject(Component.CommentModel) private readonly commentModel: types.ModelType, + @inject(Component.UserModel) + private readonly userModel: types.ModelType, ) {} - async createOffer(dto: CreateOfferDto): Promise> { + async create(dto: CreateOfferDto): Promise> { const result = await this.offerModel.create(dto); this.logger.info(`New offer created: ${dto.title}`); return result; } - async findOfferById(id: string): Promise | null> { + async findById(id: string): Promise | null> { return this.offerModel.findById(id).populate('host').exec(); } @@ -93,4 +96,32 @@ export class OfferService implements OfferServiceInterface, DocumentExists { .findByIdAndUpdate(offerId, { $inc: { commentsCount: -1 } }) .exec(); } + + async addFavorite( + offerId: string, + userId: string, + ): Promise | null> { + const result = await this.userModel + .findByIdAndUpdate(userId, { + $addToSet: { favorites: new mongoose.Types.ObjectId(offerId) }, + }) + .exec(); + + return result; + } + + async deleteFavorite( + offerId: string, + userId: string, + ): Promise | null> { + const result = await this.userModel + .findByIdAndUpdate( + userId, + { $pull: { favorites: new mongoose.Types.ObjectId(offerId) } }, + { new: true }, + ) + .exec(); + + return result; + } } diff --git a/src/shared/modules/user/user-controller.ts b/src/shared/modules/user/user-controller.ts index aaee936..033e3eb 100644 --- a/src/shared/modules/user/user-controller.ts +++ b/src/shared/modules/user/user-controller.ts @@ -19,13 +19,10 @@ import { CreateUserDto, LoginUserDTO } from './dto/user-dto.js'; import { AuthServiceInterface, UserNotFoundException } from '../auth/index.js'; import { PrivateRouteMiddleware } from '../../libs/rest/middleware/private-route.middleware.js'; import { UploadFileMiddleware } from '../../libs/rest/middleware/upload-file.middleware.js'; -import { OfferServiceInterface } from '../offer/offer-service.interface.js'; export class UserController extends BaseController { constructor( @inject(Component.Logger) protected readonly logger: Logger, - @inject(Component.OfferService) - private readonly offerService: OfferServiceInterface, @inject(Component.UserService) private readonly userService: UserServiceInterface, @inject(Component.Config) private readonly config: Config, @@ -72,20 +69,6 @@ export class UserController extends BaseController { handler: this.checkAuthenticate, middlewares: [new PrivateRouteMiddleware()], }); - - this.addRoute({ - path: '/:offerId/favorite', - method: HttpMethod.Post, - handler: this.addFavoriteOffers, - middlewares: [new PrivateRouteMiddleware()], - }); - - this.addRoute({ - path: '/:offerId/favorite', - method: HttpMethod.Delete, - handler: this.deleteFavoriteOffers, - middlewares: [new PrivateRouteMiddleware()], - }); } public async create(req: Request, res: Response) { @@ -114,9 +97,14 @@ export class UserController extends BaseController { 'UserController', ); } - const favorites = await this.userService.findFavoriteOffers(user.id); + const favorites = await this.userService.findFavorite(user.id); + + const favoritesWithStatus = favorites.map((offer) => ({ + ...offer.toObject(), + isFavorite: true, + })); - this.ok(res, fillDTO(UserFavoriteOfferRDO, favorites)); + this.ok(res, fillDTO(UserFavoriteOfferRDO, favoritesWithStatus)); } public async auth(req: Request, res: Response) { @@ -133,7 +121,9 @@ export class UserController extends BaseController { this.ok(res, response); } - public uploadAvatar(req: Request, res: Response) { + public async uploadAvatar(req: Request, res: Response) { + const user = await this.userService.findById(req.tokenPayload.id); + await this.userService.updateAvatar(user?.id, req.file?.filename as string); this.created(res, { file: req.file?.path, }); @@ -149,51 +139,6 @@ export class UserController extends BaseController { 'UserController', ); } - this.ok(res, fillDTO(LoggedUserRDO, user)); - } - - public async addFavoriteOffers(req: Request, res: Response) { - const { offerId } = req.params; - const user = await this.userService.findById(req.tokenPayload.id); - if (!user) { - throw new HttpError( - StatusCodes.UNAUTHORIZED, - 'Unauthorized', - 'UserController', - ); - } - const offer = await this.offerService.findOfferById(offerId as string); - if (!offer) { - throw new HttpError( - StatusCodes.NOT_FOUND, - 'Offer not found', - 'UserController', - ); - } - const response = await this.userService.addFavoriteOffer(offer.id, user.id); - this.ok(res, response); - } - - async deleteFavoriteOffers(req: Request, res: Response) { - const { offerId } = req.params; - const user = await this.userService.findById(req.tokenPayload.id); - if (!user) { - throw new HttpError( - StatusCodes.UNAUTHORIZED, - 'Unauthorized', - 'UserController', - ); - } - const offer = await this.offerService.findOfferById(offerId as string); - if (!offer) { - throw new HttpError( - StatusCodes.NOT_FOUND, - 'Offer not found', - 'UserController', - ); - } - await this.userService.deleteFavoriteOffer(offer.id, user.id); - const updatedUser = await this.userService.findById(user.id); - this.ok(res, updatedUser); + this.ok(res, fillDTO(UserRDO, user)); } } diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts index 83b3fea..d6827fb 100644 --- a/src/shared/modules/user/user-service.interface.ts +++ b/src/shared/modules/user/user-service.interface.ts @@ -7,11 +7,9 @@ export interface UserServiceInterface { register(dto: CreateUserDto, salt: string): Promise>; findByEmail(email: string): Promise | null>; findById(id: string): Promise | null>; - findFavoriteOffers(userId: number): Promise[]>; - deleteFavoriteOffer( - offerId: string, - userId: string, + findFavorite(userId: number): Promise[]>; + updateAvatar( + userId: number, + avatar: string, ): Promise | null>; - addFavoriteOffer(offerId: string, userId: string): Promise; - toggleFavoriteOffer(offerId: string, userId: number): Promise; } diff --git a/src/shared/modules/user/user-service.ts b/src/shared/modules/user/user-service.ts index 17e7415..7eef777 100644 --- a/src/shared/modules/user/user-service.ts +++ b/src/shared/modules/user/user-service.ts @@ -7,7 +7,6 @@ import { Component } from '../../types/container.js'; import { Logger } from '../../libs/logger/index.js'; import { OfferEntity } from '../offer/offer.entity.js'; import { DEFAULT_AVATAR_FILE_NAME } from './user.constant.js'; -import mongoose from 'mongoose'; @injectable() export class UserService implements UserServiceInterface { @@ -37,9 +36,7 @@ export class UserService implements UserServiceInterface { return this.userModel.findById(id).exec(); } - async findFavoriteOffers( - userId: number, - ): Promise[]> { + async findFavorite(userId: number): Promise[]> { const user = await this.userModel .findById(userId) .populate('favorites') @@ -47,39 +44,12 @@ export class UserService implements UserServiceInterface { return (user?.favorites as DocumentType[]) ?? []; } - async toggleFavoriteOffer(offerId: string, userId: number): Promise { - const user = await UserModel.findById(userId).populate('favorites').exec(); - if (!user) { - return; - } - - if (user.favorites.find((offer) => offer._id.toString() === offerId)) { - await UserModel.findByIdAndUpdate(userId, { - $pull: { favorites: offerId }, - }); - } else { - await UserModel.findByIdAndUpdate(userId, { - $push: { favorites: offerId }, - }); - } - } - - async addFavoriteOffer(offerId: string, userId: string): Promise { - await UserModel.findByIdAndUpdate(userId, { - $addToSet: { favorites: new mongoose.Types.ObjectId(offerId) }, - }); - } - - async deleteFavoriteOffer( - offerId: string, - userId: string, + async updateAvatar( + userId: number, + avatar: string, ): Promise | null> { - const result = await UserModel.findByIdAndUpdate( - userId, - { $pull: { favorites: new mongoose.Types.ObjectId(offerId) } }, - { new: true }, - ).exec(); - - return result; + return this.userModel + .findByIdAndUpdate(userId, { $set: { avatar } }, { new: true }) + .exec(); } } diff --git a/src/shared/types/offer.ts b/src/shared/types/offer.ts index e26ebdc..80602ca 100644 --- a/src/shared/types/offer.ts +++ b/src/shared/types/offer.ts @@ -7,10 +7,10 @@ export type User = { isPro: boolean; }; -export type Location = { - latitude: number; - longitude: number; -}; +export class Location { + public latitude: number; + public longitude: number; +} export type City = | 'Paris' diff --git a/src/uploads/25932d6e-504e-4628-be2c-aa27a258258a.false b/src/uploads/25932d6e-504e-4628-be2c-aa27a258258a.false new file mode 100644 index 0000000..ba90d3c Binary files /dev/null and b/src/uploads/25932d6e-504e-4628-be2c-aa27a258258a.false differ diff --git a/src/uploads/58d20245-f7f0-4b1e-9099-2b07015a7bd9.false b/src/uploads/58d20245-f7f0-4b1e-9099-2b07015a7bd9.false new file mode 100644 index 0000000..28da12e Binary files /dev/null and b/src/uploads/58d20245-f7f0-4b1e-9099-2b07015a7bd9.false differ diff --git a/src/uploads/778b8ff8-6fdf-4ca6-a218-b4e80d4baaac.false b/src/uploads/778b8ff8-6fdf-4ca6-a218-b4e80d4baaac.false new file mode 100644 index 0000000..28da12e Binary files /dev/null and b/src/uploads/778b8ff8-6fdf-4ca6-a218-b4e80d4baaac.false differ diff --git a/src/uploads/7d9c9c0c-1884-44cb-8bb0-7f284245508e.false b/src/uploads/7d9c9c0c-1884-44cb-8bb0-7f284245508e.false new file mode 100644 index 0000000..f6af37c Binary files /dev/null and b/src/uploads/7d9c9c0c-1884-44cb-8bb0-7f284245508e.false differ diff --git a/src/uploads/81688aa7-9440-4776-884c-c09cfe8bfd5c.false b/src/uploads/81688aa7-9440-4776-884c-c09cfe8bfd5c.false new file mode 100644 index 0000000..28da12e Binary files /dev/null and b/src/uploads/81688aa7-9440-4776-884c-c09cfe8bfd5c.false differ