From e2db9403daad498391bcc40cb0649e0b42f09576 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 May 2026 14:39:09 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 11 ++++ Workflow.md | 14 +++++ custom.d.ts | 2 +- docker-compose.yaml | 25 +++++--- queries.http | 1 - .../specification.yml | 0 src/cli/import.command.ts | 3 +- src/cli/version.command.ts | 9 ++- src/main.ts | 0 src/shared/helpers/hash.ts | 4 +- src/shared/libs/logger/logger.ts | 15 ++--- .../rest/middleware/offer-owner.middleware.ts | 52 ++++++++++++++++ .../rest/middleware/parse-token.middleware.ts | 12 ++-- src/shared/libs/transform/path-transformer.ts | 8 +-- .../libs/transform/transformer.const.ts | 3 + src/shared/modules/auth/auth-service.ts | 2 +- .../{TokenPayload.ts => token-payload.ts} | 0 src/shared/modules/offer/offer-controller.ts | 25 +++++--- src/shared/modules/offer/offer-service.ts | 1 - src/shared/modules/offer/rdo/offer.rdo.ts | 15 ----- src/shared/modules/user/user-controller.ts | 62 +++++++++++++++++++ .../modules/user/user-service.interface.ts | 5 ++ src/shared/modules/user/user-service.ts | 47 ++++++++++---- 23 files changed, 242 insertions(+), 74 deletions(-) create mode 100644 .env.example delete mode 100644 queries.http rename {src/specification => specification}/specification.yml (100%) delete mode 100644 src/main.ts create mode 100644 src/shared/libs/rest/middleware/offer-owner.middleware.ts rename src/shared/modules/auth/types/{TokenPayload.ts => token-payload.ts} (100%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ce6a3bb --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +PORT=5000 +SALT=secret +DB_USER = test +DB_PASSWORD = test +DB_HOST=127.0.0.2 +DB_PORT=27017 +DB_NAME=test_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/Workflow.md b/Workflow.md index 8b87981..1968850 100644 --- a/Workflow.md +++ b/Workflow.md @@ -80,6 +80,20 @@ npm start Советы и инструкции по внесению изменений в учебный репозиторий. +### Переменные окружения +Проект использует переменные окружения для конфигурации. Создайте файл `.env` в корне проекта на основе `.env.example`. + +`PORT` -- Порт HTTP-сервера. По умолчанию - `3000` +`SALT` -- Соль для хеширования паролей +`DB_USER` -- Пользователь для подключения к БД +`DB_PASSWORD` -- Пароль пользователя БД +`DB_HOST` -- Адрес сервера базы данных +`DB_PORT` -- Порт базы данных +`DB_NAME` -- Имя базы данных +`JWT_SECRET` -- Секретный ключ для подписи JWT +`UPLOAD_FILE_DIRECTORY` - Путь для загрузки файлов пользователями +`HOST` -- Хост сервера +`STATIC_DIRECTORY_PATH` -- Путь к директории со статикой ### Остальное Все остальные файлы в проекте являются служебными. Пожалуйста, не удаляйте и не изменяйте их самовольно. Только если того требует задание или наставник. diff --git a/custom.d.ts b/custom.d.ts index c4b240b..8e42106 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -1,4 +1,4 @@ -import { TokenPayload } from "./src/shared/modules/auth/types/TokenPayload.js"; +import { TokenPayload } from "./src/shared/modules/auth/types/token-payload.ts"; declare module "express-serve-static-core" { export interface Request { diff --git a/docker-compose.yaml b/docker-compose.yaml index 12cd7c6..f119e27 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,29 +1,36 @@ version: '3.9' + services: db: image: mongo:4.2 restart: always container_name: six-cities-mongodb environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: test + MONGO_INITDB_ROOT_USERNAME: ${DB_USER:-admin} + MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-test} ports: - - 27017:27017 + - "${DB_PORT:-27017}:27017" volumes: - six-cities_data:/data/db + networks: + - app-network db_ui: image: mongo-express restart: always container_name: six-cities-mongo-express ports: - - 8081:8081 + - "${MONGO_EXPRESS_PORT:-8081}:8081" environment: - ME_CONFIG_BASICAUTH_USERNAME: admin - ME_CONFIG_BASICAUTH_PASSWORD: test - ME_CONFIG_MONGODB_ADMINUSERNAME: admin - ME_CONFIG_MONGODB_ADMINPASSWORD: test - ME_CONFIG_MONGODB_URL: mongodb://admin:test@db:27017/ + 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/ + depends_on: + - db + networks: + - app-network volumes: six-cities_data: diff --git a/queries.http b/queries.http deleted file mode 100644 index 7b71334..0000000 --- a/queries.http +++ /dev/null @@ -1 +0,0 @@ -GET http://localhost:3123/api HTTP/1.1 \ No newline at end of file diff --git a/src/specification/specification.yml b/specification/specification.yml similarity index 100% rename from src/specification/specification.yml rename to specification/specification.yml diff --git a/src/cli/import.command.ts b/src/cli/import.command.ts index d9e54d3..263ad08 100644 --- a/src/cli/import.command.ts +++ b/src/cli/import.command.ts @@ -48,8 +48,7 @@ export class ImportCommand implements Command { resolve(); } - private onCompletedImport(count: number) { - console.log(`Imported ${count} offers`); + private onCompletedImport() { this.databaseClient.disconnect(); } diff --git a/src/cli/version.command.ts b/src/cli/version.command.ts index f6364b7..740ba0f 100644 --- a/src/cli/version.command.ts +++ b/src/cli/version.command.ts @@ -6,12 +6,11 @@ type PackageJsonConfig = { version: string; }; -const isPackageJsonConfig = (value: unknown): value is PackageJsonConfig => ( +const isPackageJsonConfig = (value: unknown): value is PackageJsonConfig => typeof value === 'object' && - value !== null && - !Array.isArray(value) && - Object.hasOwn(value, 'version') -); + value !== null && + !Array.isArray(value) && + Object.hasOwn(value, 'version'); export class VersionCommand implements Command { constructor(private filePath: string = './package.json') {} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/helpers/hash.ts b/src/shared/helpers/hash.ts index 031fd7e..9173764 100644 --- a/src/shared/helpers/hash.ts +++ b/src/shared/helpers/hash.ts @@ -1,6 +1,6 @@ import * as crypto from 'node:crypto'; -export const createSha256 = (line: string, solt: string) => { - const shaHasher = crypto.createHmac('sha256', solt); +export const createSha256 = (line: string, salt: string) => { + const shaHasher = crypto.createHmac('sha256', salt); return shaHasher.update(line).digest('hex'); }; diff --git a/src/shared/libs/logger/logger.ts b/src/shared/libs/logger/logger.ts index b24962a..748817a 100644 --- a/src/shared/libs/logger/logger.ts +++ b/src/shared/libs/logger/logger.ts @@ -4,14 +4,15 @@ import { Logger } from './logger.interface.js'; import { Logger as PinoInstance, pino, transport } from 'pino'; import { injectable } from 'inversify'; +const LOG_FILE_PATH = 'logs/rest.log'; + @injectable() export class PinoLogger implements Logger { private readonly logger: PinoInstance; constructor() { const modulePath = getCurrentModulePath(); - const logFilePath = 'logs/rest.log'; - const destination = resolve(modulePath,'../../../', logFilePath); + const destination = resolve(modulePath, '../../../', LOG_FILE_PATH); const multiTransport = transport({ targets: [ @@ -23,7 +24,7 @@ export class PinoLogger implements Logger { }, { target: 'pino-pretty', - } + }, ], }); @@ -31,18 +32,18 @@ export class PinoLogger implements Logger { } info(message: string, ...args: unknown[]): void { - this.logger.info({meta: args}, message); + this.logger.info({ meta: args }, message); } debug(message: string, ...args: unknown[]): void { - this.logger.debug({meta: args}, message); + this.logger.debug({ meta: args }, message); } warn(message: string, ...args: unknown[]): void { - this.logger.warn({meta: args}, message); + this.logger.warn({ meta: args }, message); } error(message: string, ...args: unknown[]): void { - this.logger.error({meta: args}, message); + this.logger.error({ meta: args }, message); } } diff --git a/src/shared/libs/rest/middleware/offer-owner.middleware.ts b/src/shared/libs/rest/middleware/offer-owner.middleware.ts new file mode 100644 index 0000000..66abfc7 --- /dev/null +++ b/src/shared/libs/rest/middleware/offer-owner.middleware.ts @@ -0,0 +1,52 @@ +import { Middleware } from "./middleware.interface.js"; +import { OfferServiceInterface } from "../../../modules/offer/offer-service.interface.js"; +import { Request, Response, NextFunction } from "express"; +import { StatusCodes } from "http-status-codes"; +import { HttpError } from "../errors/index.js"; + +export class IsOfferOwnerMiddleware implements Middleware { + constructor(private readonly service: OfferServiceInterface) {} + + async execute( + req: Request, + _res: Response, + next: NextFunction, + ): Promise { + const userId = req.tokenPayload.id; + const { offerId } = req.params; + + const offer = await this.service.findOfferById(offerId as string); + + if (!offer) { + return next( + new HttpError( + StatusCodes.NOT_FOUND, + "Offer not found", + "IsOfferOwnerMiddleware", + ), + ); + } + + if (!userId) { + return next( + new HttpError( + StatusCodes.UNAUTHORIZED, + "Unauthorized", + "IsOfferOwnerMiddleware", + ), + ); + } + + if (offer.host.toString() !== userId) { + return next( + new HttpError( + StatusCodes.FORBIDDEN, + "You are not the owner of this offer", + "IsOfferOwnerMiddleware", + ), + ); + } + + return next(); + } +} diff --git a/src/shared/libs/rest/middleware/parse-token.middleware.ts b/src/shared/libs/rest/middleware/parse-token.middleware.ts index e71f643..1026338 100644 --- a/src/shared/libs/rest/middleware/parse-token.middleware.ts +++ b/src/shared/libs/rest/middleware/parse-token.middleware.ts @@ -4,7 +4,7 @@ import crypto from 'node:crypto'; import { jwtVerify } from 'jose'; import { HttpError } from '../errors/index.js'; import { StatusCodes } from 'http-status-codes'; -import { TokenPayload } from '../../../modules/auth/types/TokenPayload.js'; +import { TokenPayload } from '../../../modules/auth/types/token-payload.js'; function isTokenPayload(payload: unknown): payload is TokenPayload { return ( @@ -27,17 +27,17 @@ export class ParseTokenMiddleware implements Middleware { _res: Response, next: NextFunction, ): Promise { - const authorizationHeader = req.headers?.authorization?.split(' '); + const authorizationHeaders = req.headers?.authorization?.split(' '); if ( - !authorizationHeader || - authorizationHeader?.[0] !== 'Bearer' || - !authorizationHeader?.[1] + !authorizationHeaders || + authorizationHeaders?.[0] !== 'Bearer' || + !authorizationHeaders?.[1] ) { return next(); } - const [, token] = authorizationHeader; + const [, token] = authorizationHeaders; try { const { payload } = await jwtVerify( token, diff --git a/src/shared/libs/transform/path-transformer.ts b/src/shared/libs/transform/path-transformer.ts index 2452170..2106aa6 100644 --- a/src/shared/libs/transform/path-transformer.ts +++ b/src/shared/libs/transform/path-transformer.ts @@ -4,7 +4,9 @@ import { Component } from '../../types/container.js'; import { Config, RestSchema } from '../config/index.js'; import { DEFAULT_STATIC_IMAGES, + STATIC_PATH, STATIC_RESOURCE_FIELDS, + UPLOAD_PATH, } from './transformer.const.js'; import { getFullServerPath } from '../../helpers/common.js'; @@ -43,13 +45,11 @@ export class PathTransformer { } if (this.isStaticProperty(key) && typeof value === 'string') { - const staticPath = '/static'; - const uploadPath = '/upload'; const serverHost = this.config.get('HOST'); const serverPort = this.config.get('PORT'); const rootPath = this.hasDefaultImage(value) - ? staticPath - : uploadPath; + ? STATIC_PATH + : UPLOAD_PATH; current[key] = `${getFullServerPath(serverHost, serverPort)}${rootPath}/${value}`; } diff --git a/src/shared/libs/transform/transformer.const.ts b/src/shared/libs/transform/transformer.const.ts index b113565..bcdaed5 100644 --- a/src/shared/libs/transform/transformer.const.ts +++ b/src/shared/libs/transform/transformer.const.ts @@ -1,3 +1,6 @@ export const DEFAULT_STATIC_IMAGES = ['default-avatar.jpg']; export const STATIC_RESOURCE_FIELDS = ['avatar', 'imagePreview', 'images']; + +export const STATIC_PATH = '/static'; +export const UPLOAD_PATH = '/upload'; diff --git a/src/shared/modules/auth/auth-service.ts b/src/shared/modules/auth/auth-service.ts index dae5c63..a5d9f51 100644 --- a/src/shared/modules/auth/auth-service.ts +++ b/src/shared/modules/auth/auth-service.ts @@ -6,7 +6,7 @@ import { Logger } from '../../libs/logger/index.js'; import { UserService } from '../user/user-service.js'; import { Config, RestSchema } from '../../libs/config/index.js'; import * as crypto from 'node:crypto'; -import { TokenPayload } from './types/TokenPayload.js'; +import { TokenPayload } from './types/token-payload.js'; import { SignJWT } from 'jose'; import { JWT_ALGORITHM, JWT_EXPIRED } from './auth.constant.js'; import { diff --git a/src/shared/modules/auth/types/TokenPayload.ts b/src/shared/modules/auth/types/token-payload.ts similarity index 100% rename from src/shared/modules/auth/types/TokenPayload.ts rename to src/shared/modules/auth/types/token-payload.ts diff --git a/src/shared/modules/offer/offer-controller.ts b/src/shared/modules/offer/offer-controller.ts index 174c26d..8a43e22 100644 --- a/src/shared/modules/offer/offer-controller.ts +++ b/src/shared/modules/offer/offer-controller.ts @@ -20,6 +20,7 @@ import { PrivateRouteMiddleware } from '../../libs/rest/middleware/private-route 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'; export class OfferController extends BaseController { constructor( @@ -49,6 +50,7 @@ export class OfferController extends BaseController { handler: this.update, middlewares: [ new PrivateRouteMiddleware(), + new IsOfferOwnerMiddleware(this.offerService), new ValidateObjectIdMiddleware('offerId'), new ValidateDTOMiddleware(UpdateOfferDto), new DocumentExistsMiddleware('offerId', 'offer', this.offerService), @@ -60,12 +62,13 @@ export class OfferController extends BaseController { handler: this.delete, middlewares: [ new PrivateRouteMiddleware(), + new IsOfferOwnerMiddleware(this.offerService), new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware('offerId', 'offer', this.offerService), ], }); this.addRoute({ - path: '/premium', + path: '/premium/:city', method: HttpMethod.Get, handler: this.showPremiumOffers, }); @@ -135,8 +138,9 @@ export class OfferController extends BaseController { } public async update(req: Request, res: Response) { + const { offerId } = req.params; const updateOffer = await this.offerService.updateById( - req.params.id as string, + offerId as string, req.body, ); @@ -144,29 +148,32 @@ export class OfferController extends BaseController { } public async delete(req: Request, res: Response) { - const offer = await this.offerService.findOfferById( - req.params.id as string, - ); + const { offerId } = req.params; + const offer = await this.offerService.findOfferById(offerId as string); if (!offer) { - const error = new Error(`Offer with id ${req.params.id} not found`); + const error = new Error(`Offer with id ${offerId} not found`); this.send(res, StatusCodes.NOT_FOUND, { error: error.message }); return this.logger.error(error.message, error); } const result = await this.offerService.deleteById(offer.id); + await this.commentService.deleteByOfferId(offer.id); + this.noContent(res, fillDTO(OfferRDO, result)); } public async showPremiumOffers(req: Request, res: Response) { - const city = req.params.city as City; - const premiumOffers = await this.offerService.findPremiumOffersByCity(city); + const { city } = req.params; + const premiumOffers = await this.offerService.findPremiumOffersByCity( + city as City, + ); this.ok(res, fillDTO(OfferRDO, premiumOffers)); } public async getComments(req: Request, res: Response) { - const offerId = req.params.offerId; + const { offerId } = req.params; const comments = await this.commentService.findByOfferId(offerId as string); diff --git a/src/shared/modules/offer/offer-service.ts b/src/shared/modules/offer/offer-service.ts index 6fda718..e6d2a63 100644 --- a/src/shared/modules/offer/offer-service.ts +++ b/src/shared/modules/offer/offer-service.ts @@ -75,7 +75,6 @@ export class OfferService implements OfferServiceInterface, DocumentExists { .find({ city, isPremium: true }) .sort({ date: -1 }) .limit(DISPLAY_PREMIUM_OFFERS_COUNT) - .populate('host') .exec(); } diff --git a/src/shared/modules/offer/rdo/offer.rdo.ts b/src/shared/modules/offer/rdo/offer.rdo.ts index a5e99a1..be83267 100644 --- a/src/shared/modules/offer/rdo/offer.rdo.ts +++ b/src/shared/modules/offer/rdo/offer.rdo.ts @@ -31,24 +31,9 @@ export class OfferRDO { @Expose() type: string; - @Expose() - bedrooms: number; - - @Expose() - maxAdults: number; - @Expose() price: number; - @Expose() - goods: string[]; - - @Expose() - host: string; - @Expose() commentsCount: number; - - @Expose() - location: { latitude: number; longitude: number }; } diff --git a/src/shared/modules/user/user-controller.ts b/src/shared/modules/user/user-controller.ts index febc222..aaee936 100644 --- a/src/shared/modules/user/user-controller.ts +++ b/src/shared/modules/user/user-controller.ts @@ -19,10 +19,13 @@ 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, @@ -69,6 +72,20 @@ 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) { @@ -134,4 +151,49 @@ export class UserController extends BaseController { } 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); + } } diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts index 9cb64c4..83b3fea 100644 --- a/src/shared/modules/user/user-service.interface.ts +++ b/src/shared/modules/user/user-service.interface.ts @@ -8,5 +8,10 @@ export interface UserServiceInterface { findByEmail(email: string): Promise | null>; findById(id: string): Promise | null>; findFavoriteOffers(userId: number): Promise[]>; + deleteFavoriteOffer( + offerId: string, + userId: 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 df5ec79..e4bb522 100644 --- a/src/shared/modules/user/user-service.ts +++ b/src/shared/modules/user/user-service.ts @@ -1,12 +1,13 @@ -import { DocumentType } from '@typegoose/typegoose'; -import { CreateUserDto } from './dto/user-dto.js'; -import { UserServiceInterface } from './user-service.interface.js'; -import { UserEntity, UserModel } from './user.entity.js'; -import { inject, injectable } from 'inversify'; -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 { DocumentType } from "@typegoose/typegoose"; +import { CreateUserDto } from "./dto/user-dto.js"; +import { UserServiceInterface } from "./user-service.interface.js"; +import { UserEntity, UserModel } from "./user.entity.js"; +import { inject, injectable } from "inversify"; +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 { @@ -41,13 +42,13 @@ export class UserService implements UserServiceInterface { ): Promise[]> { const user = await this.userModel .findById(userId) - .populate('favorites') + .populate("favorites") .exec(); return (user?.favorites as DocumentType[]) ?? []; } async toggleFavoriteOffer(offerId: string, userId: number): Promise { - const user = await UserModel.findById(userId).populate('favorites').exec(); + const user = await UserModel.findById(userId).populate("favorites").exec(); if (!user) { return; } @@ -62,4 +63,28 @@ export class UserService implements UserServiceInterface { }); } } + + async addFavoriteOffer(offerId: string, userId: string): Promise { + await UserModel.findByIdAndUpdate(userId, { + $addToSet: { favorites: new mongoose.Types.ObjectId(offerId) }, + }); + } + + async deleteFavoriteOffer( + offerId: string, + userId: string, + ): Promise | null> { + const user = await UserModel.findById(userId).populate("favorites").exec(); + if (!user) { + return null; + } + + const result = await UserModel.findByIdAndUpdate( + userId, + { $pull: { favorites: new mongoose.Types.ObjectId(offerId) } }, + { new: true }, + ).exec(); + + return result; + } } From 8b7d5c62e83b3b42fd90d5474310819e2355bed9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 May 2026 14:41:26 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rest/middleware/offer-owner.middleware.ts | 22 ++++++++-------- src/shared/modules/user/user-service.ts | 26 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/shared/libs/rest/middleware/offer-owner.middleware.ts b/src/shared/libs/rest/middleware/offer-owner.middleware.ts index 66abfc7..5f412f6 100644 --- a/src/shared/libs/rest/middleware/offer-owner.middleware.ts +++ b/src/shared/libs/rest/middleware/offer-owner.middleware.ts @@ -1,8 +1,8 @@ -import { Middleware } from "./middleware.interface.js"; -import { OfferServiceInterface } from "../../../modules/offer/offer-service.interface.js"; -import { Request, Response, NextFunction } from "express"; -import { StatusCodes } from "http-status-codes"; -import { HttpError } from "../errors/index.js"; +import { Middleware } from './middleware.interface.js'; +import { OfferServiceInterface } from '../../../modules/offer/offer-service.interface.js'; +import { Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { HttpError } from '../errors/index.js'; export class IsOfferOwnerMiddleware implements Middleware { constructor(private readonly service: OfferServiceInterface) {} @@ -21,8 +21,8 @@ export class IsOfferOwnerMiddleware implements Middleware { return next( new HttpError( StatusCodes.NOT_FOUND, - "Offer not found", - "IsOfferOwnerMiddleware", + 'Offer not found', + 'IsOfferOwnerMiddleware', ), ); } @@ -31,8 +31,8 @@ export class IsOfferOwnerMiddleware implements Middleware { return next( new HttpError( StatusCodes.UNAUTHORIZED, - "Unauthorized", - "IsOfferOwnerMiddleware", + 'Unauthorized', + 'IsOfferOwnerMiddleware', ), ); } @@ -41,8 +41,8 @@ export class IsOfferOwnerMiddleware implements Middleware { return next( new HttpError( StatusCodes.FORBIDDEN, - "You are not the owner of this offer", - "IsOfferOwnerMiddleware", + 'You are not the owner of this offer', + 'IsOfferOwnerMiddleware', ), ); } diff --git a/src/shared/modules/user/user-service.ts b/src/shared/modules/user/user-service.ts index e4bb522..a35f119 100644 --- a/src/shared/modules/user/user-service.ts +++ b/src/shared/modules/user/user-service.ts @@ -1,13 +1,13 @@ -import { DocumentType } from "@typegoose/typegoose"; -import { CreateUserDto } from "./dto/user-dto.js"; -import { UserServiceInterface } from "./user-service.interface.js"; -import { UserEntity, UserModel } from "./user.entity.js"; -import { inject, injectable } from "inversify"; -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"; +import { DocumentType } from '@typegoose/typegoose'; +import { CreateUserDto } from './dto/user-dto.js'; +import { UserServiceInterface } from './user-service.interface.js'; +import { UserEntity, UserModel } from './user.entity.js'; +import { inject, injectable } from 'inversify'; +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 { @@ -42,13 +42,13 @@ export class UserService implements UserServiceInterface { ): Promise[]> { const user = await this.userModel .findById(userId) - .populate("favorites") + .populate('favorites') .exec(); return (user?.favorites as DocumentType[]) ?? []; } async toggleFavoriteOffer(offerId: string, userId: number): Promise { - const user = await UserModel.findById(userId).populate("favorites").exec(); + const user = await UserModel.findById(userId).populate('favorites').exec(); if (!user) { return; } @@ -74,7 +74,7 @@ export class UserService implements UserServiceInterface { offerId: string, userId: string, ): Promise | null> { - const user = await UserModel.findById(userId).populate("favorites").exec(); + const user = await UserModel.findById(userId).populate('favorites').exec(); if (!user) { return null; } From 8e400a7c820b431a42c290062ea35f56414c9c45 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 31 May 2026 14:05:56 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= =?UTF-8?q?=20=D0=B2=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 2 +- src/cli/cli-application.ts | 2 +- .../cli/command.interface.ts | 0 src/cli/generate.command.ts | 3 +- src/cli/help.command.ts | 2 +- src/cli/import.command.ts | 39 ++-- src/cli/version.command.ts | 2 +- src/mock/mock.tsv | 11 +- src/shared/helpers/offer.ts | 3 +- src/shared/modules/offer/offer.entity.ts | 2 +- src/shared/modules/user/dist/user-service.js | 181 ++++++++++++++++++ src/shared/modules/user/dist/user.entity.js | 76 ++++++++ src/shared/modules/user/user-service.ts | 5 - src/shared/types/offer.ts | 1 + 14 files changed, 299 insertions(+), 30 deletions(-) rename "src/cli/command.interfa\321\201e.ts" => src/cli/command.interface.ts (100%) create mode 100644 src/shared/modules/user/dist/user-service.js create mode 100644 src/shared/modules/user/dist/user.entity.js diff --git a/src/cli.ts b/src/cli.ts index 2c32b67..012f2ba 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; import { CliApplication } from './cli/cli-application.js'; -import { Command } from './cli/command.interfaсe.js'; +import { Command } from './cli/command.interface.js'; import { GenerateCommand } from './cli/generate.command.js'; import { HelpCommand } from './cli/help.command.js'; import { ImportCommand } from './cli/import.command.js'; diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts index 9c15570..ba44ebf 100644 --- a/src/cli/cli-application.ts +++ b/src/cli/cli-application.ts @@ -1,5 +1,5 @@ import { CommandParser } from './command-parser.js'; -import { Command } from './command.interfaсe.js'; +import { Command } from './command.interface.js'; type CollectionCommand = Record; diff --git "a/src/cli/command.interfa\321\201e.ts" b/src/cli/command.interface.ts similarity index 100% rename from "src/cli/command.interfa\321\201e.ts" rename to src/cli/command.interface.ts diff --git a/src/cli/generate.command.ts b/src/cli/generate.command.ts index 3ac1050..4c20e5e 100644 --- a/src/cli/generate.command.ts +++ b/src/cli/generate.command.ts @@ -1,6 +1,6 @@ import got from 'got'; import { MockServerData } from '../shared/types/mock-server.js'; -import { Command } from './command.interfaсe.js'; +import { Command } from './command.interface.js'; import { appendFile } from 'node:fs/promises'; import { TSVOfferGenerator } from '../shared/libs/offer-generator/offer-generator.js'; @@ -29,7 +29,6 @@ export class GenerateCommand implements Command { } async execute(...parameters: string[]): Promise { - console.log(parameters); const [count, path, url] = parameters; const offerCount = parseInt(count, 10); diff --git a/src/cli/help.command.ts b/src/cli/help.command.ts index a859e9b..e975289 100644 --- a/src/cli/help.command.ts +++ b/src/cli/help.command.ts @@ -1,4 +1,4 @@ -import { Command } from './command.interfaсe.js'; +import { Command } from './command.interface.js'; export class HelpCommand implements Command { getName(): string { diff --git a/src/cli/import.command.ts b/src/cli/import.command.ts index 263ad08..9f27e3f 100644 --- a/src/cli/import.command.ts +++ b/src/cli/import.command.ts @@ -11,11 +11,17 @@ import { ConsoleLogger } from '../shared/libs/logger/console.logger.js'; import { OfferService } from '../shared/modules/offer/offer-service.js'; import { UserService } from '../shared/modules/user/user-service.js'; import { Offer } from '../shared/types/offer.js'; -import { Command } from './command.interfaсe.js'; import { DEFAULT_DB_PORT, DEFAULT_USER_PASSWORD } from './const.cli.js'; -import { OfferEntity } from '../shared/modules/offer/offer.entity.js'; -import { UserEntity } from '../shared/modules/user/user.entity.js'; -import { CommentEntity } from '../shared/modules/comment/comment.entity.js'; +import { + OfferEntity, + OfferModel, +} from '../shared/modules/offer/offer.entity.js'; +import { UserEntity, UserModel } from '../shared/modules/user/user.entity.js'; +import { + CommentEntity, + CommentModel, +} from '../shared/modules/comment/comment.entity.js'; +import { Command } from './command.interface.js'; export class ImportCommand implements Command { private salt: string; @@ -29,20 +35,28 @@ export class ImportCommand implements Command { constructor() { this.logger = new ConsoleLogger(); + this.databaseClient = new MongoDatabaseClient(this.logger); + } + + public getName(): string { + return '--import'; + } + + private initModels() { + this.userModel = UserModel; + this.offerModel = OfferModel; + this.commentModel = CommentModel; + this.userService = new UserService(this.logger, this.userModel); this.offerService = new OfferService( this.logger, this.offerModel, this.commentModel, ); - this.databaseClient = new MongoDatabaseClient(this.logger); - } - - public getName(): string { - return '--import'; } private async onImportedLine(line: string, resolve: () => void) { + console.log(line); const offer = createOffer(line); await this.saveOffer(offer); resolve(); @@ -53,11 +67,12 @@ export class ImportCommand implements Command { } private async saveOffer(offer: Offer) { + const { id: _id, host, ...offerData } = offer; const user = await this.userService.register( - { ...offer.host, password: DEFAULT_USER_PASSWORD }, + { ...host, password: DEFAULT_USER_PASSWORD }, this.salt, ); - await this.offerService.createOffer({ ...offer, host: user.id }); + await this.offerService.createOffer({ ...offerData, host: user.id }); } public async execute( @@ -73,6 +88,8 @@ export class ImportCommand implements Command { await this.databaseClient.connect(dbURI); + this.initModels(); + const fileReader = new TSVFileReader(filename.trim()); fileReader.on('line', this.onImportedLine.bind(this)); diff --git a/src/cli/version.command.ts b/src/cli/version.command.ts index 740ba0f..c4bcda9 100644 --- a/src/cli/version.command.ts +++ b/src/cli/version.command.ts @@ -1,4 +1,4 @@ -import { Command } from './command.interfaсe.js'; +import { Command } from './command.interface.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; diff --git a/src/mock/mock.tsv b/src/mock/mock.tsv index 9acf1b7..e92201f 100644 --- a/src/mock/mock.tsv +++ b/src/mock/mock.tsv @@ -1,6 +1,5 @@ -id title description postDate city previewImage images isPremium isFavorite rating type bedrooms maxAdults price goods hostId hostName hostEmail hostAvatar hostPassword hostIsPro locationLat locationLng commentsCount -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 john-password 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 maria-password 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 3.8 room 1 2 1200 Laptop friendly workspace 3 Ahmed Ali ahmed@example.com https://example.com/avatar3.jpg ahmed-password 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 emma-password 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 sophie-password false 53.5511 9.9937 17 \ No newline at end of file +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 diff --git a/src/shared/helpers/offer.ts b/src/shared/helpers/offer.ts index fb433ff..9d99368 100644 --- a/src/shared/helpers/offer.ts +++ b/src/shared/helpers/offer.ts @@ -2,7 +2,7 @@ import { Amenity, City, HousingType, Offer } from '../types/offer.js'; export const createOffer = (offerData: string): Offer => { const [ - // id, + id, title, description, postDate, @@ -29,6 +29,7 @@ export const createOffer = (offerData: string): Offer => { ] = offerData.replace('\n', '').split('\t'); return { + id: id, title: title, description: description, date: new Date(postDate), diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index c397e12..dad1dc0 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -89,7 +89,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { 'Baby seat', 'Washer', 'Towels', - 'Towels', + 'Fridge', ], }) public goods!: Amenity[]; diff --git a/src/shared/modules/user/dist/user-service.js b/src/shared/modules/user/dist/user-service.js new file mode 100644 index 0000000..2452a13 --- /dev/null +++ b/src/shared/modules/user/dist/user-service.js @@ -0,0 +1,181 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __param = (this && this.__param) || function (paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); } +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +exports.__esModule = true; +exports.UserService = void 0; +var user_entity_js_1 = require("./user.entity.js"); +var inversify_1 = require("inversify"); +var container_js_1 = require("../../types/container.js"); +var user_constant_js_1 = require("./user.constant.js"); +var mongoose_1 = require("mongoose"); +var UserService = /** @class */ (function () { + function UserService(logger, userModel) { + this.logger = logger; + this.userModel = userModel; + } + UserService.prototype.register = function (dto, salt) { + return __awaiter(this, void 0, Promise, function () { + var user, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + user = new user_entity_js_1.UserEntity(__assign(__assign({}, dto), { avatar: user_constant_js_1.DEFAULT_AVATAR_FILE_NAME })); + user.setPassword(dto.password, salt); + return [4 /*yield*/, user_entity_js_1.UserModel.create(user)]; + case 1: + result = _a.sent(); + this.logger.info("User created: " + user.email); + return [2 /*return*/, result]; + } + }); + }); + }; + UserService.prototype.findByEmail = function (email) { + return __awaiter(this, void 0, Promise, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.userModel.findOne({ email: email }).exec()]; + }); + }); + }; + UserService.prototype.findById = function (id) { + return __awaiter(this, void 0, Promise, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.userModel.findById(id).exec()]; + }); + }); + }; + UserService.prototype.findFavoriteOffers = function (userId) { + var _a; + return __awaiter(this, void 0, Promise, function () { + var user; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, this.userModel + .findById(userId) + .populate("favorites") + .exec()]; + case 1: + user = _b.sent(); + return [2 /*return*/, (_a = user === null || user === void 0 ? void 0 : user.favorites) !== null && _a !== void 0 ? _a : []]; + } + }); + }); + }; + UserService.prototype.toggleFavoriteOffer = function (offerId, userId) { + return __awaiter(this, void 0, Promise, function () { + var user; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, user_entity_js_1.UserModel.findById(userId).populate("favorites").exec()]; + case 1: + user = _a.sent(); + if (!user) { + return [2 /*return*/]; + } + if (!user.favorites.find(function (offer) { return offer._id.toString() === offerId; })) return [3 /*break*/, 3]; + return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { + $pull: { favorites: offerId } + })]; + case 2: + _a.sent(); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { + $push: { favorites: offerId } + })]; + case 4: + _a.sent(); + _a.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); + }; + UserService.prototype.addFavoriteOffer = function (offerId, userId) { + return __awaiter(this, void 0, Promise, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { + $addToSet: { favorites: new mongoose_1["default"].Types.ObjectId(offerId) } + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + UserService.prototype.deleteFavoriteOffer = function (offerId, userId) { + return __awaiter(this, void 0, Promise, function () { + var result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { $pull: { favorites: new mongoose_1["default"].Types.ObjectId(offerId) } }, { "new": true }).exec()]; + case 1: + result = _a.sent(); + return [2 /*return*/, result]; + } + }); + }); + }; + UserService = __decorate([ + inversify_1.injectable(), + __param(0, inversify_1.inject(container_js_1.Component.Logger)), + __param(1, inversify_1.inject(container_js_1.Component.UserModel)) + ], UserService); + return UserService; +}()); +exports.UserService = UserService; diff --git a/src/shared/modules/user/dist/user.entity.js b/src/shared/modules/user/dist/user.entity.js new file mode 100644 index 0000000..8a2ea30 --- /dev/null +++ b/src/shared/modules/user/dist/user.entity.js @@ -0,0 +1,76 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +exports.__esModule = true; +exports.UserModel = exports.UserEntity = void 0; +var hash_js_1 = require("../../helpers/hash.js"); +var typegoose_1 = require("@typegoose/typegoose"); +var UserEntity = /** @class */ (function (_super) { + __extends(UserEntity, _super); + function UserEntity(userData) { + var _this = _super.call(this) || this; + _this.name = userData.name; + _this.email = userData.email; + _this.avatar = userData.avatar; + _this.password = userData.password; + _this.isPro = userData.isPro; + return _this; + } + UserEntity.prototype.setPassword = function (password, salt) { + this.password = hash_js_1.createSha256(password, salt); + }; + UserEntity.prototype.verifyPassword = function (password, salt) { + var hash = hash_js_1.createSha256(password, salt); + return this.password === hash; + }; + UserEntity.prototype.getPassword = function () { + return this.password; + }; + __decorate([ + typegoose_1.prop({ required: true, minlength: 1, maxlength: 15, "default": "" }) + ], UserEntity.prototype, "name"); + __decorate([ + typegoose_1.prop({ required: true, "default": "", unique: true }) + ], UserEntity.prototype, "email"); + __decorate([ + typegoose_1.prop({ required: false, "default": "" }) + ], UserEntity.prototype, "avatar"); + __decorate([ + typegoose_1.prop({ required: true, minlength: 6, maxlength: 1024, "default": "" }) + ], UserEntity.prototype, "password"); + __decorate([ + typegoose_1.prop({ required: true }) + ], UserEntity.prototype, "isPro"); + __decorate([ + typegoose_1.prop({ ref: "OfferEntity", "default": [] }) + ], UserEntity.prototype, "favorites"); + UserEntity = __decorate([ + typegoose_1.modelOptions({ + schemaOptions: { + collection: "users", + timestamps: true + } + }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging + ], UserEntity); + return UserEntity; +}(typegoose_1.defaultClasses.TimeStamps)); +exports.UserEntity = UserEntity; +exports.UserModel = typegoose_1.getModelForClass(UserEntity); diff --git a/src/shared/modules/user/user-service.ts b/src/shared/modules/user/user-service.ts index a35f119..17e7415 100644 --- a/src/shared/modules/user/user-service.ts +++ b/src/shared/modules/user/user-service.ts @@ -74,11 +74,6 @@ export class UserService implements UserServiceInterface { offerId: string, userId: string, ): Promise | null> { - const user = await UserModel.findById(userId).populate('favorites').exec(); - if (!user) { - return null; - } - const result = await UserModel.findByIdAndUpdate( userId, { $pull: { favorites: new mongoose.Types.ObjectId(offerId) } }, diff --git a/src/shared/types/offer.ts b/src/shared/types/offer.ts index ee333bf..e26ebdc 100644 --- a/src/shared/types/offer.ts +++ b/src/shared/types/offer.ts @@ -32,6 +32,7 @@ export type Amenity = | 'Fridge'; export interface Offer { + id: string; title: string; description: string; date: Date; From 4c67e33bef576f868d0e68cf672fbbf429d2b895 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 31 May 2026 17:54:36 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B5=20=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=D0=B9=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli/import.command.ts | 1 - src/shared/modules/user/dist/user-service.js | 181 ------------------- src/shared/modules/user/dist/user.entity.js | 76 -------- 3 files changed, 258 deletions(-) delete mode 100644 src/shared/modules/user/dist/user-service.js delete mode 100644 src/shared/modules/user/dist/user.entity.js diff --git a/src/cli/import.command.ts b/src/cli/import.command.ts index 9f27e3f..348a647 100644 --- a/src/cli/import.command.ts +++ b/src/cli/import.command.ts @@ -56,7 +56,6 @@ export class ImportCommand implements Command { } private async onImportedLine(line: string, resolve: () => void) { - console.log(line); const offer = createOffer(line); await this.saveOffer(offer); resolve(); diff --git a/src/shared/modules/user/dist/user-service.js b/src/shared/modules/user/dist/user-service.js deleted file mode 100644 index 2452a13..0000000 --- a/src/shared/modules/user/dist/user-service.js +++ /dev/null @@ -1,181 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -var __param = (this && this.__param) || function (paramIndex, decorator) { - return function (target, key) { decorator(target, key, paramIndex); } -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (_) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -exports.__esModule = true; -exports.UserService = void 0; -var user_entity_js_1 = require("./user.entity.js"); -var inversify_1 = require("inversify"); -var container_js_1 = require("../../types/container.js"); -var user_constant_js_1 = require("./user.constant.js"); -var mongoose_1 = require("mongoose"); -var UserService = /** @class */ (function () { - function UserService(logger, userModel) { - this.logger = logger; - this.userModel = userModel; - } - UserService.prototype.register = function (dto, salt) { - return __awaiter(this, void 0, Promise, function () { - var user, result; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - user = new user_entity_js_1.UserEntity(__assign(__assign({}, dto), { avatar: user_constant_js_1.DEFAULT_AVATAR_FILE_NAME })); - user.setPassword(dto.password, salt); - return [4 /*yield*/, user_entity_js_1.UserModel.create(user)]; - case 1: - result = _a.sent(); - this.logger.info("User created: " + user.email); - return [2 /*return*/, result]; - } - }); - }); - }; - UserService.prototype.findByEmail = function (email) { - return __awaiter(this, void 0, Promise, function () { - return __generator(this, function (_a) { - return [2 /*return*/, this.userModel.findOne({ email: email }).exec()]; - }); - }); - }; - UserService.prototype.findById = function (id) { - return __awaiter(this, void 0, Promise, function () { - return __generator(this, function (_a) { - return [2 /*return*/, this.userModel.findById(id).exec()]; - }); - }); - }; - UserService.prototype.findFavoriteOffers = function (userId) { - var _a; - return __awaiter(this, void 0, Promise, function () { - var user; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: return [4 /*yield*/, this.userModel - .findById(userId) - .populate("favorites") - .exec()]; - case 1: - user = _b.sent(); - return [2 /*return*/, (_a = user === null || user === void 0 ? void 0 : user.favorites) !== null && _a !== void 0 ? _a : []]; - } - }); - }); - }; - UserService.prototype.toggleFavoriteOffer = function (offerId, userId) { - return __awaiter(this, void 0, Promise, function () { - var user; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, user_entity_js_1.UserModel.findById(userId).populate("favorites").exec()]; - case 1: - user = _a.sent(); - if (!user) { - return [2 /*return*/]; - } - if (!user.favorites.find(function (offer) { return offer._id.toString() === offerId; })) return [3 /*break*/, 3]; - return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { - $pull: { favorites: offerId } - })]; - case 2: - _a.sent(); - return [3 /*break*/, 5]; - case 3: return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { - $push: { favorites: offerId } - })]; - case 4: - _a.sent(); - _a.label = 5; - case 5: return [2 /*return*/]; - } - }); - }); - }; - UserService.prototype.addFavoriteOffer = function (offerId, userId) { - return __awaiter(this, void 0, Promise, function () { - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { - $addToSet: { favorites: new mongoose_1["default"].Types.ObjectId(offerId) } - })]; - case 1: - _a.sent(); - return [2 /*return*/]; - } - }); - }); - }; - UserService.prototype.deleteFavoriteOffer = function (offerId, userId) { - return __awaiter(this, void 0, Promise, function () { - var result; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, user_entity_js_1.UserModel.findByIdAndUpdate(userId, { $pull: { favorites: new mongoose_1["default"].Types.ObjectId(offerId) } }, { "new": true }).exec()]; - case 1: - result = _a.sent(); - return [2 /*return*/, result]; - } - }); - }); - }; - UserService = __decorate([ - inversify_1.injectable(), - __param(0, inversify_1.inject(container_js_1.Component.Logger)), - __param(1, inversify_1.inject(container_js_1.Component.UserModel)) - ], UserService); - return UserService; -}()); -exports.UserService = UserService; diff --git a/src/shared/modules/user/dist/user.entity.js b/src/shared/modules/user/dist/user.entity.js deleted file mode 100644 index 8a2ea30..0000000 --- a/src/shared/modules/user/dist/user.entity.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -exports.__esModule = true; -exports.UserModel = exports.UserEntity = void 0; -var hash_js_1 = require("../../helpers/hash.js"); -var typegoose_1 = require("@typegoose/typegoose"); -var UserEntity = /** @class */ (function (_super) { - __extends(UserEntity, _super); - function UserEntity(userData) { - var _this = _super.call(this) || this; - _this.name = userData.name; - _this.email = userData.email; - _this.avatar = userData.avatar; - _this.password = userData.password; - _this.isPro = userData.isPro; - return _this; - } - UserEntity.prototype.setPassword = function (password, salt) { - this.password = hash_js_1.createSha256(password, salt); - }; - UserEntity.prototype.verifyPassword = function (password, salt) { - var hash = hash_js_1.createSha256(password, salt); - return this.password === hash; - }; - UserEntity.prototype.getPassword = function () { - return this.password; - }; - __decorate([ - typegoose_1.prop({ required: true, minlength: 1, maxlength: 15, "default": "" }) - ], UserEntity.prototype, "name"); - __decorate([ - typegoose_1.prop({ required: true, "default": "", unique: true }) - ], UserEntity.prototype, "email"); - __decorate([ - typegoose_1.prop({ required: false, "default": "" }) - ], UserEntity.prototype, "avatar"); - __decorate([ - typegoose_1.prop({ required: true, minlength: 6, maxlength: 1024, "default": "" }) - ], UserEntity.prototype, "password"); - __decorate([ - typegoose_1.prop({ required: true }) - ], UserEntity.prototype, "isPro"); - __decorate([ - typegoose_1.prop({ ref: "OfferEntity", "default": [] }) - ], UserEntity.prototype, "favorites"); - UserEntity = __decorate([ - typegoose_1.modelOptions({ - schemaOptions: { - collection: "users", - timestamps: true - } - }) - // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging - ], UserEntity); - return UserEntity; -}(typegoose_1.defaultClasses.TimeStamps)); -exports.UserEntity = UserEntity; -exports.UserModel = typegoose_1.getModelForClass(UserEntity);