From 43b9dd941b70fd444853a8ee40364938e9d37752 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 15 Jan 2023 08:42:40 +0200 Subject: [PATCH] swagger security, update transaction --- src/app-storage/app-storage.controller.ts | 39 ++++++++- src/app-storage/app-storage.service.ts | 85 ++++++++++++++++--- .../dto/storage-add-item-request.dto.ts | 4 + .../dto/storage-item-response.dto.ts | 5 ++ src/app-storage/dto/storage-response.dto.ts | 1 + src/app-user/app-user.controller.ts | 2 +- src/main.ts | 17 ++-- src/objects/storage/storage.service.ts | 14 +++ 8 files changed, 147 insertions(+), 20 deletions(-) diff --git a/src/app-storage/app-storage.controller.ts b/src/app-storage/app-storage.controller.ts index 402626f..3d1d0f5 100644 --- a/src/app-storage/app-storage.controller.ts +++ b/src/app-storage/app-storage.controller.ts @@ -13,6 +13,7 @@ import { UseInterceptors, } from '@nestjs/common'; import { + ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, @@ -39,6 +40,7 @@ import { StorageAddItemRequestDto, StorageItemUpdateRequestDto, StorageStoredItemTransactionRequestDto, + StorageStoredItemTransactionUpdateRequestDto, StorageStoredItemUpdateRequestDto, } from './dto/storage-add-item-request.dto'; import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; @@ -47,6 +49,7 @@ import { StorageItemSearchResponseDto, StorageStoredItemResponseDto, StorageTransactionResponseDto, + StorageTransactionWithActorResponseDto, } from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, @@ -64,6 +67,7 @@ import { StorageSetResponseDto } from './dto/storage-set-response.dto'; }) @ApiTags('storage') @ApiSecurity('Bearer token') +@ApiBearerAuth('Bearer token') @UseInterceptors(ClassSerializerInterceptor) @UseGuards(AuthGuard, BuildingGuard, RoomGuard, StorageGuard) export class AppStorageController { @@ -232,7 +236,7 @@ export class AppStorageController { return this.service.updateStoredItemDetails(storage, storedItemId, body); } - @Post('item/:storageId/:storedItemId/transaction') + @Post('item/:storageId/:storedItemId/transactions') @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) @ApiOperation({ summary: 'Create a new stored item transaction' }) @ApiBody({ type: StorageStoredItemTransactionRequestDto }) @@ -251,6 +255,39 @@ export class AppStorageController { ); } + @Get('item/:storageId/:storedItemId/transactions') + @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) + @ApiOperation({ summary: 'Get a stored items transactions' }) + @ApiOkResponse({ + type: StorageTransactionWithActorResponseDto, + isArray: true, + }) + async getStoredItemTransactions( + @Param('storedItemId', ParseIntPipe) storedItemId: number, + @CurrentStorage() storage: Storage, + ) { + return this.service.getStoredItemTransaction(storage, storedItemId); + } + + @Patch('item/:storageId/:storedItemId/transactions/:transactionId') + @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) + @ApiOperation({ summary: 'Update a stored item transaction' }) + @ApiBody({ type: StorageStoredItemTransactionUpdateRequestDto }) + @ApiOkResponse({ type: StorageTransactionResponseDto }) + async updateStoredItemTransaction( + @LoggedInUser() user: User, + @Param('storedItemId', ParseIntPipe) storedItemId: number, + @Param('transactionId', ParseIntPipe) transactionId: number, + @Body() body: StorageStoredItemTransactionUpdateRequestDto, + ) { + return this.service.updateStoredItemTransaction( + user, + storedItemId, + transactionId, + body, + ); + } + @Get('item/:storageId/:storedItemId') @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) @ApiOperation({ summary: 'Get a stored items details' }) diff --git a/src/app-storage/app-storage.service.ts b/src/app-storage/app-storage.service.ts index 91d7030..4eab5dd 100644 --- a/src/app-storage/app-storage.service.ts +++ b/src/app-storage/app-storage.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; import omit from 'lodash.omit'; import pick from 'lodash.pick'; import { BuildingService } from 'src/objects/building/building.service'; @@ -17,6 +21,7 @@ import { StorageItemUpdateRequestDto, StorageStoredItemRequestDto, StorageStoredItemTransactionRequestDto, + StorageStoredItemTransactionUpdateRequestDto, StorageStoredItemUpdateRequestDto, } from './dto/storage-add-item-request.dto'; import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; @@ -25,6 +30,7 @@ import { StorageItemSearchResponseDto, StorageStoredItemResponseDto, StorageTransactionResponseDto, + StorageTransactionWithActorResponseDto, } from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, @@ -268,6 +274,7 @@ export class AppStorageService { storage, storedItemId, ); + if (!storedItem) { throw new NotFoundException('Stored item not found'); } @@ -289,14 +296,59 @@ export class AppStorageService { await this.storageService.saveStoredItem(storedItem); } - return this.formatTransactionNoDetails(transaction); + return this.formatTransaction(transaction); + } + + async updateStoredItemTransaction( + user: User, + storedItemId: number, + transactionId: number, + body: StorageStoredItemTransactionUpdateRequestDto, + ) { + const storedItemTransaction = + await this.storageService.getStoredItemTransactionById( + storedItemId, + transactionId, + ['actor'], + ); + + if (!storedItemTransaction) { + throw new NotFoundException('Stored item not found'); + } + + if (storedItemTransaction.actor.sub === user.sub) { + throw new UnauthorizedException( + 'You can only update your own transactions', + ); + } + + Object.assign(storedItemTransaction, body); + await this.storageService.saveStoredItemTransaction(storedItemTransaction); + + return this.formatTransactionWithActor(storedItemTransaction); + } + + async getStoredItemTransaction(storage: Storage, storedItemId: number) { + const storedItem = await this.storageService.getStoredItemByStorageAndId( + storage, + storedItemId, + ['transactions', 'transactions.actor'], + ); + + if (!storedItem) { + throw new NotFoundException('Stored item not found'); + } + + return storedItem.transactions.map((transaction) => + this.formatTransactionWithActor(transaction), + ); } async getStoredItemDetails(storage: Storage, storedItemId: number) { const storedItem = await this.storageService.getStoredItemByStorageAndId( storage, storedItemId, - ['transactions', 'item', 'addedBy'], + ['item', 'addedBy'], ); if (!storedItem) { throw new NotFoundException('Stored item not found'); @@ -315,18 +367,18 @@ export class AppStorageService { return this.formatStorageWithItems(storage); } - formatActor(input: User): StorageActorResponse { - return pick(input, ['name', 'sub', 'color']); + private formatActor(input: User): StorageActorResponse { + return pick(input, ['name', 'sub', 'picture', 'color']); } - formatStorageNoItems(storage: Storage): StorageResponseDto { + private formatStorageNoItems(storage: Storage): StorageResponseDto { return { ...omit(storage, ['room', 'set', 'items']), addedBy: storage.addedBy && this.formatActor(storage.addedBy), }; } - formatStorageWithItems(storage: Storage): StorageResponseDto { + private formatStorageWithItems(storage: Storage): StorageResponseDto { return { ...omit(storage, ['room', 'set']), items: !!storage.items?.length @@ -336,25 +388,36 @@ export class AppStorageService { }; } - formatItem(item: Item): StorageItemResponseDto { + private formatItem(item: Item): StorageItemResponseDto { return { ...omit(item, ['instances', 'addedBy']), addedBy: item.addedBy && this.formatActor(item.addedBy), }; } - formatTransactionNoDetails( + private formatTransaction( transaction: StoredItemTransaction, ): StorageTransactionResponseDto { return omit(transaction, ['storedItem', 'actor']); } - formatStoredItem(storedItem: StoredItem): StorageStoredItemResponseDto { + private formatTransactionWithActor( + transaction: StoredItemTransaction, + ): StorageTransactionWithActorResponseDto { + return { + ...omit(transaction, ['storedItem']), + actor: transaction.actor && this.formatActor(transaction.actor), + }; + } + + private formatStoredItem( + storedItem: StoredItem, + ): StorageStoredItemResponseDto { return { ...omit(storedItem, ['storage']), transactions: !!storedItem.transactions?.length ? storedItem.transactions.map((transaction) => - this.formatTransactionNoDetails(transaction), + this.formatTransaction(transaction), ) : null, item: storedItem.item ? this.formatItem(storedItem.item) : null, diff --git a/src/app-storage/dto/storage-add-item-request.dto.ts b/src/app-storage/dto/storage-add-item-request.dto.ts index 6851ef8..2d3d7ef 100644 --- a/src/app-storage/dto/storage-add-item-request.dto.ts +++ b/src/app-storage/dto/storage-add-item-request.dto.ts @@ -120,6 +120,10 @@ export class StorageStoredItemTransactionRequestDto { actionAt?: Date; } +export class StorageStoredItemTransactionUpdateRequestDto extends PartialType( + StorageStoredItemTransactionRequestDto, +) {} + export class StorageAddExistingItemRequestDto { @ApiPropertyOptional({ type: StorageStoredItemRequestDto }) @Type(() => StorageStoredItemRequestDto) diff --git a/src/app-storage/dto/storage-item-response.dto.ts b/src/app-storage/dto/storage-item-response.dto.ts index cee17fc..bb568d1 100644 --- a/src/app-storage/dto/storage-item-response.dto.ts +++ b/src/app-storage/dto/storage-item-response.dto.ts @@ -27,6 +27,11 @@ export class StorageTransactionResponseDto extends OmitType( ['storedItem', 'actor'], ) {} +export class StorageTransactionWithActorResponseDto extends StorageTransactionResponseDto { + @ApiProperty({ type: StorageActorResponse }) + actor: StorageActorResponse; +} + export class StorageStoredItemResponseDto extends OmitType(StoredItem, [ 'addedBy', 'transactions', diff --git a/src/app-storage/dto/storage-response.dto.ts b/src/app-storage/dto/storage-response.dto.ts index 905aeaf..e79171e 100644 --- a/src/app-storage/dto/storage-response.dto.ts +++ b/src/app-storage/dto/storage-response.dto.ts @@ -6,6 +6,7 @@ import { StorageStoredItemResponseDto } from './storage-item-response.dto'; export class StorageActorResponse extends PickType(User, [ 'sub', 'name', + 'picture', 'color', ]) {} diff --git a/src/app-user/app-user.controller.ts b/src/app-user/app-user.controller.ts index 9f8c206..eee8ad5 100644 --- a/src/app-user/app-user.controller.ts +++ b/src/app-user/app-user.controller.ts @@ -42,7 +42,7 @@ export class AppUserController { @Get() @UseGuards(AuthGuard) @ApiOperation({ summary: 'Get logged in user' }) - @ApiBearerAuth('Bearer auth') + @ApiBearerAuth('Bearer token') async userGetInfo(@LoggedInUser() user: User) { return { name: user.name, diff --git a/src/main.ts b/src/main.ts index d421a49..9cd10cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,14 +15,17 @@ async function bootstrap() { .addTag('groups') .addTag('storage') .addTag('buildings') - .addBearerAuth({ - name: 'Bearer token', - type: 'apiKey', - }) - .addBasicAuth({ - name: 'Email and Password', - description: 'For acquiring a Bearer token', + .addSecurity('Bearer token', { + name: 'Authorization', type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + in: 'header', + }) + .addSecurity('Email and Password', { + type: 'http', + description: 'For acquiring the access token', + scheme: 'basic', }) .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/src/objects/storage/storage.service.ts b/src/objects/storage/storage.service.ts index 06f8d51..26561ff 100644 --- a/src/objects/storage/storage.service.ts +++ b/src/objects/storage/storage.service.ts @@ -272,6 +272,20 @@ export class StorageService { }); } + async getStoredItemTransactionById( + storedItemId: number, + storedItemTransactionId: number, + relations = [], + ) { + return this.transactionRepository.findOne({ + where: { + id: storedItemTransactionId, + storedItem: { id: storedItemId }, + }, + relations, + }); + } + async saveStorage(data: Partial) { const newStorage = new Storage(); Object.assign(newStorage, data);