diff --git a/specification/specification.yml b/specification/specification.yml index f5b72a7..4c8f7fe 100644 --- a/specification/specification.yml +++ b/specification/specification.yml @@ -178,7 +178,7 @@ paths: "400": description: Введена некорректная информация - "403": + "401": description: Только авторизованные пользователи могут создавать предложение get: @@ -226,9 +226,12 @@ paths: "400": description: Данные введены некорректно - "403": + "401": description: Только авторизованные пользователи могут редактировать предложение + "403": + description: Доступ к предложению запрещен + delete: tags: - offers @@ -242,9 +245,12 @@ paths: "404": description: Предложение с таким id не найдено. - "403": + "401": description: Только авторизованные пользователи могут удалять предложение + "403": + description: Доступ к предложению запрещен + get: tags: - offers @@ -330,7 +336,7 @@ paths: "404": description: Предложение с таким id не найдено. - "403": + "401": description: Только авторизованные пользователи могут оставлять комментарии components: diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index 72a98d5..71e00be 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -15,8 +15,6 @@ import { ILogger } from '../../libs/logger/types/index.js'; import { ECity, ESortType } from '../../types/index.js'; import { OfferEntity } from './offer.entity.js'; -const MOCK_USER = '66f947e7e706754fb39b93a7'; - @injectable() export class DefaultOfferService implements IOfferService { constructor( @@ -36,26 +34,26 @@ export class DefaultOfferService implements IOfferService { return result; } - public async findById(offerId: string): Promise | null> { + public async findById(offerId: string, userId: string): Promise | null> { const result = await this.offerModel .aggregate([ { $match: { '_id': new Types.ObjectId(offerId) } }, ...populateComments, ...populateAuthor, - ...getIsFavorite(MOCK_USER, offerId), + ...getIsFavorite(userId, offerId), ]) .exec(); return result[0] || null; } - public async find(count?: number): Promise[]> { + public async find(count: number, userId: string): Promise[]> { const limit = count || DEFAULT_OFFER_COUNT; const result = await this.offerModel .aggregate([ ...populateComments, - ...getIsFavorite(MOCK_USER), + ...getIsFavorite(userId), { $sort: { createdAt: ESortType.Desc } }, { $limit: limit }, ]) @@ -64,7 +62,7 @@ export class DefaultOfferService implements IOfferService { return result; } - public async findPremium(city: ECity): Promise[]> { + public async findPremium(city: ECity, userId: string): Promise[]> { return this.offerModel .aggregate([ { $match: { @@ -72,20 +70,26 @@ export class DefaultOfferService implements IOfferService { isPremium: true, } }, ...populateComments, - ...getIsFavorite(MOCK_USER), + ...getIsFavorite(userId), { $sort: { createdAt: ESortType.Desc } }, { $limit: MAX_PREMIUM_NUMBER }, ]); } - public async updateById(offerId: string, dto: UpdateOfferDTO): Promise | null> { + public async updateById(offerId: string, _userId: string, dto: UpdateOfferDTO): Promise | null> { return this.offerModel .findByIdAndUpdate(offerId, dto, { new: true }); } - public async deleteById(offerId: string): Promise | null> { + public async deleteById(offerId: string, _userId: string): Promise | null> { return this.offerModel .findByIdAndDelete(offerId) .exec(); } + + public async isOwnOffer(offerId: string, userId: string): Promise { + const offer = await this.offerModel.findOne({ _id: offerId }); + + return offer?.authorId?.toString() === userId; + } } diff --git a/src/shared/modules/offer/offer.aggregation.ts b/src/shared/modules/offer/offer.aggregation.ts index 0ddb658..147ae2b 100644 --- a/src/shared/modules/offer/offer.aggregation.ts +++ b/src/shared/modules/offer/offer.aggregation.ts @@ -53,20 +53,28 @@ export const populateComments = [ { $unset: 'comments' }, ]; -export const getIsFavorite = (userId: string, offerId: string = '') => ([ - { - $lookup: { - from: 'users', - pipeline: [ - { $match: { '_id': new Types.ObjectId(userId) } }, - { $project: { favorites: 1 } } - ], - as: 'currentUser' - }, - }, - { $unwind: '$currentUser' }, - { $addFields: { isFavorite: { - $in: [offerId ? new Types.ObjectId(offerId) : '$_id' , '$currentUser.favorites'] - } }}, - { $unset: 'currentUser' } -]); +export const getIsFavorite = (userId: string, offerId: string = '') => { + if (userId) { + return [ + { + $lookup: { + from: 'users', + pipeline: [ + { $match: { '_id': new Types.ObjectId(userId) } }, + { $project: { favorites: 1 } } + ], + as: 'currentUser' + }, + }, + { $unwind: '$currentUser' }, + { $addFields: { isFavorite: { + $in: [offerId ? new Types.ObjectId(offerId) : '$_id' , '$currentUser.favorites'] + } }}, + { $unset: 'currentUser' } + ]; + } + + return [ + { $addFields: { isFavorite: false } }, + ]; +}; diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 630d153..af72bf4 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -81,7 +81,7 @@ export class OfferController extends BaseController { }); } - public async index({ query }: Request, res: Response): Promise { + public async index({ query, tokenPayload }: Request, res: Response): Promise { if (query.count !== undefined && !Number.parseInt(query.count as string, RADIX)) { throw new HttpError( StatusCodes.BAD_REQUEST, @@ -89,7 +89,10 @@ export class OfferController extends BaseController { 'UserController', ); } - const offers = await this.offerService.find(Number.parseInt(query?.count as string, RADIX)); + const offers = await this.offerService.find( + Number.parseInt(query?.count as string, RADIX), + tokenPayload?.id, + ); const responseData = fillDTO(ShortOfferRDO, offers); this.ok(res, responseData); } @@ -102,31 +105,50 @@ export class OfferController extends BaseController { this.created(res, fillDTO(FullOfferRDO, result)); } - public async show(req: Request, res: Response): Promise { - const existsOffer = await this.offerService.findById(req.params.offerId); + public async show({ tokenPayload, params }: Request, res: Response): Promise { + const existsOffer = await this.offerService.findById(params.offerId, tokenPayload?.id); const responseData = fillDTO(FullOfferRDO, existsOffer); this.ok(res, responseData); } - public async delete(req: Request, res: Response): Promise { - const result = await this.offerService.deleteById(req.params.offerId); + public async delete({ tokenPayload, params }: Request, res: Response): Promise { + if (! (await this.offerService.isOwnOffer(params.offerId, tokenPayload.id))) { + throw new HttpError( + StatusCodes.FORBIDDEN, + 'Forbidden', + 'OfferController', + ); + } + const result = await this.offerService.deleteById(params.offerId, tokenPayload.id); this.noContent(res, result); } public async update( - req: Request, + { body, params, tokenPayload }: Request, res: Response, ): Promise { - const result = await this.offerService.updateById(req.params.offerId, req.body as UpdateOfferDTO); + if (! (await this.offerService.isOwnOffer(params.offerId, tokenPayload.id))) { + throw new HttpError( + StatusCodes.FORBIDDEN, + 'Forbidden', + 'OfferController', + ); + } + + const result = await this.offerService.updateById( + params.offerId, + tokenPayload.id, + body as UpdateOfferDTO, + ); this.ok(res, fillDTO(FullOfferRDO, result)); } public async premium( - { body }: Request, Record, PremiumOfferDTO>, + { body, tokenPayload }: Request, Record, PremiumOfferDTO>, res: Response, ): Promise { - const result = await this.offerService.findPremium(body.city); + const result = await this.offerService.findPremium(body.city, tokenPayload?.id); this.ok(res, fillDTO(FullOfferRDO, result)); } } diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http index 7dca8ee..5ba1395 100644 --- a/src/shared/modules/offer/offer.http +++ b/src/shared/modules/offer/offer.http @@ -2,18 +2,21 @@ ## Получить список предложений GET http://localhost:4000/offers HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU ### ## Получить предложение по id GET http://localhost:4000/offers/66f947e7e706754fb39b93ae HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU ### ## Удалить предложение по id -DELETE http://localhost:4000/offers/66f947e7e706754fb39b93a4 HTTP/1.1 +DELETE http://localhost:4000/offers/672555f786660d7734313d1d HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU ### @@ -30,20 +33,20 @@ Content-Type: application/json ## Создать предложение POST http://localhost:4000/offers HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU { - "title": "ычмчам23423 ыыыСозданный432423423", - "description": "JGdsfdfsdf", + "title": "For test eeeeee", + "description": "JGdsfdfsdf dwewedwe ", "city": "Paris", "previewImagePath": "sfsdf.jpg", - "photos": ["sdfsfaa.jpg", "sccccc.jpg"], + "photos": ["sdfsfaa.jpg", "scc3ccc.jpg", "sccccc.jpg", "sccc.jpg", "scccc.jpg", "scceccc.jpg"], "isPremium": true, "housingType": "house", "roomsNumber": 3, "visitorsNumber": 3, "price": 10000, "facilities": ["Breakfast", "Air conditioning"], - "authorId": "66f947e7e706754fb39b93a7", "coords": { "latitude": 50.846557, "longitude": 4.351697 diff --git a/src/shared/modules/offer/types/offer-service.interface.ts b/src/shared/modules/offer/types/offer-service.interface.ts index 932ea16..76319bc 100644 --- a/src/shared/modules/offer/types/offer-service.interface.ts +++ b/src/shared/modules/offer/types/offer-service.interface.ts @@ -7,9 +7,10 @@ import { ECity } from '../../../types/index.js'; export interface IOfferService extends IDocumentExists { create(dto: CreateOfferDTO): Promise>; - findById(offerId: string): Promise | null>; - find(count?: number): Promise[]>; - deleteById(offerId: string): Promise | null>; - updateById(offerId: string, dto: UpdateOfferDTO): Promise | null>; - findPremium(city: ECity): Promise[]>; + findById(offerId: string, userId: string): Promise | null>; + find(count: number, userId: string): Promise[]>; + deleteById(offerId: string, userId: string): Promise | null>; + updateById(offerId: string, userId: string, dto: UpdateOfferDTO): Promise | null>; + findPremium(city: ECity, userId: string): Promise[]>; + isOwnOffer(offerId: string, userId: string): Promise; } diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 9c1a66a..2989ecf 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -9,6 +9,7 @@ import { ValidateDTOMiddleware, DocumentExistsMiddleware, UploadFileMiddleware, + PrivateRouteMiddleware, } from '../../../rest/index.js'; import { EHttpMethod } from '../../../rest/types/index.js'; import { ILogger } from '../../libs/logger/types/index.js'; @@ -22,8 +23,6 @@ import { ShortOfferRDO } from '../offer/index.js'; import { IAuthService } from '../auth/types/index.js'; import { LoggedUserRDO } from './index.js'; -const MOCK_USER = '66f947e7e706754fb39b93a7'; - @injectable() export class UserController extends BaseController { constructor( @@ -54,12 +53,18 @@ export class UserController extends BaseController { method: EHttpMethod.Get, handler: this.checkAuthenticate, }); - this.addRoute({ path: '/favorites/', method: EHttpMethod.Get, handler: this.showFavorites }); + this.addRoute({ + path: '/favorites/', + method: EHttpMethod.Get, + handler: this.showFavorites, + middlewares: [new PrivateRouteMiddleware()] + }); this.addRoute({ path: '/favorites/:offerId', method: EHttpMethod.Post, handler: this.addFavorite, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), ], @@ -69,6 +74,7 @@ export class UserController extends BaseController { method: EHttpMethod.Delete, handler: this.deleteFavorite, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), ], @@ -78,6 +84,7 @@ export class UserController extends BaseController { method: EHttpMethod.Post, handler: this.uploadAvatar, middlewares: [ + new PrivateRouteMiddleware(), new UploadFileMiddleware(this.config.get('UPLOAD_DIRECTORY'), 'avatarPath'), ] }); @@ -114,13 +121,13 @@ export class UserController extends BaseController { this.ok(res, responseData); } - public async showFavorites(_req: Request, res: Response): Promise { - const result = await this.userService.getFavorites(MOCK_USER); + public async showFavorites({ tokenPayload }: Request, res: Response): Promise { + const result = await this.userService.getFavorites(tokenPayload.id); this.ok(res, fillDTO(ShortOfferRDO, result)); } - public async addFavorite({ params }: Request, res: Response): Promise { - const favorites = await this.userService.getFavorites(MOCK_USER); + public async addFavorite({ params, tokenPayload }: Request, res: Response): Promise { + const favorites = await this.userService.getFavorites(tokenPayload.id); if (favorites.map((item) => item._id.toString()).includes(params.offerId)) { throw new HttpError( @@ -130,12 +137,12 @@ export class UserController extends BaseController { ); } - const result = await this.userService.addFavorite(MOCK_USER, params.offerId); + const result = await this.userService.addFavorite(tokenPayload.id, params.offerId); this.ok(res, fillDTO(UserRDO, result)); } - public async deleteFavorite({ params }: Request, res: Response): Promise { - const result = await this.userService.deleteFavorite(MOCK_USER, params.offerId); + public async deleteFavorite({ params, tokenPayload }: Request, res: Response): Promise { + const result = await this.userService.deleteFavorite(tokenPayload.id, params.offerId); this.ok(res, fillDTO(UserRDO, result)); } diff --git a/src/shared/modules/user/user.http b/src/shared/modules/user/user.http index 503c1c8..6886605 100644 --- a/src/shared/modules/user/user.http +++ b/src/shared/modules/user/user.http @@ -34,15 +34,19 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiL ## Получение списка избранных предложений GET http://localhost:4000/users/favorites HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU ### ## Добавление предложения в избранное POST http://localhost:4000/users/favorites/66f947e7e706754fb39b93ae HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU + ### POST http://localhost:4000/users/favorites/66f947e7e706754fb39b93bb HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU ###