Browse Source

Merge branch 'feature/oidc-auth' of https://github.com/gmag11/ghostfolio into feature/oidc-auth

pull/5981/head
Germán Martín 1 week ago
parent
commit
cd0b9d2c37
  1. 17
      CHANGELOG.md
  2. 4
      apps/api/src/app/admin/admin.controller.ts
  3. 2
      apps/api/src/app/auth/auth.controller.ts
  4. 7
      apps/api/src/app/auth/auth.module.ts
  5. 19
      apps/api/src/app/auth/interfaces/interfaces.ts
  6. 11
      apps/api/src/app/auth/oidc-state.store.ts
  7. 30
      apps/api/src/app/auth/oidc.strategy.ts
  8. 16
      apps/api/src/services/configuration/configuration.service.ts
  9. 4
      apps/api/src/services/cron/cron.service.ts
  10. 36
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  11. 45
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  12. 87
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  13. 34
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  14. 53
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  15. 20
      apps/client-e2e/.eslintrc.json
  16. 12
      apps/client-e2e/cypress.json
  17. 22
      apps/client-e2e/project.json
  18. 4
      apps/client-e2e/src/fixtures/example.json
  19. 13
      apps/client-e2e/src/integration/app.spec.ts
  20. 22
      apps/client-e2e/src/plugins/index.js
  21. 1
      apps/client-e2e/src/support/app.po.ts
  22. 31
      apps/client-e2e/src/support/commands.ts
  23. 16
      apps/client-e2e/src/support/index.ts
  24. 10
      apps/client-e2e/tsconfig.e2e.json
  25. 10
      apps/client-e2e/tsconfig.json
  26. 2
      apps/client/src/app/components/access-table/access-table.component.html
  27. 6
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  28. 2
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  29. 2
      apps/client/src/app/components/admin-platform/admin-platform.component.html
  30. 2
      apps/client/src/app/components/admin-tag/admin-tag.component.html
  31. 4
      apps/client/src/app/components/admin-users/admin-users.html
  32. 11
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  33. 13
      apps/ui-e2e/cypress.json
  34. 33
      apps/ui-e2e/eslint.config.cjs
  35. 28
      apps/ui-e2e/project.json
  36. 4
      apps/ui-e2e/src/fixtures/example.json
  37. 6
      apps/ui-e2e/src/integration/value/value.component.spec.ts
  38. 22
      apps/ui-e2e/src/plugins/index.js
  39. 33
      apps/ui-e2e/src/support/commands.ts
  40. 16
      apps/ui-e2e/src/support/index.ts
  41. 10
      apps/ui-e2e/tsconfig.json
  42. 6
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  43. 18
      libs/ui/src/lib/activities-table/activities-table.component.html
  44. 46
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts
  45. 422
      package-lock.json
  46. 11
      package.json
  47. 2
      prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql

17
CHANGELOG.md

@ -9,12 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added OIDC (_OpenID Connect_) as a login auth provider
- Added _OpenID Connect_ (`OIDC`) as a new login provider (experimental)
### Changed
- Refactored the API query parameters in various data provider services
- Extended the _Storybook_ stories of the portfolio proportion chart component by a story using percentage values
- Upgraded `@internationalized/number` from version `3.6.3` to `3.6.5`
### Fixed
- Improved the country weightings in the _Financial Modeling Prep_ service
## 2.220.0 - 2025-11-29
### Changed
- Restricted the asset profile data gathering on Sundays to only process outdated asset profiles
- Removed the _Cypress_ testing setup
- Eliminated `uuid` in favor of using `randomUUID` from `node:crypto`
- Upgraded `color` from version `5.0.0` to `5.0.3`
- Upgraded `prettier` from version `3.6.2` to `3.7.2`
### Fixed

4
apps/api/src/app/admin/admin.controller.ts

@ -93,7 +93,7 @@ export class AdminController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherMax(): Promise<void> {
const assetProfileIdentifiers =
await this.dataGatheringService.getAllActiveAssetProfileIdentifiers();
await this.dataGatheringService.getActiveAssetProfileIdentifiers();
await this.dataGatheringService.addJobsToQueue(
assetProfileIdentifiers.map(({ dataSource, symbol }) => {
@ -120,7 +120,7 @@ export class AdminController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherProfileData(): Promise<void> {
const assetProfileIdentifiers =
await this.dataGatheringService.getAllActiveAssetProfileIdentifiers();
await this.dataGatheringService.getActiveAssetProfileIdentifiers();
await this.dataGatheringService.addJobsToQueue(
assetProfileIdentifiers.map(({ dataSource, symbol }) => {

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

@ -111,8 +111,6 @@ export class AuthController {
StatusCodes.FORBIDDEN
);
}
// Initiates the OIDC login flow
}
@Get('oidc/callback')

7
apps/api/src/app/auth/auth.module.ts

@ -60,6 +60,7 @@ import { OidcStrategy } from './oidc.strategy';
const response = await fetch(
`${issuer}/.well-known/openid-configuration`
);
const config = (await response.json()) as {
authorization_endpoint: string;
token_endpoint: string;
@ -67,12 +68,12 @@ import { OidcStrategy } from './oidc.strategy';
};
options = {
issuer,
scope,
authorizationURL: config.authorization_endpoint,
callbackURL: callbackUrl,
clientID: configurationService.get('OIDC_CLIENT_ID'),
clientSecret: configurationService.get('OIDC_CLIENT_SECRET'),
issuer,
scope,
tokenURL: config.token_endpoint,
userInfoURL: config.userinfo_endpoint
};
@ -82,6 +83,7 @@ import { OidcStrategy } from './oidc.strategy';
}
} else {
options = {
scope,
authorizationURL: configurationService.get(
'OIDC_AUTHORIZATION_URL'
),
@ -89,7 +91,6 @@ import { OidcStrategy } from './oidc.strategy';
clientID: configurationService.get('OIDC_CLIENT_ID'),
clientSecret: configurationService.get('OIDC_CLIENT_SECRET'),
issuer: configurationService.get('OIDC_ISSUER'),
scope,
tokenURL: configurationService.get('OIDC_TOKEN_URL'),
userInfoURL: configurationService.get('OIDC_USER_INFO_URL')
};

19
apps/api/src/app/auth/interfaces/interfaces.ts

@ -6,6 +6,25 @@ export interface AuthDeviceDialogParams {
authDevice: AuthDeviceDto;
}
export interface OidcContext {
claims?: {
sub?: string;
};
}
export interface OidcIdToken {
sub?: string;
}
export interface OidcParams {
sub?: string;
}
export interface OidcProfile {
id?: string;
sub?: string;
}
export interface ValidateOAuthLoginParams {
provider: Provider;
thirdPartyId: string;

11
apps/api/src/app/auth/oidc-state.store.ts

@ -5,6 +5,8 @@ import ms from 'ms';
* This store manages OAuth2 state parameters in memory with automatic cleanup.
*/
export class OidcStateStore {
private readonly STATE_EXPIRY_MS = ms('10 minutes');
private stateMap = new Map<
string,
{
@ -14,7 +16,6 @@ export class OidcStateStore {
timestamp: number;
}
>();
private readonly STATE_EXPIRY_MS = ms('10 minutes');
/**
* Store request state.
@ -26,7 +27,7 @@ export class OidcStateStore {
appState: unknown,
ctx: { maxAge?: number; nonce?: string; issued?: Date },
callback: (err: Error | null, handle?: string) => void
): void {
) {
try {
// Generate a unique handle for this state
const handle = this.generateHandle();
@ -59,7 +60,7 @@ export class OidcStateStore {
appState?: unknown,
ctx?: { maxAge?: number; nonce?: string; issued?: Date }
) => void
): void {
) {
try {
const data = this.stateMap.get(handle);
@ -85,7 +86,7 @@ export class OidcStateStore {
/**
* Clean up expired states
*/
private cleanup(): void {
private cleanup() {
const now = Date.now();
const expiredKeys: string[] = [];
@ -103,7 +104,7 @@ export class OidcStateStore {
/**
* Generate a cryptographically secure random handle
*/
private generateHandle(): string {
private generateHandle() {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15) +

30
apps/api/src/app/auth/oidc.strategy.ts

@ -5,27 +5,14 @@ import { Request } from 'express';
import { Strategy, type StrategyOptions } from 'passport-openidconnect';
import { AuthService } from './auth.service';
import {
OidcContext,
OidcIdToken,
OidcParams,
OidcProfile
} from './interfaces/interfaces';
import { OidcStateStore } from './oidc-state.store';
interface OidcProfile {
id?: string;
sub?: string;
}
interface OidcContext {
claims?: {
sub?: string;
};
}
interface OidcIdToken {
sub?: string;
}
interface OidcParams {
sub?: string;
}
@Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private static readonly stateStore = new OidcStateStore();
@ -60,8 +47,8 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
context?.claims?.sub;
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.OIDC,
thirdPartyId
thirdPartyId,
provider: Provider.OIDC
});
if (!thirdPartyId) {
@ -69,6 +56,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
`Missing subject identifier in OIDC response from ${issuer}`,
'OidcStrategy'
);
throw new Error('Missing subject identifier in OIDC response');
}

16
apps/api/src/services/configuration/configuration.service.ts

@ -55,17 +55,17 @@ export class ConfigurationService {
GOOGLE_SHEETS_ID: str({ default: '' }),
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
HOST: host({ default: DEFAULT_HOST }),
JWT_SECRET_KEY: str({}),
JWT_SECRET_KEY: str(),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_CHART_ITEMS: num({ default: 365 }),
OIDC_AUTHORIZATION_URL: str({ default: undefined }),
OIDC_CALLBACK_URL: str({ default: undefined }),
OIDC_CLIENT_ID: str({ default: undefined }),
OIDC_CLIENT_SECRET: str({ default: undefined }),
OIDC_ISSUER: str({ default: undefined }),
OIDC_AUTHORIZATION_URL: str({ default: '' }),
OIDC_CALLBACK_URL: str({ default: '' }),
OIDC_CLIENT_ID: str({ default: '' }),
OIDC_CLIENT_SECRET: str({ default: '' }),
OIDC_ISSUER: str({ default: '' }),
OIDC_SCOPE: json({ default: ['openid'] }),
OIDC_TOKEN_URL: str({ default: undefined }),
OIDC_USER_INFO_URL: str({ default: undefined }),
OIDC_TOKEN_URL: str({ default: '' }),
OIDC_USER_INFO_URL: str({ default: '' }),
PORT: port({ default: DEFAULT_PORT }),
PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({
default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY

4
apps/api/src/services/cron/cron.service.ts

@ -59,7 +59,9 @@ export class CronService {
public async runEverySundayAtTwelvePm() {
if (await this.isDataGatheringEnabled()) {
const assetProfileIdentifiers =
await this.dataGatheringService.getAllActiveAssetProfileIdentifiers();
await this.dataGatheringService.getActiveAssetProfileIdentifiers({
maxAge: '60 days'
});
await this.dataGatheringService.addJobsToQueue(
assetProfileIdentifiers.map(({ dataSource, symbol }) => {

36
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -110,12 +110,14 @@ export class CoinGeckoService implements DataProviderInterface {
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const queryParams = new URLSearchParams({
from: getUnixTime(from).toString(),
to: getUnixTime(to).toString(),
vs_currency: DEFAULT_CURRENCY.toLowerCase()
});
const { error, prices, status } = await fetch(
`${
this.apiUrl
}/coins/${symbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime(
from
)}&to=${getUnixTime(to)}`,
`${this.apiUrl}/coins/${symbol}/market_chart/range?${queryParams.toString()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
@ -172,10 +174,13 @@ export class CoinGeckoService implements DataProviderInterface {
}
try {
const queryParams = new URLSearchParams({
ids: symbols.join(','),
vs_currencies: DEFAULT_CURRENCY.toLowerCase()
});
const quotes = await fetch(
`${this.apiUrl}/simple/price?ids=${symbols.join(
','
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
`${this.apiUrl}/simple/price?${queryParams.toString()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
@ -219,10 +224,17 @@ export class CoinGeckoService implements DataProviderInterface {
let items: LookupItem[] = [];
try {
const { coins } = await fetch(`${this.apiUrl}/search?query=${query}`, {
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}).then((res) => res.json());
const queryParams = new URLSearchParams({
query
});
const { coins } = await fetch(
`${this.apiUrl}/search?${queryParams.toString()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
items = coins.map(({ id: symbol, name }) => {
return {

45
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -96,17 +96,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
}
try {
const queryParams = new URLSearchParams({
api_token: this.apiKey,
fmt: 'json',
from: format(from, DATE_FORMAT),
to: format(to, DATE_FORMAT)
});
const response: {
[date: string]: DataProviderHistoricalResponse;
} = {};
const historicalResult = await fetch(
`${this.URL}/div/${symbol}?api_token=${
this.apiKey
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
to,
DATE_FORMAT
)}`,
`${this.URL}/div/${symbol}?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -144,13 +146,16 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol = this.convertToEodSymbol(symbol);
try {
const queryParams = new URLSearchParams({
api_token: this.apiKey,
fmt: 'json',
from: format(from, DATE_FORMAT),
period: granularity,
to: format(to, DATE_FORMAT)
});
const response = await fetch(
`${this.URL}/eod/${symbol}?api_token=${
this.apiKey
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
to,
DATE_FORMAT
)}&period=${granularity}`,
`${this.URL}/eod/${symbol}?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -208,10 +213,14 @@ export class EodHistoricalDataService implements DataProviderInterface {
});
try {
const queryParams = new URLSearchParams({
api_token: this.apiKey,
fmt: 'json',
s: eodHistoricalDataSymbols.join(',')
});
const realTimeResponse = await fetch(
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${
this.apiKey
}&fmt=json&s=${eodHistoricalDataSymbols.join(',')}`,
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -413,8 +422,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
})[] = [];
try {
const queryParams = new URLSearchParams({
api_token: this.apiKey
});
const response = await fetch(
`${this.URL}/search/${query}?api_token=${this.apiKey}`,
`${this.URL}/search/${query}?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}

87
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -44,6 +44,12 @@ import {
@Injectable()
export class FinancialModelingPrepService implements DataProviderInterface {
private static countriesMapping = {
'Korea (the Republic of)': 'South Korea',
'Russian Federation': 'Russia',
'Taiwan (Province of China)': 'Taiwan'
};
private apiKey: string;
public constructor(
@ -79,8 +85,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
symbol.length - DEFAULT_CURRENCY.length
);
} else if (this.cryptocurrencyService.isCryptocurrency(symbol)) {
const queryParams = new URLSearchParams({
symbol,
apikey: this.apiKey
});
const [quote] = await fetch(
`${this.getUrl({ version: 'stable' })}/quote?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/quote?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -93,8 +104,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
);
response.name = quote.name;
} else {
const queryParams = new URLSearchParams({
symbol,
apikey: this.apiKey
});
const [assetProfile] = await fetch(
`${this.getUrl({ version: 'stable' })}/profile?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/profile?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -114,19 +130,31 @@ export class FinancialModelingPrepService implements DataProviderInterface {
assetSubClass === AssetSubClass.ETF ||
assetSubClass === AssetSubClass.MUTUALFUND
) {
const queryParams = new URLSearchParams({
symbol,
apikey: this.apiKey
});
const etfCountryWeightings = await fetch(
`${this.getUrl({ version: 'stable' })}/etf/country-weightings?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/etf/country-weightings?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
response.countries = etfCountryWeightings.map(
({ country: countryName, weightPercentage }) => {
response.countries = etfCountryWeightings
.filter(({ country: countryName }) => {
return countryName.toLowerCase() !== 'other';
})
.map(({ country: countryName, weightPercentage }) => {
let countryCode: string;
for (const [code, country] of Object.entries(countries)) {
if (country.name === countryName) {
if (
country.name === countryName ||
country.name ===
FinancialModelingPrepService.countriesMapping[countryName]
) {
countryCode = code;
break;
}
@ -136,11 +164,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
code: countryCode,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
});
const etfHoldings = await fetch(
`${this.getUrl({ version: 'stable' })}/etf/holdings?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/etf/holdings?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -159,7 +186,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
);
const [etfInformation] = await fetch(
`${this.getUrl({ version: 'stable' })}/etf/info?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/etf/info?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -170,7 +197,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
const etfSectorWeightings = await fetch(
`${this.getUrl({ version: 'stable' })}/etf/sector-weightings?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/etf/sector-weightings?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -242,12 +269,17 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
try {
const queryParams = new URLSearchParams({
symbol,
apikey: this.apiKey
});
const response: {
[date: string]: DataProviderHistoricalResponse;
} = {};
const dividends = await fetch(
`${this.getUrl({ version: 'stable' })}/dividends?symbol=${symbol}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/dividends?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -307,8 +339,15 @@ export class FinancialModelingPrepService implements DataProviderInterface {
? addYears(currentFrom, MAX_YEARS_PER_REQUEST)
: to;
const queryParams = new URLSearchParams({
symbol,
apikey: this.apiKey,
from: format(currentFrom, DATE_FORMAT),
to: format(currentTo, DATE_FORMAT)
});
const historical = await fetch(
`${this.getUrl({ version: 'stable' })}/historical-price-eod/full?symbol=${symbol}&apikey=${this.apiKey}&from=${format(currentFrom, DATE_FORMAT)}&to=${format(currentTo, DATE_FORMAT)}`,
`${this.getUrl({ version: 'stable' })}/historical-price-eod/full?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -363,6 +402,11 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol: string]: Pick<SymbolProfile, 'currency'>;
} = {};
const queryParams = new URLSearchParams({
symbols: symbols.join(','),
apikey: this.apiKey
});
const [assetProfileResolutions, quotes] = await Promise.all([
this.prismaService.assetProfileResolution.findMany({
where: {
@ -371,7 +415,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
}),
fetch(
`${this.getUrl({ version: 'stable' })}/batch-quote-short?symbols=${symbols.join(',')}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/batch-quote-short?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -463,12 +507,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const assetProfileBySymbolMap: {
[symbol: string]: Partial<SymbolProfile>;
} = {};
let items: LookupItem[] = [];
try {
if (isISIN(query?.toUpperCase())) {
const queryParams = new URLSearchParams({
apikey: this.apiKey,
isin: query.toUpperCase()
});
const result = await fetch(
`${this.getUrl({ version: 'stable' })}/search-isin?isin=${query.toUpperCase()}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/search-isin?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
@ -494,8 +544,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
};
});
} else {
const queryParams = new URLSearchParams({
query,
apikey: this.apiKey
});
const result = await fetch(
`${this.getUrl({ version: 'stable' })}/search-symbol?query=${query}&apikey=${this.apiKey}`,
`${this.getUrl({ version: 'stable' })}/search-symbol?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')

34
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -116,11 +116,14 @@ export class GhostfolioService implements DataProviderInterface {
} = {};
try {
const queryParams = new URLSearchParams({
granularity,
from: format(from, DATE_FORMAT),
to: format(to, DATE_FORMAT)
});
const response = await fetch(
`${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format(
to,
DATE_FORMAT
)}`,
`${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?${queryParams.toString()}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
@ -165,11 +168,14 @@ export class GhostfolioService implements DataProviderInterface {
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const queryParams = new URLSearchParams({
granularity,
from: format(from, DATE_FORMAT),
to: format(to, DATE_FORMAT)
});
const response = await fetch(
`${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format(
to,
DATE_FORMAT
)}`,
`${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?${queryParams.toString()}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
@ -235,8 +241,12 @@ export class GhostfolioService implements DataProviderInterface {
}
try {
const queryParams = new URLSearchParams({
symbols: symbols.join(',')
});
const response = await fetch(
`${this.URL}/v2/data-providers/ghostfolio/quotes?symbols=${symbols.join(',')}`,
`${this.URL}/v2/data-providers/ghostfolio/quotes?${queryParams.toString()}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
@ -288,8 +298,12 @@ export class GhostfolioService implements DataProviderInterface {
let searchResult: LookupResponse = { items: [] };
try {
const queryParams = new URLSearchParams({
query
});
const response = await fetch(
`${this.URL}/v2/data-providers/ghostfolio/lookup?query=${query}`,
`${this.URL}/v2/data-providers/ghostfolio/lookup?${queryParams.toString()}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)

53
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -28,8 +28,9 @@ import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { JobOptions, Queue } from 'bull';
import { format, min, subDays, subYears } from 'date-fns';
import { format, min, subDays, subMilliseconds, subYears } from 'date-fns';
import { isEmpty } from 'lodash';
import ms, { StringValue } from 'ms';
@Injectable()
export class DataGatheringService {
@ -160,8 +161,7 @@ export class DataGatheringService {
);
if (!assetProfileIdentifiers) {
assetProfileIdentifiers =
await this.getAllActiveAssetProfileIdentifiers();
assetProfileIdentifiers = await this.getActiveAssetProfileIdentifiers();
}
if (assetProfileIdentifiers.length <= 0) {
@ -301,29 +301,36 @@ export class DataGatheringService {
);
}
public async getAllActiveAssetProfileIdentifiers(): Promise<
AssetProfileIdentifier[]
> {
const symbolProfiles = await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }],
/**
* Returns active asset profile identifiers
*
* @param {StringValue} maxAge - Optional. Specifies the maximum allowed age
* of a profiles last update timestamp. Only asset profiles considered stale
* are returned.
*/
public async getActiveAssetProfileIdentifiers({
maxAge
}: {
maxAge?: StringValue;
} = {}): Promise<AssetProfileIdentifier[]> {
return this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }, { dataSource: 'asc' }],
select: {
dataSource: true,
symbol: true
},
where: {
isActive: true
dataSource: {
notIn: ['MANUAL', 'RAPID_API']
},
isActive: true,
...(maxAge && {
updatedAt: {
lt: subMilliseconds(new Date(), ms(maxAge))
}
})
}
});
return symbolProfiles
.filter(({ dataSource }) => {
return (
dataSource !== DataSource.MANUAL &&
dataSource !== DataSource.RAPID_API
);
})
.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
}
private async getAssetProfileIdentifiersWithCompleteMarketData(): Promise<

20
apps/client-e2e/.eslintrc.json

@ -1,20 +0,0 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/client-e2e/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}

12
apps/client-e2e/cypress.json

@ -1,12 +0,0 @@
{
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"pluginsFile": "./src/plugins/index",
"supportFile": "./src/support/index.ts",
"video": true,
"videosFolder": "../../dist/cypress/apps/client-e2e/videos",
"screenshotsFolder": "../../dist/cypress/apps/client-e2e/screenshots",
"chromeWebSecurity": false
}

22
apps/client-e2e/project.json

@ -1,22 +0,0 @@
{
"name": "client-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/client-e2e/src",
"projectType": "application",
"tags": [],
"implicitDependencies": ["client"],
"targets": {
"e2e": {
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "apps/client-e2e/cypress.json",
"devServerTarget": "client:serve"
},
"configurations": {
"production": {
"devServerTarget": "client:serve:production"
}
}
}
}
}

4
apps/client-e2e/src/fixtures/example.json

@ -1,4 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

13
apps/client-e2e/src/integration/app.spec.ts

@ -1,13 +0,0 @@
import { getGreeting } from '../support/app.po';
describe('client', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
// Custom command example, see `../support/commands.ts` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see `../support/app.po.ts` file
getGreeting().contains('Welcome to client!');
});
});

22
apps/client-e2e/src/plugins/index.js

@ -1,22 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { preprocessTypescript } = require('@nx/cypress/plugins/preprocessor');
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// Preprocess Typescript file using Nx helper
on('file:preprocessor', preprocessTypescript(config));
};

1
apps/client-e2e/src/support/app.po.ts

@ -1 +0,0 @@
export const getGreeting = () => cy.get('h1');

31
apps/client-e2e/src/support/commands.ts

@ -1,31 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

16
apps/client-e2e/src/support/index.ts

@ -1,16 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';

10
apps/client-e2e/tsconfig.e2e.json

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js"]
}

10
apps/client-e2e/tsconfig.json

@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.e2e.json"
}
]
}

2
apps/client/src/app/components/access-table/access-table.component.html

@ -73,7 +73,7 @@
<button mat-menu-item (click)="onUpdateAccess(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
}

6
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -205,14 +205,16 @@
</button>
<mat-menu #jobActionsMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onViewData(element.data)">
<ng-container i18n>View Data</ng-container>
<span><ng-container i18n>View Data</ng-container>...</span>
</button>
<button
mat-menu-item
[disabled]="element.stacktrace?.length <= 0"
(click)="onViewStacktrace(element.stacktrace)"
>
<ng-container i18n>View Stacktrace</ng-container>
<span
><ng-container i18n>View Stacktrace</ng-container>...</span
>
</button>
<button mat-menu-item (click)="onExecuteJob(element.id)">
<ng-container i18n>Execute Job</ng-container>

2
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -264,7 +264,7 @@
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</a>
<hr class="m-0" />

2
apps/client/src/app/components/admin-platform/admin-platform.component.html

@ -71,7 +71,7 @@
<button mat-menu-item (click)="onUpdatePlatform(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />

2
apps/client/src/app/components/admin-tag/admin-tag.component.html

@ -64,7 +64,7 @@
<button mat-menu-item (click)="onUpdateTag(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />

4
apps/client/src/app/components/admin-users/admin-users.html

@ -222,7 +222,9 @@
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="person-outline" />
<span i18n>View Details</span>
<span
><ng-container i18n>View Details</ng-container>...</span
>
</span>
</button>
@if (hasPermissionToImpersonateAllUsers) {

11
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -56,6 +56,17 @@
>
</div>
}
@if (data.hasPermissionToUseAuthOidc) {
<div class="d-flex flex-column mt-2">
<a
class="px-4 rounded-pill"
href="../api/auth/oidc"
mat-stroked-button
><ng-container i18n>Sign in with OIDC</ng-container></a
>
</div>
}
</form>
</div>
</div>

13
apps/ui-e2e/cypress.json

@ -1,13 +0,0 @@
{
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"supportFile": "./src/support/index.ts",
"pluginsFile": "./src/plugins/index",
"video": true,
"videosFolder": "../../dist/cypress/apps/ui-e2e/videos",
"screenshotsFolder": "../../dist/cypress/apps/ui-e2e/screenshots",
"chromeWebSecurity": false,
"baseUrl": "http://localhost:4400"
}

33
apps/ui-e2e/eslint.config.cjs

@ -1,33 +0,0 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../../eslint.config.cjs');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
...compat.extends('plugin:cypress/recommended'),
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/ui-e2e/tsconfig.json']
}
}
},
{
files: ['src/plugins/index.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'no-undef': 'off'
}
}
];

28
apps/ui-e2e/project.json

@ -1,28 +0,0 @@
{
"name": "ui-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/ui-e2e/src",
"projectType": "application",
"tags": [],
"implicitDependencies": ["ui"],
"targets": {
"e2e": {
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "apps/ui-e2e/cypress.json",
"devServerTarget": "ui:storybook"
},
"configurations": {
"ci": {
"devServerTarget": "ui:storybook:ci"
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"options": {
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
}
}
}
}

4
apps/ui-e2e/src/fixtures/example.json

@ -1,4 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

6
apps/ui-e2e/src/integration/value/value.component.spec.ts

@ -1,6 +0,0 @@
describe('ui', () => {
beforeEach(() => cy.visit('/iframe.html?id=valuecomponent--loading'));
it('should render the component', () => {
cy.get('gf-value').should('exist');
});
});

22
apps/ui-e2e/src/plugins/index.js

@ -1,22 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { preprocessTypescript } = require('@nx/cypress/plugins/preprocessor');
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// Preprocess Typescript file using Nx helper
on('file:preprocessor', preprocessTypescript(config));
};

33
apps/ui-e2e/src/support/commands.ts

@ -1,33 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

16
apps/ui-e2e/src/support/index.ts

@ -1,16 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';

10
apps/ui-e2e/tsconfig.json

@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js"]
}

6
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -7,7 +7,7 @@
(click)="onTransferBalance()"
>
<ion-icon class="mr-2" name="arrow-redo-outline" />
<ng-container i18n>Transfer Cash Balance</ng-container>...
<span><ng-container i18n>Transfer Cash Balance</ng-container>...</span>
</button>
</div>
}
@ -304,13 +304,13 @@
<button mat-menu-item (click)="onOpenAccountDetailDialog(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="wallet-outline" />
<span i18n>View Details</span>
<span><ng-container i18n>View Details</ng-container>...</span>
</span>
</button>
<button mat-menu-item (click)="onUpdateAccount(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />

18
libs/ui/src/lib/activities-table/activities-table.component.html

@ -6,7 +6,7 @@
(click)="onImport()"
>
<ion-icon class="mr-2" name="cloud-upload-outline" />
<ng-container i18n>Import Activities</ng-container>...
<span><ng-container i18n>Import Activities</ng-container>...</span>
</button>
@if (hasPermissionToExportActivities) {
<button
@ -26,7 +26,7 @@
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline" />
<ng-container i18n>Import Dividends</ng-container>...
<span><ng-container i18n>Import Dividends</ng-container>...</span>
</span>
</button>
@if (hasPermissionToExportActivities) {
@ -379,7 +379,9 @@
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-upload-outline" />
<ng-container i18n>Import Activities</ng-container>...
<span
><ng-container i18n>Import Activities</ng-container>...</span
>
</span>
</button>
}
@ -391,7 +393,9 @@
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline" />
<ng-container i18n>Import Dividends</ng-container>...
<span
><ng-container i18n>Import Dividends</ng-container>...</span
>
</span>
</button>
}
@ -443,20 +447,20 @@
<button mat-menu-item (click)="onClickActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="tablet-landscape-outline" />
<span i18n>View Holding</span>
<span><ng-container i18n>View Holding</ng-container>...</span>
</span>
</button>
}
<button mat-menu-item (click)="onUpdateActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<button mat-menu-item (click)="onCloneActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline" />
<span i18n>Clone</span>
<span><ng-container i18n>Clone</ng-container>...</span>
</span>
</button>
<button

46
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts

@ -22,7 +22,7 @@ export default {
type Story = StoryObj<GfPortfolioProportionChartComponent>;
export const Simple: Story = {
export const Default: Story = {
args: {
baseCurrency: 'USD',
data: {
@ -37,3 +37,47 @@ export const Simple: Story = {
locale: 'en-US'
}
};
export const InPercentage: Story = {
args: {
data: {
US: { name: 'United States', value: 0.6515000000000001 },
NL: { name: 'Netherlands', value: 0.006 },
DE: { name: 'Germany', value: 0.0031 },
GB: { name: 'United Kingdom', value: 0.0124 },
CA: { name: 'Canada', value: 0.0247 },
IE: { name: 'Ireland', value: 0.0112 },
SE: { name: 'Sweden', value: 0.0016 },
ES: { name: 'Spain', value: 0.0042 },
AU: { name: 'Australia', value: 0.0022 },
FR: { name: 'France', value: 0.0012 },
UY: { name: 'Uruguay', value: 0.0012 },
CH: { name: 'Switzerland', value: 0.004099999999999999 },
LU: { name: 'Luxembourg', value: 0.0012 },
BR: { name: 'Brazil', value: 0.0006 },
HK: { name: 'Hong Kong', value: 0.0006 },
IT: { name: 'Italy', value: 0.0005 },
CN: { name: 'China', value: 0.002 },
KR: { name: 'South Korea', value: 0.0006 },
BM: { name: 'Bermuda', value: 0.0011 },
ZA: { name: 'South Africa', value: 0.0004 },
SG: { name: 'Singapore', value: 0.0003 },
IL: { name: 'Israel', value: 0.001 },
DK: { name: 'Denmark', value: 0.0002 },
PE: { name: 'Peru', value: 0.0002 },
NO: { name: 'Norway', value: 0.0002 },
KY: { name: 'Cayman Islands', value: 0.0001 },
IN: { name: 'India', value: 0.0001 },
TW: { name: 'Taiwan', value: 0.0002 },
GR: { name: 'Greece', value: 0.0001 },
CL: { name: 'Chile', value: 0.0001 },
MX: { name: 'Mexico', value: 0 },
RU: { name: 'Russia', value: 0 },
IS: { name: 'Iceland', value: 0 },
JP: { name: 'Japan', value: 0 },
BE: { name: 'Belgium', value: 0 }
},
isInPercent: true,
keys: ['name']
}
};

422
package-lock.json

File diff suppressed because it is too large

11
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.219.0",
"version": "2.220.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -9,7 +9,6 @@
"affected:apps": "nx affected:apps",
"affected:build": "nx affected:build",
"affected:dep-graph": "nx affected:dep-graph",
"affected:e2e": "nx affected:e2e",
"affected:libs": "nx affected:libs",
"affected:lint": "nx affected:lint",
"affected:test": "nx affected:test",
@ -27,7 +26,6 @@
"database:setup": "npm run database:push && npm run database:seed",
"database:validate-schema": "prisma validate",
"dep-graph": "nx dep-graph",
"e2e": "ng e2e",
"extract-locales": "nx run client:extract-i18n --output-path ./apps/client/src/locales",
"format": "nx format:write",
"format:check": "nx format:check",
@ -69,7 +67,7 @@
"@angular/service-worker": "20.2.4",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0",
"@internationalized/number": "3.6.3",
"@internationalized/number": "3.6.5",
"@ionic/angular": "8.7.8",
"@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4",
@ -156,7 +154,6 @@
"@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.8",
"@nx/angular": "21.5.1",
"@nx/cypress": "21.5.1",
"@nx/eslint-plugin": "21.5.1",
"@nx/jest": "21.5.1",
"@nx/js": "21.5.1",
@ -180,10 +177,8 @@
"@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0",
"cypress": "6.2.1",
"eslint": "9.35.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-cypress": "4.2.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-storybook": "9.1.5",
"husky": "9.1.7",
@ -191,7 +186,7 @@
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.6.0",
"nx": "21.5.1",
"prettier": "3.6.2",
"prettier": "3.7.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.19.0",
"react": "18.2.0",

2
prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Provider" ADD VALUE 'OIDC';
Loading…
Cancel
Save