From 7d68905f1ba3f35b34dc5e3a8f37fa1e33504ab6 Mon Sep 17 00:00:00 2001 From: underwater Date: Tue, 26 Dec 2023 19:23:25 +0100 Subject: [PATCH] Feature/use has permission annotation in endpoints (#2771) * Use HasPermission in endpoints * Update changelog --- CHANGELOG.md | 6 + apps/api/src/app/access/access.controller.ts | 27 +-- .../account-balance.controller.ts | 16 +- .../api/src/app/account/account.controller.ts | 58 ++--- apps/api/src/app/admin/admin.controller.ts | 226 +++--------------- .../src/app/admin/queue/queue.controller.ts | 59 +---- .../app/auth-device/auth-device.controller.ts | 36 +-- apps/api/src/app/auth/auth.controller.ts | 5 +- .../src/app/benchmark/benchmark.controller.ts | 43 +--- apps/api/src/app/cache/cache.controller.ts | 35 +-- .../exchange-rate/exchange-rate.controller.ts | 3 +- apps/api/src/app/export/export.controller.ts | 3 +- apps/api/src/app/import/import.controller.ts | 13 +- apps/api/src/app/order/order.controller.ts | 39 +-- .../src/app/platform/platform.controller.ts | 50 +--- .../src/app/portfolio/portfolio.controller.ts | 15 +- .../subscription/subscription.controller.ts | 5 +- apps/api/src/app/symbol/symbol.controller.ts | 5 +- apps/api/src/app/tag/tag.controller.ts | 44 +--- apps/api/src/app/user/user.controller.ts | 14 +- .../src/guards/has-permission.guard.spec.ts | 9 +- apps/api/src/guards/has-permission.guard.ts | 6 +- 22 files changed, 172 insertions(+), 545 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dee565cc8..c9f94abe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Used the `HasPermission` annotation in endpoints + ## 2.32.0 - 2023-12-26 ### Added diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index 59fd8605c..47f6c08b8 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -1,5 +1,7 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { Access } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -28,7 +30,7 @@ export class AccessController { ) {} @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getAllAccesses(): Promise { const accessesWithGranteeUser = await this.accessService.accesses({ include: { @@ -57,20 +59,12 @@ export class AccessController { }); } + @HasPermission(permissions.createAccess) @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async createAccess( @Body() data: CreateAccessDto ): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.createAccess) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.accessService.createAccess({ alias: data.alias || undefined, GranteeUser: data.granteeUserId @@ -81,15 +75,12 @@ export class AccessController { } @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deleteAccess) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteAccess(@Param('id') id: string): Promise { const access = await this.accessService.access({ id }); - if ( - !hasPermission(this.request.user.permissions, permissions.deleteAccess) || - !access || - access.userId !== this.request.user.id - ) { + if (!access || access.userId !== this.request.user.id) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), StatusCodes.FORBIDDEN diff --git a/apps/api/src/app/account-balance/account-balance.controller.ts b/apps/api/src/app/account-balance/account-balance.controller.ts index b12249827..aca528c5f 100644 --- a/apps/api/src/app/account-balance/account-balance.controller.ts +++ b/apps/api/src/app/account-balance/account-balance.controller.ts @@ -1,4 +1,6 @@ -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, @@ -22,8 +24,9 @@ export class AccountBalanceController { @Inject(REQUEST) private readonly request: RequestWithUser ) {} + @HasPermission(permissions.deleteAccountBalance) @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteAccountBalance( @Param('id') id: string ): Promise { @@ -31,14 +34,7 @@ export class AccountBalanceController { id }); - if ( - !hasPermission( - this.request.user.permissions, - permissions.deleteAccountBalance - ) || - !accountBalance || - accountBalance.userId !== this.request.user.id - ) { + if (!accountBalance || accountBalance.userId !== this.request.user.id) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), StatusCodes.FORBIDDEN diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 772a66e4c..ddbe02fcc 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -1,5 +1,7 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; @@ -7,7 +9,7 @@ import { AccountBalancesResponse, Accounts } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { permissions } from '@ghostfolio/common/permissions'; import type { AccountWithValue, RequestWithUser @@ -47,17 +49,9 @@ export class AccountController { ) {} @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deleteAccount) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteAccount(@Param('id') id: string): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.deleteAccount) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const account = await this.accountService.accountWithOrders( { id_userId: { @@ -87,7 +81,7 @@ export class AccountController { } @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAllAccounts( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId @@ -102,7 +96,7 @@ export class AccountController { } @Get(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAccountById( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @@ -122,7 +116,7 @@ export class AccountController { } @Get(':id/balances') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAccountBalancesById( @Param('id') id: string @@ -133,20 +127,12 @@ export class AccountController { }); } + @HasPermission(permissions.createAccount) @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async createAccount( @Body() data: CreateAccountDto ): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.createAccount) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - if (data.platformId) { const platformId = data.platformId; delete data.platformId; @@ -172,20 +158,12 @@ export class AccountController { } } + @HasPermission(permissions.updateAccount) @Post('transfer-balance') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async transferAccountBalance( @Body() { accountIdFrom, accountIdTo, balance }: TransferBalanceDto ) { - if ( - !hasPermission(this.request.user.permissions, permissions.updateAccount) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const accountsOfUser = await this.accountService.getAccounts( this.request.user.id ); @@ -234,18 +212,10 @@ export class AccountController { }); } + @HasPermission(permissions.updateAccount) @Put(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) { - if ( - !hasPermission(this.request.user.permissions, permissions.updateAccount) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const originalAccount = await this.accountService.account({ id_userId: { id, diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e277e77e4..cc4da298b 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -1,3 +1,5 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service'; @@ -59,56 +61,23 @@ export class AdminController { ) {} @Get() - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getAdminData(): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.adminService.get(); } + @HasPermission(permissions.accessAdminControl) @Post('gather') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gather7Days(): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - this.dataGatheringService.gather7Days(); } + @HasPermission(permissions.accessAdminControl) @Post('gather/max') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherMax(): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); await this.dataGatheringService.addJobsToQueue( @@ -130,21 +99,10 @@ export class AdminController { this.dataGatheringService.gatherMax(); } + @HasPermission(permissions.accessAdminControl) @Post('gather/profile-data') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherProfileData(): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); await this.dataGatheringService.addJobsToQueue( @@ -164,24 +122,13 @@ export class AdminController { ); } + @HasPermission(permissions.accessAdminControl) @Post('gather/profile-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherProfileDataForSymbol( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - await this.dataGatheringService.addJobToQueue({ data: { dataSource, @@ -196,47 +143,25 @@ export class AdminController { } @Post('gather/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @HasPermission(permissions.accessAdminControl) public async gatherSymbol( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - this.dataGatheringService.gatherSymbol({ dataSource, symbol }); return; } + @HasPermission(permissions.accessAdminControl) @Post('gather/:dataSource/:symbol/:dateString') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherSymbolForDate( @Param('dataSource') dataSource: DataSource, @Param('dateString') dateString: string, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const date = parseISO(dateString); if (!isDate(date)) { @@ -254,7 +179,8 @@ export class AdminController { } @Get('market-data') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @HasPermission(permissions.accessAdminControl) public async getMarketData( @Query('assetSubClasses') filterByAssetSubClasses?: string, @Query('presetId') presetId?: MarketDataPreset, @@ -264,18 +190,6 @@ export class AdminController { @Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('take') take?: number ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const filters = this.apiService.buildFiltersFromQueryParams({ filterByAssetSubClasses, filterBySearchQuery @@ -292,45 +206,23 @@ export class AdminController { } @Get('market-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getMarketDataBySymbol( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); } + @HasPermission(permissions.accessAdminControl) @Post('market-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updateMarketData( @Body() data: UpdateBulkMarketDataDto, @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ) { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map( ({ date, marketPrice }) => ({ dataSource, @@ -349,26 +241,15 @@ export class AdminController { /** * @deprecated */ + @HasPermission(permissions.accessAdminControl) @Put('market-data/:dataSource/:symbol/:dateString') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async update( @Param('dataSource') dataSource: DataSource, @Param('dateString') dateString: string, @Param('symbol') symbol: string, @Body() data: UpdateMarketDataDto ) { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const date = parseISO(dateString); return this.marketDataService.updateMarketData({ @@ -383,24 +264,14 @@ export class AdminController { }); } + @HasPermission(permissions.accessAdminControl) @Post('profile-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async addProfileData( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } return this.adminService.addAssetProfile({ dataSource, symbol, @@ -409,45 +280,23 @@ export class AdminController { } @Delete('profile-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteProfileData( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.adminService.deleteProfileData({ dataSource, symbol }); } + @HasPermission(permissions.accessAdminControl) @Patch('profile-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async patchAssetProfileData( @Body() assetProfileData: UpdateAssetProfileDto, @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.adminService.patchAssetProfileData({ ...assetProfileData, dataSource, @@ -455,24 +304,13 @@ export class AdminController { }); } + @HasPermission(permissions.accessAdminControl) @Put('settings/:key') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updateProperty( @Param('key') key: string, @Body() data: PropertyDto ) { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return await this.adminService.putSetting(key, data.value); } } diff --git a/apps/api/src/app/admin/queue/queue.controller.ts b/apps/api/src/app/admin/queue/queue.controller.ts index 1dce79c9d..e146804ef 100644 --- a/apps/api/src/app/admin/queue/queue.controller.ts +++ b/apps/api/src/app/admin/queue/queue.controller.ts @@ -1,87 +1,48 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { AdminJobs } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import { permissions } from '@ghostfolio/common/permissions'; import { Controller, Delete, Get, - HttpException, - Inject, Param, Query, UseGuards } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { JobStatus } from 'bull'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { QueueService } from './queue.service'; @Controller('admin/queue') export class QueueController { - public constructor( - private readonly queueService: QueueService, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} + public constructor(private readonly queueService: QueueService) {} @Delete('job') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteJobs( @Query('status') filterByStatus?: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const status = filterByStatus?.split(',') ?? undefined; return this.queueService.deleteJobs({ status }); } @Get('job') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getJobs( @Query('status') filterByStatus?: string ): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const status = filterByStatus?.split(',') ?? undefined; return this.queueService.getJobs({ status }); } @Delete('job/:id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteJob(@Param('id') id: string): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.queueService.deleteJob(id); } } diff --git a/apps/api/src/app/auth-device/auth-device.controller.ts b/apps/api/src/app/auth-device/auth-device.controller.ts index 33eae0cc0..ed936c88d 100644 --- a/apps/api/src/app/auth-device/auth-device.controller.ts +++ b/apps/api/src/app/auth-device/auth-device.controller.ts @@ -1,40 +1,18 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; -import { - Controller, - Delete, - HttpException, - Inject, - Param, - UseGuards -} from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; +import { Controller, Delete, Param, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @Controller('auth-device') export class AuthDeviceController { - public constructor( - private readonly authDeviceService: AuthDeviceService, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} + public constructor(private readonly authDeviceService: AuthDeviceService) {} @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deleteAuthDevice) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteAuthDevice(@Param('id') id: string): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.deleteAuthDevice - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - await this.authDeviceService.deleteAuthDevice({ id }); } } diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 376109b8d..5c4131a56 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -1,4 +1,5 @@ import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { OAuthResponse } from '@ghostfolio/common/interfaces'; @@ -118,13 +119,13 @@ export class AuthController { } @Get('webauthn/generate-registration-options') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async generateRegistrationOptions() { return this.webAuthService.generateRegistrationOptions(); } @Post('webauthn/verify-attestation') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async verifyAttestation( @Body() body: { deviceName: string; credential: AttestationCredentialJSON } ) { diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index 2230ff42b..0fd47f4e2 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -1,3 +1,5 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import type { @@ -5,8 +7,7 @@ import type { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import { permissions } from '@ghostfolio/common/permissions'; import { Body, Controller, @@ -19,7 +20,6 @@ import { UseGuards, UseInterceptors } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -28,26 +28,12 @@ import { BenchmarkService } from './benchmark.service'; @Controller('benchmark') export class BenchmarkController { - public constructor( - private readonly benchmarkService: BenchmarkService, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} + public constructor(private readonly benchmarkService: BenchmarkService) {} + @HasPermission(permissions.accessAdminControl) @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - try { const benchmark = await this.benchmarkService.addBenchmark({ dataSource, @@ -71,23 +57,12 @@ export class BenchmarkController { } @Delete(':dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteBenchmark( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ) { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - try { const benchmark = await this.benchmarkService.deleteBenchmark({ dataSource, @@ -120,7 +95,7 @@ export class BenchmarkController { } @Get(':dataSource/:symbol/:startDateString') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getBenchmarkMarketDataBySymbol( @Param('dataSource') dataSource: DataSource, diff --git a/apps/api/src/app/cache/cache.controller.ts b/apps/api/src/app/cache/cache.controller.ts index 4d8aac5d3..de46c4551 100644 --- a/apps/api/src/app/cache/cache.controller.ts +++ b/apps/api/src/app/cache/cache.controller.ts @@ -1,39 +1,18 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; -import { - Controller, - HttpException, - Inject, - Post, - UseGuards -} from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; +import { Controller, Post, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @Controller('cache') export class CacheController { - public constructor( - private readonly redisCacheService: RedisCacheService, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} + public constructor(private readonly redisCacheService: RedisCacheService) {} + @HasPermission(permissions.accessAdminControl) @Post('flush') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async flushCache(): Promise { - if ( - !hasPermission( - this.request.user.permissions, - permissions.accessAdminControl - ) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.redisCacheService.reset(); } } diff --git a/apps/api/src/app/exchange-rate/exchange-rate.controller.ts b/apps/api/src/app/exchange-rate/exchange-rate.controller.ts index 8e01c4ca9..c66dd08e6 100644 --- a/apps/api/src/app/exchange-rate/exchange-rate.controller.ts +++ b/apps/api/src/app/exchange-rate/exchange-rate.controller.ts @@ -1,3 +1,4 @@ +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { Controller, @@ -19,7 +20,7 @@ export class ExchangeRateController { ) {} @Get(':symbol/:dateString') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getExchangeRate( @Param('dateString') dateString: string, @Param('symbol') symbol: string diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index 51b7bf632..88ba79989 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -1,3 +1,4 @@ +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { Export } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; @@ -14,7 +15,7 @@ export class ExportController { ) {} @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async export( @Query('activityIds') activityIds?: string[] ): Promise { diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index 9fbc8075c..3697afb25 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -1,3 +1,5 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -34,7 +36,8 @@ export class ImportController { ) {} @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @HasPermission(permissions.createOrder) @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async import( @@ -42,11 +45,7 @@ export class ImportController { @Query('dryRun') isDryRun?: boolean ): Promise { if ( - !hasPermission( - this.request.user.permissions, - permissions.createAccount - ) || - !hasPermission(this.request.user.permissions, permissions.createOrder) + !hasPermission(this.request.user.permissions, permissions.createAccount) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -92,7 +91,7 @@ export class ImportController { } @Get('dividends/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async gatherDividends( diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 7e37f22b7..d1e4eadeb 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -1,3 +1,5 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; @@ -44,24 +46,16 @@ export class OrderController { ) {} @Delete() - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deleteOrder) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteOrders(): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.deleteOrder) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.orderService.deleteOrders({ userId: this.request.user.id }); } @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteOrder(@Param('id') id: string): Promise { const order = await this.orderService.order({ id }); @@ -82,7 +76,7 @@ export class OrderController { } @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getAllOrders( @@ -120,19 +114,11 @@ export class OrderController { return { activities, count }; } + @HasPermission(permissions.createOrder) @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async createOrder(@Body() data: CreateOrderDto): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.createOrder) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const order = await this.orderService.createOrder({ ...data, date: parseISO(data.date), @@ -170,19 +156,16 @@ export class OrderController { return order; } + @HasPermission(permissions.updateOrder) @Put(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { const originalOrder = await this.orderService.order({ id }); - if ( - !hasPermission(this.request.user.permissions, permissions.updateOrder) || - !originalOrder || - originalOrder.userId !== this.request.user.id - ) { + if (!originalOrder || originalOrder.userId !== this.request.user.id) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), StatusCodes.FORBIDDEN diff --git a/apps/api/src/app/platform/platform.controller.ts b/apps/api/src/app/platform/platform.controller.ts index 0369da8a3..3bdebd94c 100644 --- a/apps/api/src/app/platform/platform.controller.ts +++ b/apps/api/src/app/platform/platform.controller.ts @@ -1,18 +1,17 @@ -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; import { Body, Controller, Delete, Get, HttpException, - Inject, Param, Post, Put, UseGuards } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { Platform } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -23,49 +22,30 @@ import { UpdatePlatformDto } from './update-platform.dto'; @Controller('platform') export class PlatformController { - public constructor( - private readonly platformService: PlatformService, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} + public constructor(private readonly platformService: PlatformService) {} @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getPlatforms() { return this.platformService.getPlatformsWithAccountCount(); } + @HasPermission(permissions.createPlatform) @Post() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async createPlatform( @Body() data: CreatePlatformDto ): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.createPlatform) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.platformService.createPlatform(data); } + @HasPermission(permissions.updatePlatform) @Put(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updatePlatform( @Param('id') id: string, @Body() data: UpdatePlatformDto ) { - if ( - !hasPermission(this.request.user.permissions, permissions.updatePlatform) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const originalPlatform = await this.platformService.getPlatform({ id }); @@ -88,17 +68,9 @@ export class PlatformController { } @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deletePlatform) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deletePlatform(@Param('id') id: string) { - if ( - !hasPermission(this.request.user.permissions, permissions.deletePlatform) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const originalPlatform = await this.platformService.getPlatform({ id }); diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index dd013989f..e6e7def77 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,5 +1,6 @@ import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { hasNotDefinedValuesInObject, nullifyValuesInObject @@ -61,7 +62,7 @@ export class PortfolioController { ) {} @Get('details') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @@ -204,7 +205,7 @@ export class PortfolioController { } @Get('dividends') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getDividends( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @@ -254,7 +255,7 @@ export class PortfolioController { } @Get('investments') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getInvestments( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @@ -315,7 +316,7 @@ export class PortfolioController { } @Get('performance') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInResponseInterceptor) @Version('2') public async getPerformanceV2( @@ -405,7 +406,7 @@ export class PortfolioController { } @Get('positions') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPositions( @@ -500,7 +501,7 @@ export class PortfolioController { @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getPosition( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Param('dataSource') dataSource, @@ -523,7 +524,7 @@ export class PortfolioController { } @Get('report') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getReport( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string ): Promise { diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 299f32fe0..89a675b99 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -1,3 +1,4 @@ +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { @@ -37,7 +38,7 @@ export class SubscriptionController { @Post('redeem-coupon') @HttpCode(StatusCodes.OK) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) { if (!this.request.user) { throw new HttpException( @@ -109,7 +110,7 @@ export class SubscriptionController { } @Post('stripe/checkout-session') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async createCheckoutSession( @Body() { couponId, priceId }: { couponId: string; priceId: string } ) { diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index ad9042991..2aa2f61df 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,3 +1,4 @@ +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; @@ -34,7 +35,7 @@ export class SymbolController { * Must be before /:symbol */ @Get('lookup') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async lookupSymbol( @Query('includeIndices') includeIndices: boolean = false, @@ -88,7 +89,7 @@ export class SymbolController { } @Get(':dataSource/:symbol/:dateString') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherSymbolForDate( @Param('dataSource') dataSource: DataSource, @Param('dateString') dateString: string, diff --git a/apps/api/src/app/tag/tag.controller.ts b/apps/api/src/app/tag/tag.controller.ts index 950719201..36fb3be20 100644 --- a/apps/api/src/app/tag/tag.controller.ts +++ b/apps/api/src/app/tag/tag.controller.ts @@ -1,18 +1,17 @@ -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { permissions } from '@ghostfolio/common/permissions'; import { Body, Controller, Delete, Get, HttpException, - Inject, Param, Post, Put, UseGuards } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { Tag } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -23,40 +22,25 @@ import { UpdateTagDto } from './update-tag.dto'; @Controller('tag') export class TagController { - public constructor( - @Inject(REQUEST) private readonly request: RequestWithUser, - private readonly tagService: TagService - ) {} + public constructor(private readonly tagService: TagService) {} @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getTags() { return this.tagService.getTagsWithActivityCount(); } @Post() - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.createTag) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async createTag(@Body() data: CreateTagDto): Promise { - if (!hasPermission(this.request.user.permissions, permissions.createTag)) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - return this.tagService.createTag(data); } + @HasPermission(permissions.updateTag) @Put(':id') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) { - if (!hasPermission(this.request.user.permissions, permissions.updateTag)) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const originalTag = await this.tagService.getTag({ id }); @@ -79,15 +63,9 @@ export class TagController { } @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deleteTag) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteTag(@Param('id') id: string) { - if (!hasPermission(this.request.user.permissions, permissions.deleteTag)) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const originalTag = await this.tagService.getTag({ id }); diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 44d21e9c9..97d5aa195 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,3 +1,5 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { User, UserSettings } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -36,12 +38,10 @@ export class UserController { ) {} @Delete(':id') - @UseGuards(AuthGuard('jwt')) + @HasPermission(permissions.deleteUser) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async deleteUser(@Param('id') id: string): Promise { - if ( - !hasPermission(this.request.user.permissions, permissions.deleteUser) || - id === this.request.user.id - ) { + if (id === this.request.user.id) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), StatusCodes.FORBIDDEN @@ -54,7 +54,7 @@ export class UserController { } @Get() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getUser( @Headers('accept-language') acceptLanguage: string ): Promise { @@ -92,7 +92,7 @@ export class UserController { } @Put('setting') - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updateUserSetting(@Body() data: UpdateUserSettingDto) { if ( size(data) === 1 && diff --git a/apps/api/src/guards/has-permission.guard.spec.ts b/apps/api/src/guards/has-permission.guard.spec.ts index 7f5f90de9..d205a28f4 100644 --- a/apps/api/src/guards/has-permission.guard.spec.ts +++ b/apps/api/src/guards/has-permission.guard.spec.ts @@ -1,7 +1,6 @@ import { HttpException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; -import { Test, TestingModule } from '@nestjs/testing'; import { HasPermissionGuard } from './has-permission.guard'; @@ -10,12 +9,8 @@ describe('HasPermissionGuard', () => { let reflector: Reflector; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [HasPermissionGuard, Reflector] - }).compile(); - - guard = module.get(HasPermissionGuard); - reflector = module.get(Reflector); + reflector = new Reflector(); + guard = new HasPermissionGuard(reflector); }); function setupReflectorSpy(returnValue: string) { diff --git a/apps/api/src/guards/has-permission.guard.ts b/apps/api/src/guards/has-permission.guard.ts index 298992d06..871b29f13 100644 --- a/apps/api/src/guards/has-permission.guard.ts +++ b/apps/api/src/guards/has-permission.guard.ts @@ -14,17 +14,17 @@ export class HasPermissionGuard implements CanActivate { public constructor(private reflector: Reflector) {} public canActivate(context: ExecutionContext): boolean { + const { user } = context.switchToHttp().getRequest(); const requiredPermission = this.reflector.get( HAS_PERMISSION_KEY, context.getHandler() ); if (!requiredPermission) { - return true; // No specific permissions required + // No specific permissions required + return true; } - const { user } = context.switchToHttp().getRequest(); - if (!user || !hasPermission(user.permissions, requiredPermission)) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN),