Browse Source

Task/deprecate access endpoints in favor of accesses endpoints (#8316)

* Deprecate access endpoints in favor of accesses endpoints

* Update changelog
pull/8313/merge
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
db930bbab9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 35
      apps/api/src/app/__snapshots__/app.module.spec.ts.snap
  3. 4
      apps/api/src/app/app.module.ts
  4. 4
      apps/api/src/app/endpoints/accesses/access.controller.ts
  5. 250
      apps/api/src/app/endpoints/accesses/accesses.controller.ts
  6. 13
      apps/api/src/app/endpoints/accesses/accesses.module.ts
  7. 2
      apps/api/src/app/endpoints/public/public.module.ts
  8. 2
      apps/api/src/app/endpoints/public/public.service.ts
  9. 2
      apps/api/src/app/portfolio/portfolio.module.ts
  10. 5
      apps/api/src/services/access/access.module.ts
  11. 0
      apps/api/src/services/access/access.service.ts
  12. 2
      apps/api/src/services/impersonation/impersonation.module.ts
  13. 2
      apps/api/src/services/impersonation/impersonation.service.spec.ts
  14. 2
      apps/api/src/services/impersonation/impersonation.service.ts
  15. 2
      apps/client/src/app/components/user-account-access/access-dialog-host/access-dialog-host.component.ts
  16. 2
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  17. 4
      libs/common/src/lib/interfaces/index.ts
  18. 3
      libs/common/src/lib/interfaces/responses/access-response.interface.ts
  19. 5
      libs/common/src/lib/interfaces/responses/accesses-response.interface.ts
  20. 14
      libs/ui/src/lib/services/data.service.ts

6
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
- Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints
## 3.93.0 - 2026-10-25
### Changed

35
apps/api/src/app/__snapshots__/app.module.spec.ts.snap

@ -11,6 +11,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "deleteAccess",
},
"DELETE /accesses/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "deleteAccess",
},
"DELETE /account-balance/:id": {
"guards": [
"AuthGuard('jwt')",
@ -203,6 +212,14 @@ exports[`AppModule should keep the security of each route 1`] = `
],
"interceptors": [],
},
"GET /accesses": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
},
"GET /account": {
"guards": [
"AuthGuard('jwt')",
@ -802,6 +819,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "createAccess",
},
"POST /accesses": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "createAccess",
},
"POST /account": {
"guards": [
"AuthGuard('jwt')",
@ -1133,6 +1159,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "updateAccess",
},
"PUT /accesses/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "updateAccess",
},
"PUT /account/:id": {
"guards": [
"AuthGuard('jwt')",

4
apps/api/src/app/app.module.ts

@ -35,11 +35,11 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { join } from 'node:path';
import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module';
import { ActivitiesModule } from './activities/activities.module';
import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller';
import { AccessesModule } from './endpoints/accesses/accesses.module';
import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module';
@ -71,7 +71,7 @@ import { UserModule } from './user/user.module';
controllers: [AppController],
imports: [
AdminModule,
AccessModule,
AccessesModule,
AccountModule,
ActivitiesModule,
AiModule,

4
apps/api/src/app/access/access.controller.ts → apps/api/src/app/endpoints/accesses/access.controller.ts

@ -1,6 +1,7 @@
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AccessService } from '@ghostfolio/api/services/access/access.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
@ -30,8 +31,7 @@ import { AuthGuard } from '@nestjs/passport';
import { Access as AccessModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccessService } from './access.service';
/** @deprecated */
@AllowDuringImpersonation()
@Controller('access')
export class AccessController {

250
apps/api/src/app/endpoints/accesses/accesses.controller.ts

@ -0,0 +1,250 @@
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AccessService } from '@ghostfolio/api/services/access/access.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
canApplyFiltersToAccess,
isValidGranteeOfAccess
} from '@ghostfolio/common/helper';
import {
AccessResponse,
AccessSettings,
AccessesResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { getScopesOfAccess } from '@ghostfolio/common/scopes';
import type { RequestWithUser } from '@ghostfolio/common/types';
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 { Access as AccessModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@AllowDuringImpersonation()
@Controller('accesses')
export class AccessesController {
public constructor(
private readonly accessService: AccessService,
private readonly configurationService: ConfigurationService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.createAccess)
@Post()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createAccess(
@Body() data: CreateAccessDto
): Promise<AccessResponse> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription?.type === SubscriptionType.Basic
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const type = data.type ?? (data.granteeUserId ? 'PRIVATE' : 'PUBLIC');
if (
type === 'MCP' &&
!this.configurationService.get('ENABLE_FEATURE_MCP')
) {
// The client hides the type while the feature is disabled, hence an
// access of this type must not become a credential which is dormant
// until the feature is enabled
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
if (!isValidGranteeOfAccess({ granteeUserId: data.granteeUserId, type })) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
if (data.filters?.length && !canApplyFiltersToAccess({ type })) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
try {
const access = await this.accessService.createAccess({
type,
alias: data.alias || undefined,
expiresAt: new Date(data.expiresAt),
granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: undefined,
scopes: getScopesOfAccess({
type,
scopes: data.scopes
}),
settings: this.accessService.buildSettings(data.filters),
user: { connect: { id: this.request.user.id } }
});
return this.getAccessResponse(access);
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
@Delete(':id')
@HasPermission(permissions.deleteAccess)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccess(@Param('id') id: string): Promise<void> {
const originalAccess = await this.accessService.access({
id,
OR: [
{ granteeUserId: this.request.user.id },
{ userId: this.request.user.id }
]
});
if (!originalAccess) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.accessService.deleteAccess({
id
});
}
@Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getAllAccesses(): Promise<AccessesResponse> {
const accesses = await this.accessService.accesses({
orderBy: [{ granteeUserId: 'desc' }, { createdAt: 'asc' }],
where: { userId: this.request.user.id }
});
return {
accesses: accesses.map((access) => {
return this.getAccessResponse(access);
})
};
}
@HasPermission(permissions.updateAccess)
@Put(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateAccess(
@Body() data: UpdateAccessDto,
@Param('id') id: string
): Promise<AccessResponse> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription?.type === SubscriptionType.Basic
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const originalAccess = await this.accessService.access({
id,
userId: this.request.user.id
});
if (!originalAccess) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
if (
!isValidGranteeOfAccess({
granteeUserId: data.granteeUserId,
type: originalAccess.type
})
) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
if (
data.filters?.length &&
!canApplyFiltersToAccess({ type: originalAccess.type })
) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
try {
const access = await this.accessService.updateAccess({
data: {
alias: data.alias,
expiresAt: new Date(data.expiresAt),
granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: { disconnect: true },
scopes: getScopesOfAccess({
scopes: data.scopes ?? originalAccess.scopes,
type: originalAccess.type
}),
settings: this.accessService.buildSettings(data.filters)
},
where: { id }
});
return this.getAccessResponse(access);
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
private getAccessResponse(access: AccessModel): AccessResponse {
const { alias, expiresAt, granteeUserId, id, lastUsedAt, settings, type } =
access;
const { filters } = (settings ?? {}) as AccessSettings;
return {
alias,
expiresAt,
id,
lastUsedAt,
type,
grantee: granteeUserId ?? undefined,
scopes: getScopesOfAccess(access),
settings: canApplyFiltersToAccess({ type }) ? { filters } : {}
};
}
}

13
apps/api/src/app/endpoints/accesses/accesses.module.ts

@ -0,0 +1,13 @@
import { AccessModule } from '@ghostfolio/api/services/access/access.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { Module } from '@nestjs/common';
import { AccessController } from './access.controller';
import { AccessesController } from './accesses.controller';
@Module({
controllers: [AccessController, AccessesController],
imports: [AccessModule, ConfigurationModule]
})
export class AccessesModule {}

2
apps/api/src/app/endpoints/public/public.module.ts

@ -1,4 +1,3 @@
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
@ -8,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { AccessModule } from '@ghostfolio/api/services/access/access.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';

2
apps/api/src/app/endpoints/public/public.service.ts

@ -1,7 +1,7 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { AccessService } from '@ghostfolio/api/services/access/access.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';

2
apps/api/src/app/portfolio/portfolio.module.ts

@ -1,4 +1,3 @@
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
@ -7,6 +6,7 @@ import { PerformanceLoggingModule } from '@ghostfolio/api/interceptors/performan
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { AccessModule } from '@ghostfolio/api/services/access/access.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';

5
apps/api/src/app/access/access.module.ts → apps/api/src/services/access/access.module.ts

@ -1,15 +1,12 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AccessController } from './access.controller';
import { AccessService } from './access.service';
@Module({
controllers: [AccessController],
exports: [AccessService],
imports: [ConfigurationModule, PrismaModule],
imports: [PrismaModule],
providers: [AccessService]
})
export class AccessModule {}

0
apps/api/src/app/access/access.service.ts → apps/api/src/services/access/access.service.ts

2
apps/api/src/services/impersonation/impersonation.module.ts

@ -1,4 +1,4 @@
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccessModule } from '@ghostfolio/api/services/access/access.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';

2
apps/api/src/services/impersonation/impersonation.service.spec.ts

@ -1,4 +1,4 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { AccessService } from '@ghostfolio/api/services/access/access.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SubscriptionService } from '@ghostfolio/api/services/subscription/subscription.service';

2
apps/api/src/services/impersonation/impersonation.service.ts

@ -1,4 +1,4 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { AccessService } from '@ghostfolio/api/services/access/access.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SubscriptionService } from '@ghostfolio/api/services/subscription/subscription.service';

2
apps/client/src/app/components/user-account-access/access-dialog-host/access-dialog-host.component.ts

@ -135,7 +135,7 @@ export class GfAccessDialogHostComponent implements OnDestroy, OnInit {
private fetchAccess(aAccessId: string) {
return this.dataService.fetchAccesses().pipe(
map((accesses) => {
map(({ accesses }) => {
return accesses.find(({ id }) => {
return id === aAccessId;
});

2
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -216,7 +216,7 @@ export class GfUserAccountAccessComponent {
this.dataService
.fetchAccesses()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((accesses) => {
.subscribe(({ accesses }) => {
this.accessesGive = accesses;
this.changeDetectorRef.markForCheck();

4
libs/common/src/lib/interfaces/index.ts

@ -38,7 +38,9 @@ import type { PortfolioReportRule } from './portfolio-report-rule.interface';
import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Product } from './product.interface';
import type { ReferralPartner } from './referral-partner.interface';
import type { AccessResponse } from './responses/access-response.interface';
import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccessesResponse } from './responses/accesses-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
import type { AccountResponse } from './responses/account-response.interface';
import type { AccountsResponse } from './responses/accounts-response.interface';
@ -107,8 +109,10 @@ import type { XRayRulesSettings } from './x-ray-rules-settings.interface';
export {
Access,
AccessResponse,
AccessSettings,
AccessTokenResponse,
AccessesResponse,
AccountBalance,
AccountBalancesResponse,
AccountResponse,

3
libs/common/src/lib/interfaces/responses/access-response.interface.ts

@ -0,0 +1,3 @@
import type { Access } from '../access.interface';
export interface AccessResponse extends Access {}

5
libs/common/src/lib/interfaces/responses/accesses-response.interface.ts

@ -0,0 +1,5 @@
import type { AccessResponse } from './access-response.interface';
export interface AccessesResponse {
accesses: AccessResponse[];
}

14
libs/ui/src/lib/services/data.service.ts

@ -21,8 +21,9 @@ import {
} from '@ghostfolio/common/dtos';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
Access,
AccessResponse,
AccessTokenResponse,
AccessesResponse,
AccountBalancesResponse,
AccountResponse,
AccountsResponse,
@ -310,7 +311,7 @@ export class DataService {
}
public deleteAccess(aId: string) {
return this.http.delete<void>(`/api/v1/access/${aId}`);
return this.http.delete<void>(`/api/v1/accesses/${aId}`);
}
public deleteAccount(aId: string) {
@ -376,7 +377,7 @@ export class DataService {
}
public fetchAccesses() {
return this.http.get<Access[]>('/api/v1/access');
return this.http.get<AccessesResponse>('/api/v1/accesses');
}
public fetchAsset({
@ -862,7 +863,7 @@ export class DataService {
}
public postAccess(aAccess: CreateAccessDto) {
return this.http.post<Access>('/api/v1/access', aAccess);
return this.http.post<AccessResponse>('/api/v1/accesses', aAccess);
}
public postAccount(aAccount: CreateAccountDto) {
@ -926,7 +927,10 @@ export class DataService {
}
public putAccess(aAccess: UpdateAccessDto) {
return this.http.put<Access>(`/api/v1/access/${aAccess.id}`, aAccess);
return this.http.put<AccessResponse>(
`/api/v1/accesses/${aAccess.id}`,
aAccess
);
}
public putAccount(aAccount: UpdateAccountDto) {

Loading…
Cancel
Save