Browse Source

Add support for access of type API

pull/7262/head
Thomas Kaul 1 week ago
parent
commit
c5e780f10f
  1. 36
      apps/api/src/app/access/access.controller.ts
  2. 6
      apps/api/src/app/access/access.module.ts
  3. 10
      apps/api/src/app/access/access.service.ts
  4. 89
      apps/api/src/app/endpoints/api/api.controller.ts
  5. 45
      apps/api/src/app/endpoints/api/api.module.ts
  6. 93
      apps/api/src/app/endpoints/public/public.controller.ts
  7. 92
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 7
      apps/api/src/app/user/user.service.ts
  9. 8
      apps/api/src/helper/hash.helper.ts
  10. 1
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  11. 6
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

36
apps/api/src/app/access/access.controller.ts

@ -30,6 +30,7 @@ import {
AccessType, AccessType,
Access as AccessModel Access as AccessModel
} from '@prisma/client'; } from '@prisma/client';
import { endOfDay, isBefore, parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { omit } from 'lodash'; import { omit } from 'lodash';
@ -106,9 +107,21 @@ export class AccessController {
); );
} }
const isApiAccess = data.type === AccessType.API; const type =
data.type ??
(data.granteeUserId ? AccessType.PRIVATE : AccessType.PUBLIC);
const isApiAccess = type === AccessType.API;
const expiresAt = data.expiresAt
? endOfDay(parseISO(data.expiresAt))
: undefined;
if (isApiAccess && data.granteeUserId) { if (
(type === AccessType.PRIVATE && !data.granteeUserId) ||
(type !== AccessType.PRIVATE && data.granteeUserId) ||
(!isApiAccess && expiresAt) ||
(expiresAt && isBefore(expiresAt, new Date()))
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST), getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST StatusCodes.BAD_REQUEST
@ -121,17 +134,15 @@ export class AccessController {
: { apiToken: undefined, hashedApiToken: undefined }; : { apiToken: undefined, hashedApiToken: undefined };
const access = await this.accessService.createAccess({ const access = await this.accessService.createAccess({
expiresAt,
hashedApiToken, hashedApiToken,
type,
alias: data.alias || undefined, alias: data.alias || undefined,
expiresAt: isApiAccess ? data.expiresAt : undefined,
granteeUser: data.granteeUserId granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: undefined, : undefined,
permissions: isApiAccess ? [AccessPermission.READ] : data.permissions, permissions: isApiAccess ? [AccessPermission.READ] : data.permissions,
settings: this.accessService.buildSettings(data.filters), settings: this.accessService.buildSettings(data.filters),
type:
data.type ??
(data.granteeUserId ? AccessType.PRIVATE : AccessType.PUBLIC),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}); });
@ -203,7 +214,10 @@ export class AccessController {
const isApiAccess = originalAccess.type === AccessType.API; const isApiAccess = originalAccess.type === AccessType.API;
if (isApiAccess && data.granteeUserId) { if (
(originalAccess.type === AccessType.PRIVATE && !data.granteeUserId) ||
(originalAccess.type !== AccessType.PRIVATE && data.granteeUserId)
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST), getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST StatusCodes.BAD_REQUEST
@ -216,11 +230,11 @@ export class AccessController {
alias: data.alias, alias: data.alias,
granteeUser: data.granteeUserId granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: isApiAccess : undefined,
? undefined
: { disconnect: true },
permissions: isApiAccess ? undefined : data.permissions, permissions: isApiAccess ? undefined : data.permissions,
settings: this.accessService.buildSettings(data.filters) settings: data.filters
? this.accessService.buildSettings(data.filters)
: undefined
}, },
where: { id } where: { id }
}); });

6
apps/api/src/app/access/access.module.ts

@ -1,4 +1,4 @@
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ApiKeyModule } from '@ghostfolio/api/services/api-key/api-key.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
@ -10,7 +10,7 @@ import { AccessService } from './access.service';
@Module({ @Module({
controllers: [AccessController], controllers: [AccessController],
exports: [AccessService], exports: [AccessService],
imports: [ConfigurationModule, PrismaModule], imports: [ApiKeyModule, ConfigurationModule, PrismaModule],
providers: [AccessService, ApiKeyService] providers: [AccessService]
}) })
export class AccessModule {} export class AccessModule {}

10
apps/api/src/app/access/access.service.ts

@ -1,3 +1,4 @@
import { createSha512HmacHash } from '@ghostfolio/api/helper/hash.helper';
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -6,7 +7,6 @@ import { AccessWithGranteeUser } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Access, Prisma } from '@prisma/client'; import { Access, Prisma } from '@prisma/client';
import { createHmac } from 'node:crypto';
@Injectable() @Injectable()
export class AccessService { export class AccessService {
@ -101,12 +101,10 @@ export class AccessService {
} }
private hashApiToken(apiToken: string) { private hashApiToken(apiToken: string) {
const hash = createHmac( // Rotating ACCESS_TOKEN_SALT invalidates all stored api tokens
'sha512', return createSha512HmacHash(
apiToken,
this.configurationService.get('ACCESS_TOKEN_SALT') this.configurationService.get('ACCESS_TOKEN_SALT')
); );
hash.update(apiToken);
return hash.digest('hex');
} }
} }

89
apps/api/src/app/endpoints/api/api.controller.ts

@ -1,4 +1,3 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
@ -18,13 +17,11 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Type as ActivityType } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
@Controller('api') @Controller('api')
export class ApiController { export class ApiController {
public constructor( public constructor(
private readonly activitiesService: ActivitiesService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: { user: UserWithApiAccess } @Inject(REQUEST) private readonly request: { user: UserWithApiAccess }
) {} ) {}
@ -37,65 +34,14 @@ export class ApiController {
const { filters } = (apiAccess.settings ?? {}) as AccessSettings; const { filters } = (apiAccess.settings ?? {}) as AccessSettings;
const [ const { createdAt, holdings, latestActivities, markets, performance } =
{ createdAt, holdings, markets }, await this.portfolioService.getPortfolioOverview({
{ performance: performance1d },
{ performance: performanceMax },
{ performance: performanceYtd }
] = await Promise.all([
this.portfolioService.getDetails({
filters,
impersonationId: apiAccess.userId,
userId: user.id,
withMarkets: true
}),
...['1d', 'max', 'ytd'].map((dateRange) => {
return this.portfolioService.getPerformance({
dateRange,
filters, filters,
impersonationId: undefined, impersonationId: undefined,
userId: user.id
});
})
]);
const { activities } = await this.activitiesService.getActivities({
filters,
sortColumn: 'date',
sortDirection: 'desc',
take: 10,
types: [ActivityType.BUY, ActivityType.SELL],
userCurrency: user.settings.settings.baseCurrency ?? DEFAULT_CURRENCY, userCurrency: user.settings.settings.baseCurrency ?? DEFAULT_CURRENCY,
userId: user.id, userId: user.id
withExcludedAccountsAndActivities: false
}); });
const latestActivities = activities.map(
({
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
}) => {
return {
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
};
}
);
const totalValueInBaseCurrency = getSum( const totalValueInBaseCurrency = getSum(
Object.values(holdings).map(({ valueInBaseCurrency }) => { Object.values(holdings).map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency ?? 0); return new Big(valueInBaseCurrency ?? 0);
@ -106,29 +52,21 @@ export class ApiController {
createdAt, createdAt,
latestActivities, latestActivities,
markets, markets,
performance,
totalValueInBaseCurrency, totalValueInBaseCurrency,
alias: apiAccess.alias, alias: apiAccess.alias,
holdings: {}, holdings: {}
performance: {
'1d': {
relativeChange:
performance1d.netPerformancePercentageWithCurrencyEffect
},
max: {
relativeChange:
performanceMax.netPerformancePercentageWithCurrencyEffect
},
ytd: {
relativeChange:
performanceYtd.netPerformancePercentageWithCurrencyEffect
}
}
}; };
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
const allocationInPercentage =
totalValueInBaseCurrency > 0
? (portfolioPosition.valueInBaseCurrency ?? 0) /
totalValueInBaseCurrency
: 0;
apiPortfolioResponse.holdings[symbol] = { apiPortfolioResponse.holdings[symbol] = {
allocationInPercentage: allocationInPercentage,
portfolioPosition.valueInBaseCurrency / totalValueInBaseCurrency,
assetProfile: portfolioPosition.assetProfile, assetProfile: portfolioPosition.assetProfile,
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
markets: portfolioPosition.markets, markets: portfolioPosition.markets,
@ -136,8 +74,7 @@ export class ApiController {
portfolioPosition.netPerformancePercentWithCurrencyEffect, portfolioPosition.netPerformancePercentWithCurrencyEffect,
quantity: portfolioPosition.quantity, quantity: portfolioPosition.quantity,
valueInBaseCurrency: portfolioPosition.valueInBaseCurrency, valueInBaseCurrency: portfolioPosition.valueInBaseCurrency,
valueInPercentage: valueInPercentage: allocationInPercentage
portfolioPosition.valueInBaseCurrency / totalValueInBaseCurrency
}; };
} }

45
apps/api/src/app/endpoints/api/api.module.ts

@ -1,22 +1,5 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
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 { 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';
import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -24,28 +7,6 @@ import { ApiController } from './api.controller';
@Module({ @Module({
controllers: [ApiController], controllers: [ApiController],
imports: [ imports: [PortfolioModule, TransformDataSourceInResponseModule]
ActivitiesModule,
BenchmarkModule,
DataProviderModule,
ExchangeRateDataModule,
I18nModule,
ImpersonationModule,
MarketDataModule,
PortfolioSnapshotQueueModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule,
TransformDataSourceInRequestModule,
UserModule
],
providers: [
AccountBalanceService,
AccountService,
CurrentRateService,
PortfolioCalculatorFactory,
PortfolioService,
RulesService
]
}) })
export class ApiModule {} export class ApiModule {}

93
apps/api/src/app/endpoints/public/public.controller.ts

@ -1,5 +1,4 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service'; 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 { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
@ -21,12 +20,7 @@ import {
Param, Param,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { import { AccessType, AssetClass, AssetSubClass } from '@prisma/client';
AccessType,
AssetClass,
AssetSubClass,
Type as ActivityType
} from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -34,7 +28,6 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
export class PublicController { export class PublicController {
public constructor( public constructor(
private readonly accessService: AccessService, private readonly accessService: AccessService,
private readonly activitiesService: ActivitiesService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@ -72,70 +65,14 @@ export class PublicController {
const { filters } = (access.settings ?? {}) as AccessSettings; const { filters } = (access.settings ?? {}) as AccessSettings;
const [ const { createdAt, holdings, latestActivities, markets, performance } =
{ createdAt, holdings, markets }, await this.portfolioService.getPortfolioOverview({
{ performance: performance1d },
{ performance: performanceMax },
{ performance: performanceYtd }
] = await Promise.all([
this.portfolioService.getDetails({
filters, filters,
impersonationId: access.userId, impersonationId: access.userId,
userId: user.id,
withMarkets: true
}),
...['1d', 'max', 'ytd'].map((dateRange) => {
return this.portfolioService.getPerformance({
dateRange,
filters,
impersonationId: undefined,
userId: user.id
});
})
]);
const { activities } = await this.activitiesService.getActivities({
filters,
sortColumn: 'date',
sortDirection: 'desc',
take: 10,
types: [ActivityType.BUY, ActivityType.SELL],
userCurrency: user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY, userCurrency: user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY,
userId: user.id, userId: user.id
withExcludedAccountsAndActivities: false
}); });
// Experimental
const latestActivities = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
? []
: activities.map(
({
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
}) => {
return {
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
};
}
);
Object.values(markets ?? {}).forEach((market) => { Object.values(markets ?? {}).forEach((market) => {
delete market.valueInBaseCurrency; delete market.valueInBaseCurrency;
}); });
@ -143,24 +80,16 @@ export class PublicController {
const publicPortfolioResponse: PublicPortfolioResponse = { const publicPortfolioResponse: PublicPortfolioResponse = {
createdAt, createdAt,
hasDetails, hasDetails,
latestActivities,
markets, markets,
performance,
alias: access.alias, alias: access.alias,
holdings: {}, holdings: {},
performance: { // Experimental
'1d': { latestActivities: this.configurationService.get(
relativeChange: 'ENABLE_FEATURE_SUBSCRIPTION'
performance1d.netPerformancePercentageWithCurrencyEffect )
}, ? []
max: { : latestActivities
relativeChange:
performanceMax.netPerformancePercentageWithCurrencyEffect
},
ytd: {
relativeChange:
performanceYtd.netPerformancePercentageWithCurrencyEffect
}
}
}; };
const totalValue = getSum( const totalValue = getSum(

92
apps/api/src/app/portfolio/portfolio.service.ts

@ -1100,6 +1100,98 @@ export class PortfolioService {
}; };
} }
public async getPortfolioOverview({
filters,
impersonationId,
userCurrency,
userId
}: {
filters?: Filter[];
impersonationId: string;
userCurrency: string;
userId: string;
}) {
const [
{ activities },
{ createdAt, holdings, markets },
{ performance: performance1d },
{ performance: performanceMax },
{ performance: performanceYtd }
] = await Promise.all([
this.activitiesService.getActivities({
filters,
userCurrency,
userId,
sortColumn: 'date',
sortDirection: 'desc',
take: 10,
types: [ActivityType.BUY, ActivityType.SELL],
withExcludedAccountsAndActivities: false
}),
this.getDetails({
filters,
impersonationId,
userId,
withMarkets: true
}),
...(['1d', 'max', 'ytd'] as DateRange[]).map((dateRange) => {
return this.getPerformance({
dateRange,
filters,
userId,
impersonationId: undefined
});
})
]);
const latestActivities = activities.map(
({
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
}) => {
return {
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
};
}
);
return {
createdAt,
holdings,
latestActivities,
markets,
performance: {
'1d': {
relativeChange:
performance1d.netPerformancePercentageWithCurrencyEffect
},
max: {
relativeChange:
performanceMax.netPerformancePercentageWithCurrencyEffect
},
ytd: {
relativeChange:
performanceYtd.netPerformancePercentageWithCurrencyEffect
}
}
};
}
public async getReport({ public async getReport({
impersonationId, impersonationId,
userId userId

7
apps/api/src/app/user/user.service.ts

@ -2,6 +2,7 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { createSha512HmacHash } from '@ghostfolio/api/helper/hash.helper';
import { getRandomString } from '@ghostfolio/api/helper/string.helper'; import { getRandomString } from '@ghostfolio/api/helper/string.helper';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
@ -54,7 +55,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, Role, Settings, User } from '@prisma/client'; import { Prisma, Role, Settings, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns'; import { differenceInDays, subDays } from 'date-fns';
import { without } from 'lodash'; import { without } from 'lodash';
import { createHmac } from 'node:crypto';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -80,10 +80,7 @@ export class UserService {
password: string; password: string;
salt: string; salt: string;
}): string { }): string {
const hash = createHmac('sha512', salt); return createSha512HmacHash(password, salt);
hash.update(password);
return hash.digest('hex');
} }
public generateAccessToken({ userId }: { userId: string }) { public generateAccessToken({ userId }: { userId: string }) {

8
apps/api/src/helper/hash.helper.ts

@ -0,0 +1,8 @@
import { createHmac } from 'node:crypto';
export function createSha512HmacHash(value: string, salt: string) {
const hash = createHmac('sha512', salt);
hash.update(value);
return hash.digest('hex');
}

1
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -88,6 +88,7 @@ export class TransformDataSourceInResponseInterceptor<
'holdings[*].assetProfile.dataSource', 'holdings[*].assetProfile.dataSource',
'holdings[*].dataSource', 'holdings[*].dataSource',
'items[*].dataSource', 'items[*].dataSource',
'latestActivities[*].SymbolProfile.dataSource',
'SymbolProfile.dataSource', 'SymbolProfile.dataSource',
'watchlist[*].dataSource' 'watchlist[*].dataSource'
] ]

6
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -76,6 +76,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
protected readonly minExpiresAtDate = new Date(); protected readonly minExpiresAtDate = new Date();
protected readonly mode: 'create' | 'update'; protected readonly mode: 'create' | 'update';
private hasLoadedFilters = false;
private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly data = private readonly data =
@ -250,6 +252,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.updateFiltersFormControl(this.data.access?.settings?.filters); this.updateFiltersFormControl(this.data.access?.settings?.filters);
this.hasLoadedFilters = true;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
@ -265,7 +269,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
const access: UpdateAccessDto = { const access: UpdateAccessDto = {
alias: this.accessForm.get('alias')?.value, alias: this.accessForm.get('alias')?.value,
filters: filters.length > 0 ? filters : undefined, filters: this.hasLoadedFilters ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value, granteeUserId: this.accessForm.get('granteeUserId')?.value,
id: accessId, id: accessId,
permissions: [this.accessForm.get('permissions')?.value] permissions: [this.accessForm.get('permissions')?.value]

Loading…
Cancel
Save