Browse Source

Merge remote-tracking branch 'origin/main' into merge/main-in-dockerpush

pull/5027/head
Daniel Devaud 2 years ago
parent
commit
6ec9d4a2f7
  1. 11
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 17
      CHANGELOG.md
  3. 4
      apps/api/project.json
  4. 27
      apps/api/src/app/access/access.controller.ts
  5. 16
      apps/api/src/app/account-balance/account-balance.controller.ts
  6. 58
      apps/api/src/app/account/account.controller.ts
  7. 214
      apps/api/src/app/admin/admin.controller.ts
  8. 1
      apps/api/src/app/admin/admin.service.ts
  9. 59
      apps/api/src/app/admin/queue/queue.controller.ts
  10. 36
      apps/api/src/app/auth-device/auth-device.controller.ts
  11. 5
      apps/api/src/app/auth/auth.controller.ts
  12. 43
      apps/api/src/app/benchmark/benchmark.controller.ts
  13. 35
      apps/api/src/app/cache/cache.controller.ts
  14. 3
      apps/api/src/app/exchange-rate/exchange-rate.controller.ts
  15. 3
      apps/api/src/app/export/export.controller.ts
  16. 13
      apps/api/src/app/import/import.controller.ts
  17. 39
      apps/api/src/app/order/order.controller.ts
  18. 50
      apps/api/src/app/platform/platform.controller.ts
  19. 1
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts
  20. 1
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts
  21. 9
      apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  22. 9
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  23. 1
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts
  24. 47
      apps/api/src/app/portfolio/portfolio-calculator.ts
  25. 15
      apps/api/src/app/portfolio/portfolio.controller.ts
  26. 5
      apps/api/src/app/subscription/subscription.controller.ts
  27. 5
      apps/api/src/app/symbol/symbol.controller.ts
  28. 44
      apps/api/src/app/tag/tag.controller.ts
  29. 14
      apps/api/src/app/user/user.controller.ts
  30. 9
      apps/api/src/guards/has-permission.guard.spec.ts
  31. 6
      apps/api/src/guards/has-permission.guard.ts
  32. 2
      apps/client/project.json
  33. 8
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  34. 2
      apps/ui-e2e/project.json
  35. 2
      libs/common/project.json
  36. 1
      libs/common/src/lib/interfaces/timeline-position.interface.ts
  37. 2
      libs/ui/project.json
  38. 8
      package.json
  39. 98
      yarn.lock

11
.github/ISSUE_TEMPLATE/bug_report.md

@ -6,7 +6,13 @@ labels: ''
assignees: '' assignees: ''
--- ---
The Issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) community or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions). **Important Notice**
The issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) community or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
Incomplete or non-reproducible issues may be closed, but we are here to help! If you encounter difficulties reproducing the bug or need assistance, please reach out to our community channels mentioned above.
Thank you for your understanding and cooperation!
**Bug Description** **Bug Description**
@ -36,8 +42,9 @@ The Issue tracker is **ONLY** used for reporting bugs. New features should be di
<!-- Please complete the following information --> <!-- Please complete the following information -->
- Cloud or Self-hosted
- Ghostfolio Version X.Y.Z - Ghostfolio Version X.Y.Z
- Cloud or Self-hosted
- Experimental Features enabled or disabled
- Browser - Browser
- OS - OS

17
CHANGELOG.md

@ -9,9 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Changed the performance calculation to a time-weighted approach
- Used the `HasPermission` annotation in endpoints
## 2.32.0 - 2023-12-26
### Added
- Added support to search for an asset profile by `id` as an administrator
### Changed
- Set the select column of the lazy-loaded activities table to stick at the end (experimental) - Set the select column of the lazy-loaded activities table to stick at the end (experimental)
- Dropped the activity id in the activities import
- Improved the validation of the currency management in the admin control panel - Improved the validation of the currency management in the admin control panel
- Improved the performance of the value redaction interceptor for the impersonation mode by eliminating `cloneDeep` - Improved the performance of the value redaction interceptor for the impersonation mode by eliminating `cloneDeep`
- Modernized the `Nx` executors
- `@nx/eslint:lint`
- `@nx/webpack:webpack`
- Upgraded `prettier` from version `3.1.0` to `3.1.1`
- Upgraded `prisma` from version `5.7.0` to `5.7.1`
### Fixed ### Fixed

4
apps/api/project.json

@ -7,7 +7,7 @@
"generators": {}, "generators": {},
"targets": { "targets": {
"build": { "build": {
"executor": "@nrwl/webpack:webpack", "executor": "@nx/webpack:webpack",
"options": { "options": {
"outputPath": "dist/apps/api", "outputPath": "dist/apps/api",
"main": "apps/api/src/main.ts", "main": "apps/api/src/main.ts",
@ -40,7 +40,7 @@
} }
}, },
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "executor": "@nx/eslint:lint",
"options": { "options": {
"lintFilePatterns": ["apps/api/**/*.ts"] "lintFilePatterns": ["apps/api/**/*.ts"]
} }

27
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 { 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 type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
@ -28,7 +30,7 @@ export class AccessController {
) {} ) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getAllAccesses(): Promise<Access[]> { public async getAllAccesses(): Promise<Access[]> {
const accessesWithGranteeUser = await this.accessService.accesses({ const accessesWithGranteeUser = await this.accessService.accesses({
include: { include: {
@ -57,20 +59,12 @@ export class AccessController {
}); });
} }
@HasPermission(permissions.createAccess)
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createAccess( public async createAccess(
@Body() data: CreateAccessDto @Body() data: CreateAccessDto
): Promise<AccessModel> { ): Promise<AccessModel> {
if (
!hasPermission(this.request.user.permissions, permissions.createAccess)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accessService.createAccess({ return this.accessService.createAccess({
alias: data.alias || undefined, alias: data.alias || undefined,
GranteeUser: data.granteeUserId GranteeUser: data.granteeUserId
@ -81,15 +75,12 @@ export class AccessController {
} }
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deleteAccess)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> { public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
const access = await this.accessService.access({ id }); const access = await this.accessService.access({ id });
if ( if (!access || access.userId !== this.request.user.id) {
!hasPermission(this.request.user.permissions, permissions.deleteAccess) ||
!access ||
access.userId !== this.request.user.id
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN

16
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 type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
@ -22,8 +24,9 @@ export class AccountBalanceController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@HasPermission(permissions.deleteAccountBalance)
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccountBalance( public async deleteAccountBalance(
@Param('id') id: string @Param('id') id: string
): Promise<AccountBalance> { ): Promise<AccountBalance> {
@ -31,14 +34,7 @@ export class AccountBalanceController {
id id
}); });
if ( if (!accountBalance || accountBalance.userId !== this.request.user.id) {
!hasPermission(
this.request.user.permissions,
permissions.deleteAccountBalance
) ||
!accountBalance ||
accountBalance.userId !== this.request.user.id
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN

58
apps/api/src/app/account/account.controller.ts

@ -1,5 +1,7 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.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 { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
@ -7,7 +9,7 @@ import {
AccountBalancesResponse, AccountBalancesResponse,
Accounts Accounts
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { import type {
AccountWithValue, AccountWithValue,
RequestWithUser RequestWithUser
@ -47,17 +49,9 @@ export class AccountController {
) {} ) {}
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deleteAccount)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccount(@Param('id') id: string): Promise<AccountModel> { public async deleteAccount(@Param('id') id: string): Promise<AccountModel> {
if (
!hasPermission(this.request.user.permissions, permissions.deleteAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const account = await this.accountService.accountWithOrders( const account = await this.accountService.accountWithOrders(
{ {
id_userId: { id_userId: {
@ -87,7 +81,7 @@ export class AccountController {
} }
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
public async getAllAccounts( public async getAllAccounts(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId
@ -102,7 +96,7 @@ export class AccountController {
} }
@Get(':id') @Get(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountById( public async getAccountById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@ -122,7 +116,7 @@ export class AccountController {
} }
@Get(':id/balances') @Get(':id/balances')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById( public async getAccountBalancesById(
@Param('id') id: string @Param('id') id: string
@ -133,20 +127,12 @@ export class AccountController {
}); });
} }
@HasPermission(permissions.createAccount)
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createAccount( public async createAccount(
@Body() data: CreateAccountDto @Body() data: CreateAccountDto
): Promise<AccountModel> { ): Promise<AccountModel> {
if (
!hasPermission(this.request.user.permissions, permissions.createAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
if (data.platformId) { if (data.platformId) {
const platformId = data.platformId; const platformId = data.platformId;
delete data.platformId; delete data.platformId;
@ -172,20 +158,12 @@ export class AccountController {
} }
} }
@HasPermission(permissions.updateAccount)
@Post('transfer-balance') @Post('transfer-balance')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async transferAccountBalance( public async transferAccountBalance(
@Body() { accountIdFrom, accountIdTo, balance }: TransferBalanceDto @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( const accountsOfUser = await this.accountService.getAccounts(
this.request.user.id this.request.user.id
); );
@ -234,18 +212,10 @@ export class AccountController {
}); });
} }
@HasPermission(permissions.updateAccount)
@Put(':id') @Put(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) { 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({ const originalAccount = await this.accountService.account({
id_userId: { id_userId: {
id, id,

214
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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
@ -59,56 +61,23 @@ export class AdminController {
) {} ) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getAdminData(): Promise<AdminData> { public async getAdminData(): Promise<AdminData> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.get(); return this.adminService.get();
} }
@HasPermission(permissions.accessAdminControl)
@Post('gather') @Post('gather')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gather7Days(): Promise<void> { public async gather7Days(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
this.dataGatheringService.gather7Days(); this.dataGatheringService.gather7Days();
} }
@HasPermission(permissions.accessAdminControl)
@Post('gather/max') @Post('gather/max')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherMax(): Promise<void> { public async gatherMax(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
await this.dataGatheringService.addJobsToQueue( await this.dataGatheringService.addJobsToQueue(
@ -130,21 +99,10 @@ export class AdminController {
this.dataGatheringService.gatherMax(); this.dataGatheringService.gatherMax();
} }
@HasPermission(permissions.accessAdminControl)
@Post('gather/profile-data') @Post('gather/profile-data')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherProfileData(): Promise<void> { public async gatherProfileData(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
await this.dataGatheringService.addJobsToQueue( await this.dataGatheringService.addJobsToQueue(
@ -164,24 +122,13 @@ export class AdminController {
); );
} }
@HasPermission(permissions.accessAdminControl)
@Post('gather/profile-data/:dataSource/:symbol') @Post('gather/profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherProfileDataForSymbol( public async gatherProfileDataForSymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<void> { ): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.dataGatheringService.addJobToQueue({ await this.dataGatheringService.addJobToQueue({
data: { data: {
dataSource, dataSource,
@ -196,47 +143,25 @@ export class AdminController {
} }
@Post('gather/:dataSource/:symbol') @Post('gather/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@HasPermission(permissions.accessAdminControl)
public async gatherSymbol( public async gatherSymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<void> { ): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
this.dataGatheringService.gatherSymbol({ dataSource, symbol }); this.dataGatheringService.gatherSymbol({ dataSource, symbol });
return; return;
} }
@HasPermission(permissions.accessAdminControl)
@Post('gather/:dataSource/:symbol/:dateString') @Post('gather/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherSymbolForDate( public async gatherSymbolForDate(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<MarketData> { ): Promise<MarketData> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const date = parseISO(dateString); const date = parseISO(dateString);
if (!isDate(date)) { if (!isDate(date)) {
@ -254,7 +179,8 @@ export class AdminController {
} }
@Get('market-data') @Get('market-data')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@HasPermission(permissions.accessAdminControl)
public async getMarketData( public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string, @Query('assetSubClasses') filterByAssetSubClasses?: string,
@Query('presetId') presetId?: MarketDataPreset, @Query('presetId') presetId?: MarketDataPreset,
@ -264,18 +190,6 @@ export class AdminController {
@Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('sortDirection') sortDirection?: Prisma.SortOrder,
@Query('take') take?: number @Query('take') take?: number
): Promise<AdminMarketData> { ): Promise<AdminMarketData> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetSubClasses, filterByAssetSubClasses,
filterBySearchQuery filterBySearchQuery
@ -292,45 +206,23 @@ export class AdminController {
} }
@Get('market-data/:dataSource/:symbol') @Get('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketDataBySymbol( public async getMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> { ): Promise<AdminMarketDataDetails> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
} }
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol') @Post('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateMarketData( public async updateMarketData(
@Body() data: UpdateBulkMarketDataDto, @Body() data: UpdateBulkMarketDataDto,
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @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( const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map(
({ date, marketPrice }) => ({ ({ date, marketPrice }) => ({
dataSource, dataSource,
@ -349,26 +241,15 @@ export class AdminController {
/** /**
* @deprecated * @deprecated
*/ */
@HasPermission(permissions.accessAdminControl)
@Put('market-data/:dataSource/:symbol/:dateString') @Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async update( public async update(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string, @Param('symbol') symbol: string,
@Body() data: UpdateMarketDataDto @Body() data: UpdateMarketDataDto
) { ) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const date = parseISO(dateString); const date = parseISO(dateString);
return this.marketDataService.updateMarketData({ return this.marketDataService.updateMarketData({
@ -383,24 +264,14 @@ export class AdminController {
}); });
} }
@HasPermission(permissions.accessAdminControl)
@Post('profile-data/:dataSource/:symbol') @Post('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async addProfileData( public async addProfileData(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<SymbolProfile | never> { ): Promise<SymbolProfile | never> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.addAssetProfile({ return this.adminService.addAssetProfile({
dataSource, dataSource,
symbol, symbol,
@ -409,28 +280,18 @@ export class AdminController {
} }
@Delete('profile-data/:dataSource/:symbol') @Delete('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteProfileData( public async deleteProfileData(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<void> { ): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.deleteProfileData({ dataSource, symbol }); return this.adminService.deleteProfileData({ dataSource, symbol });
} }
@HasPermission(permissions.accessAdminControl)
@Patch('profile-data/:dataSource/:symbol') @Patch('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async patchAssetProfileData( public async patchAssetProfileData(
@Body() assetProfileData: UpdateAssetProfileDto, @Body() assetProfileData: UpdateAssetProfileDto,
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@ -489,24 +350,13 @@ export class AdminController {
} }
} }
@HasPermission(permissions.accessAdminControl)
@Put('settings/:key') @Put('settings/:key')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateProperty( public async updateProperty(
@Param('key') key: string, @Param('key') key: string,
@Body() data: PropertyDto @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); return await this.adminService.putSetting(key, data.value);
} }
} }

1
apps/api/src/app/admin/admin.service.ts

@ -180,6 +180,7 @@ export class AdminService {
if (searchQuery) { if (searchQuery) {
where.OR = [ where.OR = [
{ id: { mode: 'insensitive', startsWith: searchQuery } },
{ isin: { mode: 'insensitive', startsWith: searchQuery } }, { isin: { mode: 'insensitive', startsWith: searchQuery } },
{ name: { mode: 'insensitive', startsWith: searchQuery } }, { name: { mode: 'insensitive', startsWith: searchQuery } },
{ symbol: { mode: 'insensitive', startsWith: searchQuery } } { symbol: { mode: 'insensitive', startsWith: searchQuery } }

59
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 { AdminJobs } 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 { import {
Controller, Controller,
Delete, Delete,
Get, Get,
HttpException,
Inject,
Param, Param,
Query, Query,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { JobStatus } from 'bull'; import { JobStatus } from 'bull';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { QueueService } from './queue.service'; import { QueueService } from './queue.service';
@Controller('admin/queue') @Controller('admin/queue')
export class QueueController { export class QueueController {
public constructor( public constructor(private readonly queueService: QueueService) {}
private readonly queueService: QueueService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Delete('job') @Delete('job')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteJobs( public async deleteJobs(
@Query('status') filterByStatus?: string @Query('status') filterByStatus?: string
): Promise<void> { ): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined; const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined;
return this.queueService.deleteJobs({ status }); return this.queueService.deleteJobs({ status });
} }
@Get('job') @Get('job')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getJobs( public async getJobs(
@Query('status') filterByStatus?: string @Query('status') filterByStatus?: string
): Promise<AdminJobs> { ): Promise<AdminJobs> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined; const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined;
return this.queueService.getJobs({ status }); return this.queueService.getJobs({ status });
} }
@Delete('job/:id') @Delete('job/:id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteJob(@Param('id') id: string): Promise<void> { public async deleteJob(@Param('id') id: string): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.queueService.deleteJob(id); return this.queueService.deleteJob(id);
} }
} }

36
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 { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import type { RequestWithUser } from '@ghostfolio/common/types'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { import { permissions } from '@ghostfolio/common/permissions';
Controller, import { Controller, Delete, Param, UseGuards } from '@nestjs/common';
Delete,
HttpException,
Inject,
Param,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Controller('auth-device') @Controller('auth-device')
export class AuthDeviceController { export class AuthDeviceController {
public constructor( public constructor(private readonly authDeviceService: AuthDeviceService) {}
private readonly authDeviceService: AuthDeviceService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deleteAuthDevice)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAuthDevice(@Param('id') id: string): Promise<void> { public async deleteAuthDevice(@Param('id') id: string): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.deleteAuthDevice
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.authDeviceService.deleteAuthDevice({ id }); await this.authDeviceService.deleteAuthDevice({ id });
} }
} }

5
apps/api/src/app/auth/auth.controller.ts

@ -1,4 +1,5 @@
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; 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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { OAuthResponse } from '@ghostfolio/common/interfaces'; import { OAuthResponse } from '@ghostfolio/common/interfaces';
@ -118,13 +119,13 @@ export class AuthController {
} }
@Get('webauthn/generate-registration-options') @Get('webauthn/generate-registration-options')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateRegistrationOptions() { public async generateRegistrationOptions() {
return this.webAuthService.generateRegistrationOptions(); return this.webAuthService.generateRegistrationOptions();
} }
@Post('webauthn/verify-attestation') @Post('webauthn/verify-attestation')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async verifyAttestation( public async verifyAttestation(
@Body() body: { deviceName: string; credential: AttestationCredentialJSON } @Body() body: { deviceName: string; credential: AttestationCredentialJSON }
) { ) {

43
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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import type { import type {
@ -5,8 +7,7 @@ import type {
BenchmarkResponse, BenchmarkResponse,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } 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 { import {
Body, Body,
Controller, Controller,
@ -19,7 +20,6 @@ import {
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -28,26 +28,12 @@ import { BenchmarkService } from './benchmark.service';
@Controller('benchmark') @Controller('benchmark')
export class BenchmarkController { export class BenchmarkController {
public constructor( public constructor(private readonly benchmarkService: BenchmarkService) {}
private readonly benchmarkService: BenchmarkService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.accessAdminControl)
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try { try {
const benchmark = await this.benchmarkService.addBenchmark({ const benchmark = await this.benchmarkService.addBenchmark({
dataSource, dataSource,
@ -71,23 +57,12 @@ export class BenchmarkController {
} }
@Delete(':dataSource/:symbol') @Delete(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteBenchmark( public async deleteBenchmark(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
) { ) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try { try {
const benchmark = await this.benchmarkService.deleteBenchmark({ const benchmark = await this.benchmarkService.deleteBenchmark({
dataSource, dataSource,
@ -120,7 +95,7 @@ export class BenchmarkController {
} }
@Get(':dataSource/:symbol/:startDateString') @Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol( public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,

35
apps/api/src/app/cache/cache.controller.ts

@ -1,39 +1,18 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import type { RequestWithUser } from '@ghostfolio/common/types'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { import { permissions } from '@ghostfolio/common/permissions';
Controller, import { Controller, Post, UseGuards } from '@nestjs/common';
HttpException,
Inject,
Post,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Controller('cache') @Controller('cache')
export class CacheController { export class CacheController {
public constructor( public constructor(private readonly redisCacheService: RedisCacheService) {}
private readonly redisCacheService: RedisCacheService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.accessAdminControl)
@Post('flush') @Post('flush')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async flushCache(): Promise<void> { public async flushCache(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.redisCacheService.reset(); return this.redisCacheService.reset();
} }
} }

3
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 { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
Controller, Controller,
@ -19,7 +20,7 @@ export class ExchangeRateController {
) {} ) {}
@Get(':symbol/:dateString') @Get(':symbol/:dateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getExchangeRate( public async getExchangeRate(
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string

3
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 { Export } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
@ -14,7 +15,7 @@ export class ExportController {
) {} ) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async export( public async export(
@Query('activityIds') activityIds?: string[] @Query('activityIds') activityIds?: string[]
): Promise<Export> { ): Promise<Export> {

13
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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
@ -34,7 +36,8 @@ export class ImportController {
) {} ) {}
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@HasPermission(permissions.createOrder)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async import( public async import(
@ -42,11 +45,7 @@ export class ImportController {
@Query('dryRun') isDryRun?: boolean @Query('dryRun') isDryRun?: boolean
): Promise<ImportResponse> { ): Promise<ImportResponse> {
if ( if (
!hasPermission( !hasPermission(this.request.user.permissions, permissions.createAccount)
this.request.user.permissions,
permissions.createAccount
) ||
!hasPermission(this.request.user.permissions, permissions.createOrder)
) { ) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
@ -92,7 +91,7 @@ export class ImportController {
} }
@Get('dividends/:dataSource/:symbol') @Get('dividends/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async gatherDividends( public async gatherDividends(

39
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 { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.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'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
@ -44,24 +46,16 @@ export class OrderController {
) {} ) {}
@Delete() @Delete()
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deleteOrder)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteOrders(): Promise<number> { public async deleteOrders(): Promise<number> {
if (
!hasPermission(this.request.user.permissions, permissions.deleteOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.orderService.deleteOrders({ return this.orderService.deleteOrders({
userId: this.request.user.id userId: this.request.user.id
}); });
} }
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> { public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
const order = await this.orderService.order({ id }); const order = await this.orderService.order({ id });
@ -82,7 +76,7 @@ export class OrderController {
} }
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders( public async getAllOrders(
@ -120,19 +114,11 @@ export class OrderController {
return { activities, count }; return { activities, count };
} }
@HasPermission(permissions.createOrder)
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> { public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> {
if (
!hasPermission(this.request.user.permissions, permissions.createOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const order = await this.orderService.createOrder({ const order = await this.orderService.createOrder({
...data, ...data,
date: parseISO(data.date), date: parseISO(data.date),
@ -170,19 +156,16 @@ export class OrderController {
return order; return order;
} }
@HasPermission(permissions.updateOrder)
@Put(':id') @Put(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) {
const originalOrder = await this.orderService.order({ const originalOrder = await this.orderService.order({
id id
}); });
if ( if (!originalOrder || originalOrder.userId !== this.request.user.id) {
!hasPermission(this.request.user.permissions, permissions.updateOrder) ||
!originalOrder ||
originalOrder.userId !== this.request.user.id
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN

50
apps/api/src/app/platform/platform.controller.ts

@ -1,18 +1,17 @@
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import type { RequestWithUser } from '@ghostfolio/common/types'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import { import {
Body, Body,
Controller, Controller,
Delete, Delete,
Get, Get,
HttpException, HttpException,
Inject,
Param, Param,
Post, Post,
Put, Put,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Platform } from '@prisma/client'; import { Platform } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -23,49 +22,30 @@ import { UpdatePlatformDto } from './update-platform.dto';
@Controller('platform') @Controller('platform')
export class PlatformController { export class PlatformController {
public constructor( public constructor(private readonly platformService: PlatformService) {}
private readonly platformService: PlatformService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPlatforms() { public async getPlatforms() {
return this.platformService.getPlatformsWithAccountCount(); return this.platformService.getPlatformsWithAccountCount();
} }
@HasPermission(permissions.createPlatform)
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createPlatform( public async createPlatform(
@Body() data: CreatePlatformDto @Body() data: CreatePlatformDto
): Promise<Platform> { ): Promise<Platform> {
if (
!hasPermission(this.request.user.permissions, permissions.createPlatform)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.platformService.createPlatform(data); return this.platformService.createPlatform(data);
} }
@HasPermission(permissions.updatePlatform)
@Put(':id') @Put(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updatePlatform( public async updatePlatform(
@Param('id') id: string, @Param('id') id: string,
@Body() data: UpdatePlatformDto @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({ const originalPlatform = await this.platformService.getPlatform({
id id
}); });
@ -88,17 +68,9 @@ export class PlatformController {
} }
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deletePlatform)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deletePlatform(@Param('id') id: string) { 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({ const originalPlatform = await this.platformService.getPlatform({
id id
}); });

1
apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -92,6 +92,7 @@ describe('PortfolioCalculator', () => {
marketPrice: 148.9, marketPrice: 148.9,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'BALN.SW', symbol: 'BALN.SW',
timeWeightedInvestment: new Big('285.8'),
transactionCount: 2 transactionCount: 2
} }
], ],

1
apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts

@ -81,6 +81,7 @@ describe('PortfolioCalculator', () => {
marketPrice: 148.9, marketPrice: 148.9,
quantity: new Big('2'), quantity: new Big('2'),
symbol: 'BALN.SW', symbol: 'BALN.SW',
timeWeightedInvestment: new Big('273.2'),
transactionCount: 1 transactionCount: 1
} }
], ],

9
apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -73,10 +73,10 @@ describe('PortfolioCalculator', () => {
currentValue: new Big('13657.2'), currentValue: new Big('13657.2'),
errors: [], errors: [],
grossPerformance: new Big('27172.74'), grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.40043067128546016291'), grossPerformancePercentage: new Big('42.41978276196153750666'),
hasErrors: false, hasErrors: false,
netPerformance: new Big('27172.74'), netPerformance: new Big('27172.74'),
netPerformancePercentage: new Big('42.40043067128546016291'), netPerformancePercentage: new Big('42.41978276196153750666'),
positions: [ positions: [
{ {
averagePrice: new Big('320.43'), averagePrice: new Big('320.43'),
@ -85,13 +85,14 @@ describe('PortfolioCalculator', () => {
fee: new Big('0'), fee: new Big('0'),
firstBuyDate: '2015-01-01', firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74'), grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.40043067128546016291'), grossPerformancePercentage: new Big('42.41978276196153750666'),
investment: new Big('320.43'), investment: new Big('320.43'),
netPerformance: new Big('27172.74'), netPerformance: new Big('27172.74'),
netPerformancePercentage: new Big('42.40043067128546016291'), netPerformancePercentage: new Big('42.41978276196153750666'),
marketPrice: 13657.2, marketPrice: 13657.2,
quantity: new Big('1'), quantity: new Big('1'),
symbol: 'BTCUSD', symbol: 'BTCUSD',
timeWeightedInvestment: new Big('640.56763686131386861314'),
transactionCount: 2 transactionCount: 2
} }
], ],

9
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -73,10 +73,10 @@ describe('PortfolioCalculator', () => {
currentValue: new Big('87.8'), currentValue: new Big('87.8'),
errors: [], errors: [],
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.14465699208443271768'), grossPerformancePercentage: new Big('0.15113417083448194384'),
hasErrors: false, hasErrors: false,
netPerformance: new Big('17.68'), netPerformance: new Big('17.68'),
netPerformancePercentage: new Big('0.11662269129287598945'), netPerformancePercentage: new Big('0.12184460284330327256'),
positions: [ positions: [
{ {
averagePrice: new Big('75.80'), averagePrice: new Big('75.80'),
@ -85,13 +85,14 @@ describe('PortfolioCalculator', () => {
fee: new Big('4.25'), fee: new Big('4.25'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.14465699208443271768'), grossPerformancePercentage: new Big('0.15113417083448194384'),
investment: new Big('75.80'), investment: new Big('75.80'),
netPerformance: new Big('17.68'), netPerformance: new Big('17.68'),
netPerformancePercentage: new Big('0.11662269129287598945'), netPerformancePercentage: new Big('0.12184460284330327256'),
marketPrice: 87.8, marketPrice: 87.8,
quantity: new Big('1'), quantity: new Big('1'),
symbol: 'NOVN.SW', symbol: 'NOVN.SW',
timeWeightedInvestment: new Big('145.10285714285714285714'),
transactionCount: 2 transactionCount: 2
} }
], ],

1
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -114,6 +114,7 @@ describe('PortfolioCalculator', () => {
marketPrice: 87.8, marketPrice: 87.8,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'NOVN.SW', symbol: 'NOVN.SW',
timeWeightedInvestment: new Big('151.6'),
transactionCount: 2 transactionCount: 2
} }
], ],

47
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -16,6 +16,7 @@ import {
addMilliseconds, addMilliseconds,
addMonths, addMonths,
addYears, addYears,
differenceInDays,
endOfDay, endOfDay,
format, format,
isAfter, isAfter,
@ -44,7 +45,7 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator { export class PortfolioCalculator {
private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT = private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT =
true; true;
private static readonly ENABLE_LOGGING = false; private static readonly ENABLE_LOGGING = false;
@ -621,7 +622,6 @@ export class PortfolioCalculator {
if (firstIndex > 0) { if (firstIndex > 0) {
firstIndex--; firstIndex--;
} }
const initialValues: { [symbol: string]: Big } = {};
const positions: TimelinePosition[] = []; const positions: TimelinePosition[] = [];
let hasAnySymbolMetricsErrors = false; let hasAnySymbolMetricsErrors = false;
@ -635,9 +635,9 @@ export class PortfolioCalculator {
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
hasErrors, hasErrors,
initialValue,
netPerformance, netPerformance,
netPerformancePercentage netPerformancePercentage,
timeWeightedInvestment
} = this.getSymbolMetrics({ } = this.getSymbolMetrics({
end, end,
marketSymbolMap, marketSymbolMap,
@ -646,9 +646,9 @@ export class PortfolioCalculator {
}); });
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
initialValues[item.symbol] = initialValue;
positions.push({ positions.push({
timeWeightedInvestment,
averagePrice: item.quantity.eq(0) averagePrice: item.quantity.eq(0)
? new Big(0) ? new Big(0)
: item.investment.div(item.quantity), : item.investment.div(item.quantity),
@ -683,7 +683,7 @@ export class PortfolioCalculator {
} }
} }
const overall = this.calculateOverallPerformance(positions, initialValues); const overall = this.calculateOverallPerformance(positions);
return { return {
...overall, ...overall,
@ -906,18 +906,13 @@ export class PortfolioCalculator {
}; };
} }
private calculateOverallPerformance( private calculateOverallPerformance(positions: TimelinePosition[]) {
positions: TimelinePosition[],
initialValues: { [symbol: string]: Big }
) {
let currentValue = new Big(0); let currentValue = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
let grossPerformancePercentage = new Big(0);
let hasErrors = false; let hasErrors = false;
let netPerformance = new Big(0); let netPerformance = new Big(0);
let netPerformancePercentage = new Big(0);
let sumOfWeights = new Big(0);
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let totalTimeWeightedInvestment = new Big(0);
for (const currentPosition of positions) { for (const currentPosition of positions) {
if (currentPosition.marketPrice) { if (currentPosition.marketPrice) {
@ -965,22 +960,18 @@ export class PortfolioCalculator {
} }
} }
if (sumOfWeights.gt(0)) {
grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights);
netPerformancePercentage = netPerformancePercentage.div(sumOfWeights);
} else {
grossPerformancePercentage = new Big(0);
netPerformancePercentage = new Big(0);
}
return { return {
currentValue, currentValue,
grossPerformance, grossPerformance,
grossPerformancePercentage,
hasErrors, hasErrors,
netPerformance, netPerformance,
netPerformancePercentage, totalInvestment,
totalInvestment netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
? new Big(0)
: netPerformance.div(totalTimeWeightedInvestment),
grossPerformancePercentage: totalTimeWeightedInvestment.eq(0)
? new Big(0)
: grossPerformance.div(totalTimeWeightedInvestment)
}; };
} }
@ -1194,6 +1185,7 @@ export class PortfolioCalculator {
let averagePriceAtEndDate = new Big(0); let averagePriceAtEndDate = new Big(0);
let averagePriceAtStartDate = new Big(0); let averagePriceAtStartDate = new Big(0);
const currentValues: { [date: string]: Big } = {};
let feesAtStartDate = new Big(0); let feesAtStartDate = new Big(0);
let fees = new Big(0); let fees = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
@ -1201,7 +1193,6 @@ export class PortfolioCalculator {
let grossPerformanceFromSells = new Big(0); let grossPerformanceFromSells = new Big(0);
let initialValue: Big; let initialValue: Big;
let investmentAtStartDate: Big; let investmentAtStartDate: Big;
const currentValues: { [date: string]: Big } = {};
const investmentValues: { [date: string]: Big } = {}; const investmentValues: { [date: string]: Big } = {};
const maxInvestmentValues: { [date: string]: Big } = {}; const maxInvestmentValues: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
@ -1249,6 +1240,9 @@ export class PortfolioCalculator {
return order.itemType === 'end'; return order.itemType === 'end';
}); });
let totalInvestmentDays = 0;
let sumOfTimeWeightedInvestments = new Big(0);
return this.calculatePerformanceOfSymbol( return this.calculatePerformanceOfSymbol(
orders, orders,
indexOfStartOrder, indexOfStartOrder,
@ -1980,6 +1974,9 @@ export class PortfolioCalculator {
2 2
)} -> ${averagePriceAtEndDate.toFixed(2)} )} -> ${averagePriceAtEndDate.toFixed(2)}
Total investment: ${totalInvestment.toFixed(2)} Total investment: ${totalInvestment.toFixed(2)}
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed(
2
)}
Max. total investment: ${maxTotalInvestment.toFixed(2)} Max. total investment: ${maxTotalInvestment.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed( Gross performance: ${totalGrossPerformance.toFixed(
2 2

15
apps/api/src/app/portfolio/portfolio.controller.ts

@ -1,5 +1,6 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { import {
hasNotDefinedValuesInObject, hasNotDefinedValuesInObject,
nullifyValuesInObject nullifyValuesInObject
@ -61,7 +62,7 @@ export class PortfolioController {
) {} ) {}
@Get('details') @Get('details')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails( public async getDetails(
@ -239,7 +240,7 @@ export class PortfolioController {
} }
@Get('dividends') @Get('dividends')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getDividends( public async getDividends(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@ -289,7 +290,7 @@ export class PortfolioController {
} }
@Get('investments') @Get('investments')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getInvestments( public async getInvestments(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@ -350,7 +351,7 @@ export class PortfolioController {
} }
@Get('performance') @Get('performance')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2') @Version('2')
public async getPerformanceV2( public async getPerformanceV2(
@ -442,7 +443,7 @@ export class PortfolioController {
} }
@Get('positions') @Get('positions')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions( public async getPositions(
@ -537,7 +538,7 @@ export class PortfolioController {
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPosition( public async getPosition(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource, @Param('dataSource') dataSource,
@ -560,7 +561,7 @@ export class PortfolioController {
} }
@Get('report') @Get('report')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getReport( public async getReport(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
): Promise<PortfolioReport> { ): Promise<PortfolioReport> {

5
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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -37,7 +38,7 @@ export class SubscriptionController {
@Post('redeem-coupon') @Post('redeem-coupon')
@HttpCode(StatusCodes.OK) @HttpCode(StatusCodes.OK)
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) { public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) {
if (!this.request.user) { if (!this.request.user) {
throw new HttpException( throw new HttpException(
@ -109,7 +110,7 @@ export class SubscriptionController {
} }
@Post('stripe/checkout-session') @Post('stripe/checkout-session')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createCheckoutSession( public async createCheckoutSession(
@Body() { couponId, priceId }: { couponId: string; priceId: string } @Body() { couponId, priceId }: { couponId: string; priceId: string }
) { ) {

5
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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
@ -34,7 +35,7 @@ export class SymbolController {
* Must be before /:symbol * Must be before /:symbol
*/ */
@Get('lookup') @Get('lookup')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async lookupSymbol( public async lookupSymbol(
@Query('includeIndices') includeIndices: boolean = false, @Query('includeIndices') includeIndices: boolean = false,
@ -88,7 +89,7 @@ export class SymbolController {
} }
@Get(':dataSource/:symbol/:dateString') @Get(':dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherSymbolForDate( public async gatherSymbolForDate(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string, @Param('dateString') dateString: string,

44
apps/api/src/app/tag/tag.controller.ts

@ -1,18 +1,17 @@
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import type { RequestWithUser } from '@ghostfolio/common/types'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import { import {
Body, Body,
Controller, Controller,
Delete, Delete,
Get, Get,
HttpException, HttpException,
Inject,
Param, Param,
Post, Post,
Put, Put,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -23,40 +22,25 @@ import { UpdateTagDto } from './update-tag.dto';
@Controller('tag') @Controller('tag')
export class TagController { export class TagController {
public constructor( public constructor(private readonly tagService: TagService) {}
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly tagService: TagService
) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getTags() { public async getTags() {
return this.tagService.getTagsWithActivityCount(); return this.tagService.getTagsWithActivityCount();
} }
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.createTag)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createTag(@Body() data: CreateTagDto): Promise<Tag> { public async createTag(@Body() data: CreateTagDto): Promise<Tag> {
if (!hasPermission(this.request.user.permissions, permissions.createTag)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.tagService.createTag(data); return this.tagService.createTag(data);
} }
@HasPermission(permissions.updateTag)
@Put(':id') @Put(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) { 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({ const originalTag = await this.tagService.getTag({
id id
}); });
@ -79,15 +63,9 @@ export class TagController {
} }
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deleteTag)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteTag(@Param('id') id: string) { 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({ const originalTag = await this.tagService.getTag({
id id
}); });

14
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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { User, UserSettings } from '@ghostfolio/common/interfaces'; import { User, UserSettings } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -36,12 +38,10 @@ export class UserController {
) {} ) {}
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @HasPermission(permissions.deleteUser)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteUser(@Param('id') id: string): Promise<UserModel> { public async deleteUser(@Param('id') id: string): Promise<UserModel> {
if ( if (id === this.request.user.id) {
!hasPermission(this.request.user.permissions, permissions.deleteUser) ||
id === this.request.user.id
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN
@ -54,7 +54,7 @@ export class UserController {
} }
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getUser( public async getUser(
@Headers('accept-language') acceptLanguage: string @Headers('accept-language') acceptLanguage: string
): Promise<User> { ): Promise<User> {
@ -92,7 +92,7 @@ export class UserController {
} }
@Put('setting') @Put('setting')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateUserSetting(@Body() data: UpdateUserSettingDto) { public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if ( if (
size(data) === 1 && size(data) === 1 &&

9
apps/api/src/guards/has-permission.guard.spec.ts

@ -1,7 +1,6 @@
import { HttpException } from '@nestjs/common'; import { HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { Test, TestingModule } from '@nestjs/testing';
import { HasPermissionGuard } from './has-permission.guard'; import { HasPermissionGuard } from './has-permission.guard';
@ -10,12 +9,8 @@ describe('HasPermissionGuard', () => {
let reflector: Reflector; let reflector: Reflector;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ reflector = new Reflector();
providers: [HasPermissionGuard, Reflector] guard = new HasPermissionGuard(reflector);
}).compile();
guard = module.get<HasPermissionGuard>(HasPermissionGuard);
reflector = module.get<Reflector>(Reflector);
}); });
function setupReflectorSpy(returnValue: string) { function setupReflectorSpy(returnValue: string) {

6
apps/api/src/guards/has-permission.guard.ts

@ -14,17 +14,17 @@ export class HasPermissionGuard implements CanActivate {
public constructor(private reflector: Reflector) {} public constructor(private reflector: Reflector) {}
public canActivate(context: ExecutionContext): boolean { public canActivate(context: ExecutionContext): boolean {
const { user } = context.switchToHttp().getRequest();
const requiredPermission = this.reflector.get<string>( const requiredPermission = this.reflector.get<string>(
HAS_PERMISSION_KEY, HAS_PERMISSION_KEY,
context.getHandler() context.getHandler()
); );
if (!requiredPermission) { 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)) { if (!user || !hasPermission(user.permissions, requiredPermission)) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),

2
apps/client/project.json

@ -207,7 +207,7 @@
} }
}, },
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "executor": "@nx/eslint:lint",
"options": { "options": {
"lintFilePatterns": ["apps/client/**/*.ts"] "lintFilePatterns": ["apps/client/**/*.ts"]
} }

8
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -260,6 +260,14 @@ export class ImportActivitiesDialog implements OnDestroy {
} }
} }
content.activities = content.activities.map((activity) => {
if (activity.id) {
delete activity.id;
}
return activity;
});
try { try {
const { activities } = const { activities } =
await this.importActivitiesService.importJson({ await this.importActivitiesService.importJson({

2
apps/ui-e2e/project.json

@ -18,7 +18,7 @@
} }
}, },
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "executor": "@nx/eslint:lint",
"options": { "options": {
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"] "lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
} }

2
libs/common/project.json

@ -5,7 +5,7 @@
"projectType": "library", "projectType": "library",
"targets": { "targets": {
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "executor": "@nx/eslint:lint",
"options": { "options": {
"lintFilePatterns": ["libs/common/**/*.ts"] "lintFilePatterns": ["libs/common/**/*.ts"]
} }

1
libs/common/src/lib/interfaces/timeline-position.interface.ts

@ -16,5 +16,6 @@ export interface TimelinePosition {
quantity: Big; quantity: Big;
symbol: string; symbol: string;
tags?: Tag[]; tags?: Tag[];
timeWeightedInvestment: Big;
transactionCount: number; transactionCount: number;
} }

2
libs/ui/project.json

@ -18,7 +18,7 @@
} }
}, },
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "executor": "@nx/eslint:lint",
"options": { "options": {
"lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"] "lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"]
} }

8
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.31.0", "version": "2.32.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -81,7 +81,7 @@
"@nestjs/platform-express": "10.1.3", "@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2", "@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0", "@nestjs/serve-static": "4.0.0",
"@prisma/client": "5.7.0", "@prisma/client": "5.7.1",
"@simplewebauthn/browser": "8.3.1", "@simplewebauthn/browser": "8.3.1",
"@simplewebauthn/server": "8.3.2", "@simplewebauthn/server": "8.3.2",
"@stripe/stripe-js": "1.47.0", "@stripe/stripe-js": "1.47.0",
@ -123,7 +123,7 @@
"passport": "0.6.0", "passport": "0.6.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"prisma": "5.7.0", "prisma": "5.7.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "7.5.6", "rxjs": "7.5.6",
"stripe": "11.12.0", "stripe": "11.12.0",
@ -188,7 +188,7 @@
"jest-environment-jsdom": "29.4.3", "jest-environment-jsdom": "29.4.3",
"jest-preset-angular": "13.1.4", "jest-preset-angular": "13.1.4",
"nx": "17.2.5", "nx": "17.2.5",
"prettier": "3.1.0", "prettier": "3.1.1",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",

98
yarn.lock

@ -4795,46 +4795,46 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@prisma/client@5.7.0": "@prisma/client@5.7.1":
version "5.7.0" version "5.7.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.7.0.tgz#c29dd9a16e100902eb2d2443d90fee2482d2aeac" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.7.1.tgz#a124afd05663267f7255a639a81d28303684a063"
integrity sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg== integrity sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==
"@prisma/debug@5.7.0": "@prisma/debug@5.7.1":
version "5.7.0" version "5.7.1"
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.7.0.tgz#abdb2060be4fe819e73e2683cf1b039841566198" resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.7.1.tgz#064177066e630beb43492ffa608acc21a118e2ce"
integrity sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA== integrity sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==
"@prisma/engines-version@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9": "@prisma/engines-version@5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5":
version "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9" version "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz#777827898f1bfe6a76b17fbe7d9600cf543c4cc1" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz#b7845425313e5395a3a3e64f3e0d04c1f320fa92"
integrity sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg== integrity sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==
"@prisma/engines@5.7.0": "@prisma/engines@5.7.1":
version "5.7.0" version "5.7.1"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.7.0.tgz#a32e232819b66bd9dee7500b455781742dc54b2f" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.7.1.tgz#631c27daa326bbacd5d7119446e0d3f15c0f274c"
integrity sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA== integrity sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==
dependencies: dependencies:
"@prisma/debug" "5.7.0" "@prisma/debug" "5.7.1"
"@prisma/engines-version" "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9" "@prisma/engines-version" "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5"
"@prisma/fetch-engine" "5.7.0" "@prisma/fetch-engine" "5.7.1"
"@prisma/get-platform" "5.7.0" "@prisma/get-platform" "5.7.1"
"@prisma/fetch-engine@5.7.0": "@prisma/fetch-engine@5.7.1":
version "5.7.0" version "5.7.1"
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz#7d2795828b692b02707e7ab6876f6227a68fc309" resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz#d7baa3493867c6f7cedfc41df477cfd0963059ca"
integrity sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w== integrity sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==
dependencies: dependencies:
"@prisma/debug" "5.7.0" "@prisma/debug" "5.7.1"
"@prisma/engines-version" "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9" "@prisma/engines-version" "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5"
"@prisma/get-platform" "5.7.0" "@prisma/get-platform" "5.7.1"
"@prisma/get-platform@5.7.0": "@prisma/get-platform@5.7.1":
version "5.7.0" version "5.7.1"
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.7.0.tgz#eb81011f537c2d10c0225278cd5165a82d0b57c8" resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.7.1.tgz#bc2fe43838c7d47b321aa4728a0f60990d02bc9e"
integrity sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA== integrity sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==
dependencies: dependencies:
"@prisma/debug" "5.7.0" "@prisma/debug" "5.7.1"
"@radix-ui/number@1.0.1": "@radix-ui/number@1.0.1":
version "1.0.1" version "1.0.1"
@ -16863,10 +16863,10 @@ prettier-plugin-organize-attributes@1.0.0:
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63" resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63"
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg== integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
prettier@3.1.0: prettier@3.1.1:
version "3.1.0" version "3.1.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848"
integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==
prettier@^2.8.0: prettier@^2.8.0:
version "2.8.8" version "2.8.8"
@ -16900,12 +16900,12 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
prisma@5.7.0: prisma@5.7.1:
version "5.7.0" version "5.7.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.7.0.tgz#3c1c56d392b5d1137de954edefa4533fa092663e" resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.7.1.tgz#af60ed90531adc0ab8a683c9b1fc86d841c39864"
integrity sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q== integrity sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==
dependencies: dependencies:
"@prisma/engines" "5.7.0" "@prisma/engines" "5.7.1"
prismjs@^1.28.0: prismjs@^1.28.0:
version "1.29.0" version "1.29.0"

Loading…
Cancel
Save