mirror of https://github.com/ghostfolio/ghostfolio
152 changed files with 4436 additions and 2460 deletions
@ -0,0 +1,13 @@ |
|||||
|
# Security Policy |
||||
|
|
||||
|
## Reporting Security Issues |
||||
|
|
||||
|
If you discover a security vulnerability in this repository, please report it to security[at]ghostfol.io. We will acknowledge your report and provide guidance on the next steps. |
||||
|
|
||||
|
To help us resolve the issue, please include the following details: |
||||
|
|
||||
|
- A description of the vulnerability |
||||
|
- Steps to reproduce the vulnerability |
||||
|
- Affected versions of the software |
||||
|
|
||||
|
We appreciate your responsible disclosure and will work to address the issue promptly. |
@ -1,36 +0,0 @@ |
|||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|
||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.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 { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.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 { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|
||||
|
|
||||
import { Module } from '@nestjs/common'; |
|
||||
|
|
||||
import { BenchmarkController } from './benchmark.controller'; |
|
||||
import { BenchmarkService } from './benchmark.service'; |
|
||||
|
|
||||
@Module({ |
|
||||
controllers: [BenchmarkController], |
|
||||
exports: [BenchmarkService], |
|
||||
imports: [ |
|
||||
ConfigurationModule, |
|
||||
DataProviderModule, |
|
||||
ExchangeRateDataModule, |
|
||||
MarketDataModule, |
|
||||
PrismaModule, |
|
||||
PropertyModule, |
|
||||
RedisCacheModule, |
|
||||
SymbolModule, |
|
||||
SymbolProfileModule, |
|
||||
TransformDataSourceInRequestModule, |
|
||||
TransformDataSourceInResponseModule |
|
||||
], |
|
||||
providers: [BenchmarkService] |
|
||||
}) |
|
||||
export class BenchmarkModule {} |
|
@ -0,0 +1,63 @@ |
|||||
|
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; |
||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service'; |
||||
|
import { OrderModule } from '@ghostfolio/api/app/order/order.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 { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.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 { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; |
||||
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; |
||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.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 { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; |
||||
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.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 { BenchmarksController } from './benchmarks.controller'; |
||||
|
import { BenchmarksService } from './benchmarks.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [BenchmarksController], |
||||
|
imports: [ |
||||
|
ApiModule, |
||||
|
ConfigurationModule, |
||||
|
DataProviderModule, |
||||
|
ExchangeRateDataModule, |
||||
|
ImpersonationModule, |
||||
|
MarketDataModule, |
||||
|
OrderModule, |
||||
|
PortfolioSnapshotQueueModule, |
||||
|
PrismaModule, |
||||
|
PropertyModule, |
||||
|
RedisCacheModule, |
||||
|
SymbolModule, |
||||
|
SymbolProfileModule, |
||||
|
TransformDataSourceInRequestModule, |
||||
|
TransformDataSourceInResponseModule, |
||||
|
UserModule |
||||
|
], |
||||
|
providers: [ |
||||
|
AccountBalanceService, |
||||
|
AccountService, |
||||
|
BenchmarkService, |
||||
|
BenchmarksService, |
||||
|
CurrentRateService, |
||||
|
MarketDataService, |
||||
|
PortfolioCalculatorFactory, |
||||
|
PortfolioService, |
||||
|
RulesService |
||||
|
] |
||||
|
}) |
||||
|
export class BenchmarksModule {} |
@ -0,0 +1,163 @@ |
|||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
||||
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; |
||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
||||
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
||||
|
import { |
||||
|
AssetProfileIdentifier, |
||||
|
BenchmarkMarketDataDetails, |
||||
|
Filter |
||||
|
} from '@ghostfolio/common/interfaces'; |
||||
|
import { DateRange, UserWithSettings } from '@ghostfolio/common/types'; |
||||
|
|
||||
|
import { Injectable, Logger } from '@nestjs/common'; |
||||
|
import { format, isSameDay } from 'date-fns'; |
||||
|
import { isNumber } from 'lodash'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class BenchmarksService { |
||||
|
public constructor( |
||||
|
private readonly benchmarkService: BenchmarkService, |
||||
|
private readonly exchangeRateDataService: ExchangeRateDataService, |
||||
|
private readonly marketDataService: MarketDataService, |
||||
|
private readonly portfolioService: PortfolioService, |
||||
|
private readonly symbolService: SymbolService |
||||
|
) {} |
||||
|
|
||||
|
public async getMarketDataForUser({ |
||||
|
dataSource, |
||||
|
dateRange, |
||||
|
endDate = new Date(), |
||||
|
filters, |
||||
|
impersonationId, |
||||
|
startDate, |
||||
|
symbol, |
||||
|
user, |
||||
|
withExcludedAccounts |
||||
|
}: { |
||||
|
dateRange: DateRange; |
||||
|
endDate?: Date; |
||||
|
filters?: Filter[]; |
||||
|
impersonationId: string; |
||||
|
startDate: Date; |
||||
|
user: UserWithSettings; |
||||
|
withExcludedAccounts?: boolean; |
||||
|
} & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> { |
||||
|
const marketData: { date: string; value: number }[] = []; |
||||
|
const userCurrency = user.Settings.settings.baseCurrency; |
||||
|
const userId = user.id; |
||||
|
|
||||
|
const { chart } = await this.portfolioService.getPerformance({ |
||||
|
dateRange, |
||||
|
filters, |
||||
|
impersonationId, |
||||
|
userId, |
||||
|
withExcludedAccounts |
||||
|
}); |
||||
|
|
||||
|
const [currentSymbolItem, marketDataItems] = await Promise.all([ |
||||
|
this.symbolService.get({ |
||||
|
dataGatheringItem: { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
} |
||||
|
}), |
||||
|
this.marketDataService.marketDataItems({ |
||||
|
orderBy: { |
||||
|
date: 'asc' |
||||
|
}, |
||||
|
where: { |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
date: { |
||||
|
in: chart.map(({ date }) => { |
||||
|
return resetHours(parseDate(date)); |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
]); |
||||
|
|
||||
|
const exchangeRates = |
||||
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({ |
||||
|
startDate, |
||||
|
currencies: [currentSymbolItem.currency], |
||||
|
targetCurrency: userCurrency |
||||
|
}); |
||||
|
|
||||
|
const exchangeRateAtStartDate = |
||||
|
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ |
||||
|
format(startDate, DATE_FORMAT) |
||||
|
]; |
||||
|
|
||||
|
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => { |
||||
|
return isSameDay(date, startDate); |
||||
|
})?.marketPrice; |
||||
|
|
||||
|
if (!marketPriceAtStartDate) { |
||||
|
Logger.error( |
||||
|
`No historical market data has been found for ${symbol} (${dataSource}) at ${format( |
||||
|
startDate, |
||||
|
DATE_FORMAT |
||||
|
)}`,
|
||||
|
'BenchmarkService' |
||||
|
); |
||||
|
|
||||
|
return { marketData }; |
||||
|
} |
||||
|
|
||||
|
for (const marketDataItem of marketDataItems) { |
||||
|
const exchangeRate = |
||||
|
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ |
||||
|
format(marketDataItem.date, DATE_FORMAT) |
||||
|
]; |
||||
|
|
||||
|
const exchangeRateFactor = |
||||
|
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) |
||||
|
? exchangeRate / exchangeRateAtStartDate |
||||
|
: 1; |
||||
|
|
||||
|
marketData.push({ |
||||
|
date: format(marketDataItem.date, DATE_FORMAT), |
||||
|
value: |
||||
|
marketPriceAtStartDate === 0 |
||||
|
? 0 |
||||
|
: this.benchmarkService.calculateChangeInPercentage( |
||||
|
marketPriceAtStartDate, |
||||
|
marketDataItem.marketPrice * exchangeRateFactor |
||||
|
) * 100 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const includesEndDate = isSameDay( |
||||
|
parseDate(marketData.at(-1).date), |
||||
|
endDate |
||||
|
); |
||||
|
|
||||
|
if (currentSymbolItem?.marketPrice && !includesEndDate) { |
||||
|
const exchangeRate = |
||||
|
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ |
||||
|
format(endDate, DATE_FORMAT) |
||||
|
]; |
||||
|
|
||||
|
const exchangeRateFactor = |
||||
|
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) |
||||
|
? exchangeRate / exchangeRateAtStartDate |
||||
|
: 1; |
||||
|
|
||||
|
marketData.push({ |
||||
|
date: format(endDate, DATE_FORMAT), |
||||
|
value: |
||||
|
this.benchmarkService.calculateChangeInPercentage( |
||||
|
marketPriceAtStartDate, |
||||
|
currentSymbolItem.marketPrice * exchangeRateFactor |
||||
|
) * 100 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
marketData |
||||
|
}; |
||||
|
} |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
import { IsOptional, IsString } from 'class-validator'; |
||||
|
|
||||
|
export class CreateTagDto { |
||||
|
@IsString() |
||||
|
name: string; |
||||
|
|
||||
|
@IsOptional() |
||||
|
@IsString() |
||||
|
userId?: string; |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { TagsController } from './tags.controller'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [TagsController], |
||||
|
imports: [PrismaModule, TagModule] |
||||
|
}) |
||||
|
export class TagsModule {} |
@ -0,0 +1,13 @@ |
|||||
|
import { IsOptional, IsString } from 'class-validator'; |
||||
|
|
||||
|
export class UpdateTagDto { |
||||
|
@IsString() |
||||
|
id: string; |
||||
|
|
||||
|
@IsString() |
||||
|
name: string; |
||||
|
|
||||
|
@IsOptional() |
||||
|
@IsString() |
||||
|
userId?: string; |
||||
|
} |
@ -1,6 +0,0 @@ |
|||||
import { IsString } from 'class-validator'; |
|
||||
|
|
||||
export class CreateTagDto { |
|
||||
@IsString() |
|
||||
name: string; |
|
||||
} |
|
@ -1,14 +0,0 @@ |
|||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|
||||
|
|
||||
import { Module } from '@nestjs/common'; |
|
||||
|
|
||||
import { TagController } from './tag.controller'; |
|
||||
import { TagService } from './tag.service'; |
|
||||
|
|
||||
@Module({ |
|
||||
controllers: [TagController], |
|
||||
exports: [TagService], |
|
||||
imports: [PrismaModule], |
|
||||
providers: [TagService] |
|
||||
}) |
|
||||
export class TagModule {} |
|
@ -1,9 +0,0 @@ |
|||||
import { IsString } from 'class-validator'; |
|
||||
|
|
||||
export class UpdateTagDto { |
|
||||
@IsString() |
|
||||
id: string; |
|
||||
|
|
||||
@IsString() |
|
||||
name: string; |
|
||||
} |
|
@ -0,0 +1,77 @@ |
|||||
|
import { Rule } from '@ghostfolio/api/models/rule'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { UserSettings } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Settings } from './interfaces/rule-settings.interface'; |
||||
|
|
||||
|
export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> { |
||||
|
private asiaPacificValueInBaseCurrency: number; |
||||
|
private currentValueInBaseCurrency: number; |
||||
|
|
||||
|
public constructor( |
||||
|
protected exchangeRateDataService: ExchangeRateDataService, |
||||
|
currentValueInBaseCurrency: number, |
||||
|
asiaPacificValueInBaseCurrency: number |
||||
|
) { |
||||
|
super(exchangeRateDataService, { |
||||
|
key: RegionalMarketClusterRiskAsiaPacific.name, |
||||
|
name: 'Asia-Pacific' |
||||
|
}); |
||||
|
|
||||
|
this.asiaPacificValueInBaseCurrency = asiaPacificValueInBaseCurrency; |
||||
|
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
||||
|
} |
||||
|
|
||||
|
public evaluate(ruleSettings: Settings) { |
||||
|
const asiaPacificMarketValueRatio = this.currentValueInBaseCurrency |
||||
|
? this.asiaPacificValueInBaseCurrency / this.currentValueInBaseCurrency |
||||
|
: 0; |
||||
|
|
||||
|
if (asiaPacificMarketValueRatio > ruleSettings.thresholdMax) { |
||||
|
return { |
||||
|
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( |
||||
|
ruleSettings.thresholdMax * 100 |
||||
|
).toPrecision(3)}%`,
|
||||
|
value: false |
||||
|
}; |
||||
|
} else if (asiaPacificMarketValueRatio < ruleSettings.thresholdMin) { |
||||
|
return { |
||||
|
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is below ${( |
||||
|
ruleSettings.thresholdMin * 100 |
||||
|
).toPrecision(3)}%`,
|
||||
|
value: false |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
||||
|
ruleSettings.thresholdMin * 100 |
||||
|
).toPrecision( |
||||
|
3 |
||||
|
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
|
value: true |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public getConfiguration() { |
||||
|
return { |
||||
|
threshold: { |
||||
|
max: 1, |
||||
|
min: 0, |
||||
|
step: 0.01, |
||||
|
unit: '%' |
||||
|
}, |
||||
|
thresholdMax: true, |
||||
|
thresholdMin: true |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
||||
|
return { |
||||
|
baseCurrency, |
||||
|
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
||||
|
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03, |
||||
|
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02 |
||||
|
}; |
||||
|
} |
||||
|
} |
@ -0,0 +1,77 @@ |
|||||
|
import { Rule } from '@ghostfolio/api/models/rule'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { UserSettings } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Settings } from './interfaces/rule-settings.interface'; |
||||
|
|
||||
|
export class RegionalMarketClusterRiskJapan extends Rule<Settings> { |
||||
|
private currentValueInBaseCurrency: number; |
||||
|
private japanValueInBaseCurrency: number; |
||||
|
|
||||
|
public constructor( |
||||
|
protected exchangeRateDataService: ExchangeRateDataService, |
||||
|
currentValueInBaseCurrency: number, |
||||
|
japanValueInBaseCurrency: number |
||||
|
) { |
||||
|
super(exchangeRateDataService, { |
||||
|
key: RegionalMarketClusterRiskJapan.name, |
||||
|
name: 'Japan' |
||||
|
}); |
||||
|
|
||||
|
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
||||
|
this.japanValueInBaseCurrency = japanValueInBaseCurrency; |
||||
|
} |
||||
|
|
||||
|
public evaluate(ruleSettings: Settings) { |
||||
|
const japanMarketValueRatio = this.currentValueInBaseCurrency |
||||
|
? this.japanValueInBaseCurrency / this.currentValueInBaseCurrency |
||||
|
: 0; |
||||
|
|
||||
|
if (japanMarketValueRatio > ruleSettings.thresholdMax) { |
||||
|
return { |
||||
|
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( |
||||
|
ruleSettings.thresholdMax * 100 |
||||
|
).toPrecision(3)}%`,
|
||||
|
value: false |
||||
|
}; |
||||
|
} else if (japanMarketValueRatio < ruleSettings.thresholdMin) { |
||||
|
return { |
||||
|
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) is below ${( |
||||
|
ruleSettings.thresholdMin * 100 |
||||
|
).toPrecision(3)}%`,
|
||||
|
value: false |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
||||
|
ruleSettings.thresholdMin * 100 |
||||
|
).toPrecision( |
||||
|
3 |
||||
|
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
|
value: true |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public getConfiguration() { |
||||
|
return { |
||||
|
threshold: { |
||||
|
max: 1, |
||||
|
min: 0, |
||||
|
step: 0.01, |
||||
|
unit: '%' |
||||
|
}, |
||||
|
thresholdMax: true, |
||||
|
thresholdMin: true |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
||||
|
return { |
||||
|
baseCurrency, |
||||
|
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
||||
|
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06, |
||||
|
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04 |
||||
|
}; |
||||
|
} |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
||||
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { BenchmarkService } from './benchmark.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
exports: [BenchmarkService], |
||||
|
imports: [ |
||||
|
DataProviderModule, |
||||
|
MarketDataModule, |
||||
|
PrismaModule, |
||||
|
PropertyModule, |
||||
|
RedisCacheModule, |
||||
|
SymbolProfileModule |
||||
|
], |
||||
|
providers: [BenchmarkService] |
||||
|
}) |
||||
|
export class BenchmarkModule {} |
@ -1,18 +1,20 @@ |
|||||
-----BEGIN CERTIFICATE----- |
-----BEGIN CERTIFICATE----- |
||||
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 |
||||
2mOJdLV/DzqoggUsTpPEG0dRhzQLTGHKDQ== |
ezPPMwCNbfamUPlZZnHu74fUrFrDPI9c/YSu8Ex/LegZXJAEzA+8I0g64rjGtzJp |
||||
|
fkRDyQcBuT5SVa+USBlALQmdIuT/fN6R729DcGzvV8JqdoG9sLra4hrRCn3+A3c9 |
||||
|
izZguW1BQNQ2N7II6QCDnWkdUFSQCiQunX/xsg== |
||||
-----END CERTIFICATE----- |
-----END CERTIFICATE----- |
||||
|
@ -1,28 +1,28 @@ |
|||||
-----BEGIN PRIVATE KEY----- |
-----BEGIN PRIVATE KEY----- |
||||
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 |
||||
an3xbjjN+Rq9iKLzmPxIMg== |
x26H2i3DOUFzdgbkoNB0ifHd |
||||
-----END PRIVATE KEY----- |
-----END PRIVATE KEY----- |
||||
|
@ -1,3 +1,17 @@ |
|||||
:host { |
:host { |
||||
display: block; |
display: block; |
||||
|
|
||||
|
.icon-container { |
||||
|
background-color: rgba(var(--palette-foreground-base), 0.02); |
||||
|
border-radius: 0.25rem; |
||||
|
height: 2rem; |
||||
|
|
||||
|
&.okay { |
||||
|
color: var(--success); |
||||
|
} |
||||
|
|
||||
|
&.warn { |
||||
|
color: var(--danger); |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
@ -1,19 +1,58 @@ |
|||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; |
import { DataService } from '@ghostfolio/client/services/data.service'; |
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
|
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { MatStepper } from '@angular/material/stepper'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
@Component({ |
@Component({ |
||||
selector: 'gf-show-access-token-dialog', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
selector: 'gf-show-access-token-dialog', |
||||
|
standalone: false, |
||||
styleUrls: ['./show-access-token-dialog.scss'], |
styleUrls: ['./show-access-token-dialog.scss'], |
||||
templateUrl: 'show-access-token-dialog.html', |
templateUrl: 'show-access-token-dialog.html' |
||||
standalone: false |
|
||||
}) |
}) |
||||
export class ShowAccessTokenDialog { |
export class ShowAccessTokenDialog { |
||||
public isAgreeButtonDisabled = true; |
@ViewChild(MatStepper) stepper!: MatStepper; |
||||
|
|
||||
|
public accessToken: string; |
||||
|
public authToken: string; |
||||
|
public isCreateAccountButtonDisabled = true; |
||||
|
public isDisclaimerChecked = false; |
||||
|
public role: string; |
||||
|
|
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
public constructor( |
||||
|
private changeDetectorRef: ChangeDetectorRef, |
||||
|
private dataService: DataService |
||||
|
) {} |
||||
|
|
||||
public constructor(@Inject(MAT_DIALOG_DATA) public data: any) {} |
public createAccount() { |
||||
|
this.dataService |
||||
|
.postUser() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(({ accessToken, authToken, role }) => { |
||||
|
this.accessToken = accessToken; |
||||
|
this.authToken = authToken; |
||||
|
this.role = role; |
||||
|
|
||||
|
this.stepper.next(); |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public enableCreateAccountButton() { |
||||
|
this.isCreateAccountButtonDisabled = false; |
||||
|
} |
||||
|
|
||||
public enableAgreeButton() { |
public onChangeDislaimerChecked() { |
||||
this.isAgreeButtonDisabled = false; |
this.isDisclaimerChecked = !this.isDisclaimerChecked; |
||||
} |
} |
||||
} |
} |
||||
|
@ -1,48 +1,93 @@ |
|||||
<h1 mat-dialog-title> |
<h1 mat-dialog-title> |
||||
<span i18n>Create Account</span> |
<span i18n>Create Account</span> |
||||
@if (data.role === 'ADMIN') { |
@if (role === 'ADMIN') { |
||||
<span class="badge badge-light ml-2">{{ data.role }}</span> |
<span class="badge badge-light ml-2">{{ role }}</span> |
||||
} |
} |
||||
</h1> |
</h1> |
||||
<div class="py-3" mat-dialog-content> |
<div class="px-0" mat-dialog-content> |
||||
|
<mat-stepper #stepper animationDuration="0ms" linear> |
||||
|
<mat-step editable="false" [completed]="isDisclaimerChecked"> |
||||
|
<ng-template i18n matStepLabel>Terms and Conditions</ng-template> |
||||
|
<div class="pt-2"> |
||||
|
<ng-container i18n |
||||
|
>Please keep your security token safe. If you lose it, you will not be |
||||
|
able to recover your account.</ng-container |
||||
|
> |
||||
|
</div> |
||||
|
<mat-checkbox |
||||
|
class="pt-2" |
||||
|
color="primary" |
||||
|
(change)="onChangeDislaimerChecked()" |
||||
|
> |
||||
|
<ng-container i18n |
||||
|
>I understand that if I lose my security token, I cannot recover my |
||||
|
account.</ng-container |
||||
|
> |
||||
|
</mat-checkbox> |
||||
|
<div class="mt-3" mat-dialog-actions> |
||||
|
<div class="flex-grow-1"> |
||||
|
<button i18n mat-button [mat-dialog-close]="undefined">Cancel</button> |
||||
|
</div> |
||||
<div> |
<div> |
||||
<mat-form-field appearance="outline" class="w-100"> |
<button |
||||
|
color="primary" |
||||
|
mat-flat-button |
||||
|
[disabled]="!isDisclaimerChecked" |
||||
|
(click)="createAccount()" |
||||
|
> |
||||
|
<ng-container i18n>Continue</ng-container> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-step> |
||||
|
<mat-step editable="false"> |
||||
|
<ng-template i18n matStepLabel>Security Token</ng-template> |
||||
|
<div class="pt-2"> |
||||
|
<ng-container i18n |
||||
|
>Here is your security token. It is only visible once, please store |
||||
|
and keep it in a safe place.</ng-container |
||||
|
> |
||||
|
</div> |
||||
|
<mat-form-field appearance="outline" class="pt-3 w-100 without-hint"> |
||||
<mat-label i18n>Security Token</mat-label> |
<mat-label i18n>Security Token</mat-label> |
||||
<textarea |
<textarea |
||||
cdkTextareaAutosize |
cdkTextareaAutosize |
||||
matInput |
matInput |
||||
readonly |
readonly |
||||
type="text" |
type="text" |
||||
[(value)]="data.accessToken" |
[(value)]="accessToken" |
||||
></textarea> |
></textarea> |
||||
<div class="float-right mt-3"> |
<div class="float-right mt-1"> |
||||
<button |
<button |
||||
color="secondary" |
color="secondary" |
||||
mat-flat-button |
mat-flat-button |
||||
[cdkCopyToClipboard]="data.accessToken" |
[cdkCopyToClipboard]="accessToken" |
||||
(click)="enableAgreeButton()" |
(click)="enableCreateAccountButton()" |
||||
> |
|
||||
<ion-icon class="mr-1" name="copy-outline" /><span i18n |
|
||||
>Copy to clipboard</span |
|
||||
> |
> |
||||
|
<ion-icon class="mr-1" name="copy-outline" /> |
||||
|
<span i18n>Copy to clipboard</span> |
||||
</button> |
</button> |
||||
</div> |
</div> |
||||
</mat-form-field> |
</mat-form-field> |
||||
</div> |
<div align="end" class="mt-1" mat-dialog-actions> |
||||
<p i18n> |
<div> |
||||
I agree to have stored my <i>Security Token</i> from above in a secure |
|
||||
place. If I lose it, I cannot get my account back. |
|
||||
</p> |
|
||||
</div> |
|
||||
<div class="justify-content-end" mat-dialog-actions> |
|
||||
<button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button> |
|
||||
<button |
<button |
||||
color="primary" |
color="primary" |
||||
mat-flat-button |
mat-flat-button |
||||
[disabled]="isAgreeButtonDisabled" |
matStepperNext |
||||
[mat-dialog-close]="data" |
[disabled]="isCreateAccountButtonDisabled" |
||||
|
[mat-dialog-close]="authToken" |
||||
> |
> |
||||
<span i18n>Agree and continue</span> |
<span i18n>Create Account</span> |
||||
<ion-icon class="ml-1" name="arrow-forward-outline" /> |
<ion-icon class="ml-1" name="arrow-forward-outline" /> |
||||
</button> |
</button> |
||||
</div> |
</div> |
||||
|
</div> |
||||
|
</mat-step> |
||||
|
|
||||
|
<ng-template matStepperIcon="done"> |
||||
|
<ion-icon name="checkmark-outline"></ion-icon> |
||||
|
</ng-template> |
||||
|
</mat-stepper> |
||||
|
<div></div> |
||||
|
</div> |
||||
|
@ -1,2 +1,6 @@ |
|||||
:host { |
:host { |
||||
|
.mat-mdc-dialog-actions { |
||||
|
padding-left: 0 !important; |
||||
|
padding-right: 0 !important; |
||||
|
} |
||||
} |
} |
||||
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue