Browse Source

Merge branch 'main' into feature/upgrade-envalid-to-version-8.0.0

pull/4161/head
Thomas Kaul 8 months ago
committed by GitHub
parent
commit
6a94fefe4d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .env.example
  2. 13
      CHANGELOG.md
  3. 2
      DEVELOPMENT.md
  4. 10
      Dockerfile
  5. 24
      README.md
  6. 21
      apps/api/src/app/info/info.service.ts
  7. 15
      apps/api/src/app/logo/logo.controller.ts
  8. 19
      apps/api/src/app/logo/logo.service.ts
  9. 211
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  10. 29
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  11. 23
      apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts
  12. 21
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  13. 25
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  14. 20
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  15. 25
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  16. 12
      apps/api/src/services/data-provider/manual/manual.service.ts
  17. 8
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  18. 2
      apps/client/src/app/components/access-table/access-table.component.html
  19. 14
      apps/client/src/app/components/access-table/access-table.component.ts
  20. 7
      apps/client/src/assets/oss-friends.json
  21. 49
      docker/docker-compose.build.yml
  22. 17
      docker/docker-compose.dev.yml
  23. 20
      docker/docker-compose.yml
  24. 355
      package-lock.json
  25. 5
      package.json
  26. 14
      replace.build.mjs

4
.env.example

@ -1,7 +1,7 @@
COMPOSE_PROJECT_NAME=ghostfolio COMPOSE_PROJECT_NAME=ghostfolio
# CACHE # CACHE
REDIS_HOST=localhost REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD=<INSERT_REDIS_PASSWORD> REDIS_PASSWORD=<INSERT_REDIS_PASSWORD>
@ -12,5 +12,5 @@ POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
# VARIOUS # VARIOUS
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING> ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING> JWT_SECRET_KEY=<INSERT_RANDOM_STRING>

13
CHANGELOG.md

@ -9,8 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the usability of the _Copy link to clipboard_ action by adding a confirmation on success in the access table to share the portfolio
- Improved the endpoint to fetch the logo of an asset or a platform by sending the original MIME type
- Eliminated `got` in favor of using `fetch`
- Changed the `REDIS_HOST` from `localhost` to `redis` in `.env.example`
- Changed the _Postgres_ host from `localhost` to `postgres` in `.env.example`
- Changed the _Postgres_ image from `postgres:15` to `postgres:15-alpine` in the `docker-compose` files
- Introduced `extends` in the `docker-compose` files
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Refreshed the cryptocurrencies list
- Upgraded `envalid` from version `7.3.1` to `8.0.0` - Upgraded `envalid` from version `7.3.1` to `8.0.0`
- Upgraded `replace-in-file` from version `7.0.1` to `8.3.0`
### Fixed
- Improved the handling of a missing url in the endpoint to fetch the logo of an asset or a platform
## 2.132.0 - 2024-12-30 ## 2.132.0 - 2024-12-30

2
DEVELOPMENT.md

@ -12,7 +12,7 @@
### Setup ### Setup
1. Run `npm install` 1. Run `npm install`
1. Run `docker compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io) 1. Run `docker compose -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `npm run database:setup` to initialize the database schema 1. Run `npm run database:setup` to initialize the database schema
1. Start the [server](#start-server) and the [client](#start-client) 1. Start the [server](#start-server) and the [client](#start-client)
1. Open https://localhost:4200/en in your browser 1. Open https://localhost:4200/en in your browser

10
Dockerfile

@ -25,13 +25,13 @@ RUN npm install
COPY ./decorate-angular-cli.js decorate-angular-cli.js COPY ./decorate-angular-cli.js decorate-angular-cli.js
RUN node decorate-angular-cli.js RUN node decorate-angular-cli.js
COPY ./nx.json nx.json COPY ./apps apps
COPY ./replace.build.js replace.build.js COPY ./libs libs
COPY ./jest.preset.js jest.preset.js
COPY ./jest.config.ts jest.config.ts COPY ./jest.config.ts jest.config.ts
COPY ./jest.preset.js jest.preset.js
COPY ./nx.json nx.json
COPY ./replace.build.mjs replace.build.mjs
COPY ./tsconfig.base.json tsconfig.base.json COPY ./tsconfig.base.json tsconfig.base.json
COPY ./libs libs
COPY ./apps apps
RUN npm run build:production RUN npm run build:production

24
README.md

@ -118,7 +118,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio): Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio):
```bash ```bash
docker compose --env-file ./.env -f docker/docker-compose.yml up -d docker compose -f docker/docker-compose.yml up -d
``` ```
#### b. Build and run environment #### b. Build and run environment
@ -126,8 +126,8 @@ docker compose --env-file ./.env -f docker/docker-compose.yml up -d
Run the following commands to build and start the Docker images: Run the following commands to build and start the Docker images:
```bash ```bash
docker compose --env-file ./.env -f docker/docker-compose.build.yml build docker compose -f docker/docker-compose.build.yml build
docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d docker compose -f docker/docker-compose.build.yml up -d
``` ```
#### Setup #### Setup
@ -137,9 +137,19 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
#### Upgrade Version #### Upgrade Version
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml` 1. Update the _Ghostfolio_ Docker image
1. Run the following command to start the new Docker image: `docker compose --env-file ./.env -f docker/docker-compose.yml up -d`
At each start, the container will automatically apply the database schema migrations if needed. - Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
- Run the following command if `ghostfolio:latest` is set:
```bash
docker compose -f docker/docker-compose.yml pull
```
1. Run the following command to start the new Docker image:
```bash
docker compose -f docker/docker-compose.yml up -d
```
The container will automatically apply any required database schema migrations during startup.
### Home Server Systems (Community) ### Home Server Systems (Community)
@ -296,6 +306,6 @@ If you like to support this project, get [**Ghostfolio Premium**](https://ghostf
## License ## License
© 2021 - 2024 [Ghostfolio](https://ghostfol.io) © 2021 - 2025 [Ghostfolio](https://ghostfol.io)
Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html). Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html).

21
apps/api/src/app/info/info.service.ts

@ -33,7 +33,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { format, subDays } from 'date-fns'; import { format, subDays } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class InfoService { export class InfoService {
@ -155,16 +154,15 @@ export class InfoService {
private async countDockerHubPulls(): Promise<number> { private async countDockerHubPulls(): Promise<number> {
try { try {
const { pull_count } = await got( const { pull_count } = (await fetch(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`, `https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
{ {
headers: { 'User-Agent': 'request' }, headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<any>(); ).then((res) => res.json())) as { pull_count: number };
return pull_count; return pull_count;
} catch (error) { } catch (error) {
@ -176,12 +174,11 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> { private async countGitHubContributors(): Promise<number> {
try { try {
const { body } = await got('https://github.com/ghostfolio/ghostfolio', { const body = await fetch('https://github.com/ghostfolio/ghostfolio', {
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
}); }).then((res) => res.text());
const $ = cheerio.load(body); const $ = cheerio.load(body);
@ -199,16 +196,15 @@ export class InfoService {
private async countGitHubStargazers(): Promise<number> { private async countGitHubStargazers(): Promise<number> {
try { try {
const { stargazers_count } = await got( const { stargazers_count } = (await fetch(
`https://api.github.com/repos/ghostfolio/ghostfolio`, `https://api.github.com/repos/ghostfolio/ghostfolio`,
{ {
headers: { 'User-Agent': 'request' }, headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<any>(); ).then((res) => res.json())) as { stargazers_count: number };
return stargazers_count; return stargazers_count;
} catch (error) { } catch (error) {
@ -323,7 +319,7 @@ export class InfoService {
PROPERTY_BETTER_UPTIME_MONITOR_ID PROPERTY_BETTER_UPTIME_MONITOR_ID
)) as string; )) as string;
const { data } = await got( const { data } = await fetch(
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( `https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90), subDays(new Date(), 90),
DATE_FORMAT DATE_FORMAT
@ -334,12 +330,11 @@ export class InfoService {
'API_KEY_BETTER_UPTIME' 'API_KEY_BETTER_UPTIME'
)}` )}`
}, },
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<any>(); ).then((res) => res.json());
return data.attributes.availability / 100; return data.attributes.availability / 100;
} catch (error) { } catch (error) {

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

@ -26,12 +26,13 @@ export class LogoController {
@Res() response: Response @Res() response: Response
) { ) {
try { try {
const buffer = await this.logoService.getLogoByDataSourceAndSymbol({ const { buffer, type } =
dataSource, await this.logoService.getLogoByDataSourceAndSymbol({
symbol dataSource,
}); symbol
});
response.contentType('image/png'); response.contentType(type);
response.send(buffer); response.send(buffer);
} catch { } catch {
response.status(HttpStatus.NOT_FOUND).send(); response.status(HttpStatus.NOT_FOUND).send();
@ -44,9 +45,9 @@ export class LogoController {
@Res() response: Response @Res() response: Response
) { ) {
try { try {
const buffer = await this.logoService.getLogoByUrl(url); const { buffer, type } = await this.logoService.getLogoByUrl(url);
response.contentType('image/png'); response.contentType(type);
response.send(buffer); response.send(buffer);
} catch { } catch {
response.status(HttpStatus.NOT_FOUND).send(); response.status(HttpStatus.NOT_FOUND).send();

19
apps/api/src/app/logo/logo.service.ts

@ -4,7 +4,6 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import got from 'got';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable() @Injectable()
@ -29,7 +28,7 @@ export class LogoService {
{ dataSource, symbol } { dataSource, symbol }
]); ]);
if (!assetProfile) { if (!assetProfile?.url) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND), getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND StatusCodes.NOT_FOUND
@ -39,20 +38,26 @@ export class LogoService {
return this.getBuffer(assetProfile.url); return this.getBuffer(assetProfile.url);
} }
public async getLogoByUrl(aUrl: string) { public getLogoByUrl(aUrl: string) {
return this.getBuffer(aUrl); return this.getBuffer(aUrl);
} }
private getBuffer(aUrl: string) { private async getBuffer(aUrl: string) {
return got( const blob = await fetch(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`, `https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
{ {
headers: { 'User-Agent': 'request' }, headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).buffer(); ).then((res) => res.blob());
return {
buffer: await blob.arrayBuffer().then((arrayBuffer) => {
return Buffer.from(arrayBuffer);
}),
type: blob.type
};
} }
} }

211
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

File diff suppressed because it is too large

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

@ -26,12 +26,11 @@ import {
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
import { format, fromUnixTime, getUnixTime } from 'date-fns'; import { format, fromUnixTime, getUnixTime } from 'date-fns';
import got, { Headers } from 'got';
@Injectable() @Injectable()
export class CoinGeckoService implements DataProviderInterface { export class CoinGeckoService implements DataProviderInterface {
private readonly apiUrl: string; private readonly apiUrl: string;
private readonly headers: Headers = {}; private readonly headers: HeadersInit = {};
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService
@ -69,19 +68,18 @@ export class CoinGeckoService implements DataProviderInterface {
}; };
try { try {
const { name } = await got(`${this.apiUrl}/coins/${symbol}`, { const { name } = await fetch(`${this.apiUrl}/coins/${symbol}`, {
headers: this.headers, headers: this.headers,
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
}).json<any>(); }).then((res) => res.json());
response.name = name; response.name = name;
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -114,7 +112,7 @@ export class CoinGeckoService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const { prices } = await got( const { prices } = await fetch(
`${ `${
this.apiUrl this.apiUrl
}/coins/${symbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime( }/coins/${symbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime(
@ -122,10 +120,9 @@ export class CoinGeckoService implements DataProviderInterface {
)}&to=${getUnixTime(to)}`, )}&to=${getUnixTime(to)}`,
{ {
headers: this.headers, headers: this.headers,
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@ -169,16 +166,15 @@ export class CoinGeckoService implements DataProviderInterface {
} }
try { try {
const quotes = await got( const quotes = await fetch(
`${this.apiUrl}/simple/price?ids=${symbols.join( `${this.apiUrl}/simple/price?ids=${symbols.join(
',' ','
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`, )}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
{ {
headers: this.headers, headers: this.headers,
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
for (const symbol in quotes) { for (const symbol in quotes) {
response[symbol] = { response[symbol] = {
@ -192,7 +188,7 @@ export class CoinGeckoService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -212,13 +208,12 @@ export class CoinGeckoService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const { coins } = await got(`${this.apiUrl}/search?query=${query}`, { const { coins } = await fetch(`${this.apiUrl}/search?query=${query}`, {
headers: this.headers, headers: this.headers,
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
}).json<any>(); }).then((res) => res.json());
items = coins.map(({ id: symbol, name }) => { items = coins.map(({ id: symbol, name }) => {
return { return {
@ -234,7 +229,7 @@ export class CoinGeckoService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

23
apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts

@ -4,7 +4,6 @@ import { parseSymbol } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import got, { Headers } from 'got';
@Injectable() @Injectable()
export class OpenFigiDataEnhancerService implements DataEnhancerInterface { export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
@ -32,7 +31,7 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
const headers: Headers = {}; const headers: HeadersInit = {};
const { exchange, ticker } = parseSymbol({ const { exchange, ticker } = parseSymbol({
symbol, symbol,
dataSource: response.dataSource dataSource: response.dataSource
@ -43,14 +42,20 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
this.configurationService.get('API_KEY_OPEN_FIGI'); this.configurationService.get('API_KEY_OPEN_FIGI');
} }
const mappings = await got const mappings = (await fetch(
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, { `${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`,
headers, {
json: [{ exchCode: exchange, idType: 'TICKER', idValue: ticker }], body: JSON.stringify([
// @ts-ignore { exchCode: exchange, idType: 'TICKER', idValue: ticker }
]),
headers: {
'Content-Type': 'application/json',
...headers
},
method: 'POST',
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
}) }
.json<any[]>(); ).then((res) => res.json())) as any[];
if (mappings?.length === 1 && mappings[0].data?.length === 1) { if (mappings?.length === 1 && mappings[0].data?.length === 1) {
const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0]; const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0];

21
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -7,7 +7,6 @@ import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import { countries } from 'countries-list'; import { countries } from 'countries-list';
import got from 'got';
@Injectable() @Injectable()
export class TrackinsightDataEnhancerService implements DataEnhancerInterface { export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
@ -45,27 +44,25 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
const profile = await got( const profile = await fetch(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`, `${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
{ {
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
) )
.json<any>() .then((res) => res.json())
.catch(() => { .catch(() => {
return got( return fetch(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${ `${TrackinsightDataEnhancerService.baseUrl}/funds/${
symbol.split('.')?.[0] symbol.split('.')?.[0]
}.json`, }.json`,
{ {
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
) )
.json<any>() .then((res) => res.json())
.catch(() => { .catch(() => {
return {}; return {};
}); });
@ -77,29 +74,27 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response.isin = isin; response.isin = isin;
} }
const holdings = await got( const holdings = await fetch(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`, `${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`,
{ {
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
) )
.json<any>() .then((res) => res.json())
.catch(() => { .catch(() => {
return got( return fetch(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${ `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
symbol.split('.')?.[0] symbol.split('.')?.[0]
}.json`, }.json`,
{ {
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
) )
.json<any>() .then((res) => res.json())
.catch(() => { .catch(() => {
return {}; return {};
}); });

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

@ -31,7 +31,6 @@ import {
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
import { addDays, format, isSameDay, isToday } from 'date-fns'; import { addDays, format, isSameDay, isToday } from 'date-fns';
import got from 'got';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
@Injectable() @Injectable()
@ -95,7 +94,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
[date: string]: IDataProviderHistoricalResponse; [date: string]: IDataProviderHistoricalResponse;
} = {}; } = {};
const historicalResult = await got( const historicalResult = await fetch(
`${this.URL}/div/${symbol}?api_token=${ `${this.URL}/div/${symbol}?api_token=${
this.apiKey this.apiKey
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format( }&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
@ -103,10 +102,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
DATE_FORMAT DATE_FORMAT
)}`, )}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
for (const { date, value } of historicalResult) { for (const { date, value } of historicalResult) {
response[date] = { response[date] = {
@ -140,7 +138,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol = this.convertToEodSymbol(symbol); symbol = this.convertToEodSymbol(symbol);
try { try {
const response = await got( const response = await fetch(
`${this.URL}/eod/${symbol}?api_token=${ `${this.URL}/eod/${symbol}?api_token=${
this.apiKey this.apiKey
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format( }&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
@ -148,10 +146,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
DATE_FORMAT DATE_FORMAT
)}&period=${granularity}`, )}&period=${granularity}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
return response.reduce( return response.reduce(
(result, { adjusted_close, date }) => { (result, { adjusted_close, date }) => {
@ -205,15 +202,14 @@ export class EodHistoricalDataService implements DataProviderInterface {
}); });
try { try {
const realTimeResponse = await got( const realTimeResponse = await fetch(
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${ `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${
this.apiKey this.apiKey
}&fmt=json&s=${eodHistoricalDataSymbols.join(',')}`, }&fmt=json&s=${eodHistoricalDataSymbols.join(',')}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
const quotes: { const quotes: {
close: number; close: number;
@ -286,7 +282,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -400,15 +396,14 @@ export class EodHistoricalDataService implements DataProviderInterface {
})[] = []; })[] = [];
try { try {
const response = await got( const response = await fetch(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`, `${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<any>(); ).then((res) => res.json());
searchResult = response.map( searchResult = response.map(
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => { ({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {
@ -431,7 +426,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to search for ${aQuery} was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to search for ${aQuery} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

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

@ -21,7 +21,6 @@ import {
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format, isAfter, isBefore, isSameDay } from 'date-fns'; import { format, isAfter, isBefore, isSameDay } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class FinancialModelingPrepService implements DataProviderInterface { export class FinancialModelingPrepService implements DataProviderInterface {
@ -72,13 +71,12 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const { historical } = await got( const { historical } = await fetch(
`${this.URL}/historical-price-full/${symbol}?apikey=${this.apiKey}`, `${this.URL}/historical-price-full/${symbol}?apikey=${this.apiKey}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@ -124,13 +122,12 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} }
try { try {
const quotes = await got( const quotes = await fetch(
`${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`, `${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<any>(); ).then((res) => res.json());
for (const { price, symbol } of quotes) { for (const { price, symbol } of quotes) {
response[symbol] = { response[symbol] = {
@ -144,7 +141,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -164,15 +161,14 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const result = await got( const result = await fetch(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`, `${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
{ {
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<any>(); ).then((res) => res.json());
items = result.map(({ currency, name, symbol }) => { items = result.map(({ currency, name, symbol }) => {
return { return {
@ -187,7 +183,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

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

@ -28,7 +28,6 @@ import {
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import got from 'got';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
@Injectable() @Injectable()
@ -86,17 +85,16 @@ export class GhostfolioService implements DataProviderInterface {
} = {}; } = {};
try { try {
const { dividends } = await got( const { dividends } = (await fetch(
`${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format( `${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format(
to, to,
DATE_FORMAT DATE_FORMAT
)}`, )}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<DividendsResponse>(); ).then((res) => res.json())) as DividendsResponse;
response = dividends; response = dividends;
} catch (error) { } catch (error) {
@ -130,17 +128,16 @@ export class GhostfolioService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const { historicalData } = await got( const { historicalData } = (await fetch(
`${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format( `${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format(
to, to,
DATE_FORMAT DATE_FORMAT
)}`, )}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<HistoricalResponse>(); ).then((res) => res.json())) as HistoricalResponse;
return { return {
[symbol]: historicalData [symbol]: historicalData
@ -192,20 +189,19 @@ export class GhostfolioService implements DataProviderInterface {
} }
try { try {
const { quotes } = await got( const { quotes } = (await fetch(
`${this.URL}/v2/data-providers/ghostfolio/quotes?symbols=${symbols.join(',')}`, `${this.URL}/v2/data-providers/ghostfolio/quotes?symbols=${symbols.join(',')}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
// @ts-ignore
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).json<QuotesResponse>(); ).then((res) => res.json())) as QuotesResponse;
response = quotes; response = quotes;
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error.code === 'ABORT_ERR') { if (error.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -235,20 +231,19 @@ export class GhostfolioService implements DataProviderInterface {
let searchResult: LookupResponse = { items: [] }; let searchResult: LookupResponse = { items: [] };
try { try {
searchResult = await got( searchResult = (await fetch(
`${this.URL}/v2/data-providers/ghostfolio/lookup?query=${query}`, `${this.URL}/v2/data-providers/ghostfolio/lookup?query=${query}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<LookupResponse>(); ).then((res) => res.json())) as LookupResponse;
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error.code === 'ABORT_ERR') { if (error.name === 'AbortError') {
message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

12
apps/api/src/services/data-provider/manual/manual.service.ts

@ -27,7 +27,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { addDays, format, isBefore } from 'date-fns'; import { addDays, format, isBefore } from 'date-fns';
import got, { Headers } from 'got';
import * as jsonpath from 'jsonpath'; import * as jsonpath from 'jsonpath';
@Injectable() @Injectable()
@ -276,23 +275,22 @@ export class ManualService implements DataProviderInterface {
): Promise<number> { ): Promise<number> {
try { try {
let locale = scraperConfiguration.locale; let locale = scraperConfiguration.locale;
const { body, headers } = await got(scraperConfiguration.url, { const response = await fetch(scraperConfiguration.url, {
headers: scraperConfiguration.headers as Headers, headers: scraperConfiguration.headers as HeadersInit,
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
}); });
if (headers['content-type'].includes('application/json')) { if (response.headers['content-type'].includes('application/json')) {
const data = JSON.parse(body); const data = await response.json();
const value = String( const value = String(
jsonpath.query(data, scraperConfiguration.selector)[0] jsonpath.query(data, scraperConfiguration.selector)[0]
); );
return extractNumberFromString({ locale, value }); return extractNumberFromString({ locale, value });
} else { } else {
const $ = cheerio.load(body); const $ = cheerio.load(await response.text());
if (!locale) { if (!locale) {
try { try {

8
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -20,7 +20,6 @@ import {
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class RapidApiService implements DataProviderInterface { export class RapidApiService implements DataProviderInterface {
@ -135,7 +134,7 @@ export class RapidApiService implements DataProviderInterface {
oneYearAgo: { value: number; valueText: string }; oneYearAgo: { value: number; valueText: string };
}> { }> {
try { try {
const { fgi } = await got( const { fgi } = await fetch(
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`, `https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
{ {
headers: { headers: {
@ -143,18 +142,17 @@ export class RapidApiService implements DataProviderInterface {
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com', 'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
'x-rapidapi-key': this.configurationService.get('API_KEY_RAPID_API') 'x-rapidapi-key': this.configurationService.get('API_KEY_RAPID_API')
}, },
// @ts-ignore
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).json<any>(); ).then((res) => res.json());
return fgi; return fgi;
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.name === 'AbortError') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${( message = `RequestError: The operation was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

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

@ -66,7 +66,7 @@
</button> </button>
<mat-menu #transactionMenu="matMenu" xPosition="before"> <mat-menu #transactionMenu="matMenu" xPosition="before">
@if (element.type === 'PUBLIC') { @if (element.type === 'PUBLIC') {
<button mat-menu-item (click)="onCopyToClipboard(element.id)"> <button mat-menu-item (click)="onCopyUrlToClipboard(element.id)">
<ng-container i18n>Copy link to clipboard</ng-container> <ng-container i18n>Copy link to clipboard</ng-container>
</button> </button>
<hr class="my-0" /> <hr class="my-0" />

14
apps/client/src/app/components/access-table/access-table.component.ts

@ -12,6 +12,7 @@ import {
OnChanges, OnChanges,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
@Component({ @Component({
@ -34,7 +35,8 @@ export class AccessTableComponent implements OnChanges {
public constructor( public constructor(
private clipboard: Clipboard, private clipboard: Clipboard,
private notificationService: NotificationService private notificationService: NotificationService,
private snackBar: MatSnackBar
) {} ) {}
public ngOnChanges() { public ngOnChanges() {
@ -55,8 +57,16 @@ export class AccessTableComponent implements OnChanges {
return `${this.baseUrl}/${languageCode}/p/${aId}`; return `${this.baseUrl}/${languageCode}/p/${aId}`;
} }
public onCopyToClipboard(aId: string): void { public onCopyUrlToClipboard(aId: string): void {
this.clipboard.copy(this.getPublicUrl(aId)); this.clipboard.copy(this.getPublicUrl(aId));
this.snackBar.open(
'✅ ' + $localize`Link has been copied to the clipboard`,
undefined,
{
duration: 3000
}
);
} }
public onDeleteAccess(aId: string) { public onDeleteAccess(aId: string) {

7
apps/client/src/assets/oss-friends.json

@ -1,6 +1,11 @@
{ {
"createdAt": "2024-11-27T00:00:00.000Z", "createdAt": "2024-12-30T00:00:00.000Z",
"data": [ "data": [
{
"name": "Activepieces",
"description": "Activepieces is an open source, no-code, AI-first business automation tool. Alternative to Zapier, Make and Workato.",
"href": "https://activepieces.com"
},
{ {
"name": "Aptabase", "name": "Aptabase",
"description": "Analytics for Apps, open source, simple and privacy-friendly. SDKs for Swift, React Native, Electron, Flutter and many others.", "description": "Analytics for Apps, open source, simple and privacy-friendly. SDKs for Swift, React Native, Electron, Flutter and many others.",

49
docker/docker-compose.build.yml

@ -2,51 +2,22 @@ name: ghostfolio_build
services: services:
ghostfolio: ghostfolio:
build: ../ build: ../
container_name: ghostfolio-build image: ghostfolio/ghostfolio:local
init: true extends:
env_file: file: docker-compose.yml
- ../.env service: ghostfolio
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:
- 3333:3333
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost:3333/api/v1/health']
interval: 10s
timeout: 5s
retries: 5
postgres: postgres:
image: docker.io/library/postgres:15
container_name: gf-postgres-build container_name: gf-postgres-build
env_file: extends:
- ../.env file: docker-compose.yml
healthcheck: service: postgres
test: ['CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
interval: 10s
timeout: 5s
retries: 5
volumes:
- postgres:/var/lib/postgresql/data
redis: redis:
image: docker.io/library/redis:alpine
container_name: gf-redis-build container_name: gf-redis-build
env_file: extends:
- ../.env file: docker-compose.yml
command: ['redis-server', '--requirepass', $REDIS_PASSWORD] service: redis
healthcheck:
test: ['CMD-SHELL', 'redis-cli --pass "$REDIS_PASSWORD" ping | grep PONG']
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
postgres: postgres:

17
docker/docker-compose.dev.yml

@ -1,23 +1,18 @@
name: ghostfolio_dev name: ghostfolio_dev
services: services:
postgres: postgres:
image: docker.io/library/postgres:15 extends:
file: docker-compose.yml
service: postgres
container_name: gf-postgres-dev container_name: gf-postgres-dev
restart: unless-stopped
env_file:
- ../.env
ports: ports:
- ${POSTGRES_PORT:-5432}:5432 - ${POSTGRES_PORT:-5432}:5432
volumes:
- postgres:/var/lib/postgresql/data
redis: redis:
image: docker.io/library/redis:alpine extends:
file: docker-compose.yml
service: redis
container_name: gf-redis-dev container_name: gf-redis-dev
restart: unless-stopped
env_file:
- ../.env
command: ['redis-server', '--requirepass', $REDIS_PASSWORD]
ports: ports:
- ${REDIS_PORT:-6379}:6379 - ${REDIS_PORT:-6379}:6379

20
docker/docker-compose.yml

@ -3,6 +3,7 @@ services:
ghostfolio: ghostfolio:
image: docker.io/ghostfolio/ghostfolio:latest image: docker.io/ghostfolio/ghostfolio:latest
container_name: ghostfolio container_name: ghostfolio
restart: unless-stopped
init: true init: true
cap_drop: cap_drop:
- ALL - ALL
@ -10,10 +11,6 @@ services:
- no-new-privileges:true - no-new-privileges:true
env_file: env_file:
- ../.env - ../.env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports: ports:
- 3333:3333 - 3333:3333
depends_on: depends_on:
@ -28,8 +25,9 @@ services:
retries: 5 retries: 5
postgres: postgres:
image: docker.io/library/postgres:15 image: docker.io/library/postgres:15-alpine
container_name: gf-postgres container_name: gf-postgres
restart: unless-stopped
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
@ -43,7 +41,8 @@ services:
env_file: env_file:
- ../.env - ../.env
healthcheck: healthcheck:
test: ['CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}'] test:
['CMD-SHELL', 'pg_isready -d "$${POSTGRES_DB}" -U $${POSTGRES_USER}']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@ -53,6 +52,7 @@ services:
redis: redis:
image: docker.io/library/redis:alpine image: docker.io/library/redis:alpine
container_name: gf-redis container_name: gf-redis
restart: unless-stopped
user: '999:1000' user: '999:1000'
cap_drop: cap_drop:
- ALL - ALL
@ -60,9 +60,13 @@ services:
- no-new-privileges:true - no-new-privileges:true
env_file: env_file:
- ../.env - ../.env
command: ['redis-server', '--requirepass', $REDIS_PASSWORD] command:
- /bin/sh
- -c
- redis-server --requirepass "$${REDIS_PASSWORD:?REDIS_PASSWORD variable is not set}"
healthcheck: healthcheck:
test: ['CMD-SHELL', 'redis-cli --pass "$REDIS_PASSWORD" ping | grep PONG'] test:
['CMD-SHELL', 'redis-cli --pass "$${REDIS_PASSWORD}" ping | grep PONG']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5

355
package-lock.json

@ -65,7 +65,6 @@
"date-fns": "3.6.0", "date-fns": "3.6.0",
"envalid": "8.0.0", "envalid": "8.0.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",
"got": "11.8.6",
"helmet": "7.0.0", "helmet": "7.0.0",
"http-status-codes": "2.3.0", "http-status-codes": "2.3.0",
"ionicons": "7.4.0", "ionicons": "7.4.0",
@ -153,7 +152,7 @@
"prisma": "6.1.0", "prisma": "6.1.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "7.0.1", "replace-in-file": "8.3.0",
"shx": "0.3.4", "shx": "0.3.4",
"storybook": "8.4.7", "storybook": "8.4.7",
"ts-jest": "29.1.0", "ts-jest": "29.1.0",
@ -9561,18 +9560,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sindresorhus/merge-streams": { "node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
@ -10643,18 +10630,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@ -11148,33 +11123,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/cacheable-request/node_modules/@types/node": {
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/cacheable-request/node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/@types/color": { "node_modules/@types/color": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz",
@ -11418,12 +11366,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/http-errors": { "node_modules/@types/http-errors": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@ -11558,30 +11500,6 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/keyv/node_modules/@types/node": {
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/keyv/node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.7", "version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
@ -11826,30 +11744,6 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/responselike/node_modules/@types/node": {
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/responselike/node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/@types/retry": { "node_modules/@types/retry": {
"version": "0.12.2", "version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
@ -14422,33 +14316,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"license": "MIT",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"license": "MIT",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cachedir": { "node_modules/cachedir": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
@ -15058,27 +14925,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"license": "MIT",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clone-response/node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/cluster-key-slot": { "node_modules/cluster-key-slot": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@ -16881,21 +16727,6 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dedent": { "node_modules/dedent": {
"version": "1.5.3", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
@ -16984,15 +16815,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": { "node_modules/define-data-property": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@ -17531,6 +17353,7 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"once": "^1.4.0" "once": "^1.4.0"
@ -19943,6 +19766,7 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"pump": "^3.0.0" "pump": "^3.0.0"
@ -20266,31 +20090,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -20786,6 +20585,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/http-deceiver": { "node_modules/http-deceiver": {
@ -20937,19 +20737,6 @@
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@ -23281,6 +23068,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-parse-even-better-errors": { "node_modules/json-parse-even-better-errors": {
@ -23521,6 +23309,7 @@
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
@ -24746,15 +24535,6 @@
"devOptional": true, "devOptional": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@ -25600,18 +25380,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": { "node_modules/min-indent": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -26361,18 +26129,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-bundled": { "node_modules/npm-bundled": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz",
@ -26888,6 +26644,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"devOptional": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
@ -27036,15 +26793,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -28662,6 +28410,7 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
@ -28759,18 +28508,6 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rambda": { "node_modules/rambda": {
"version": "9.4.1", "version": "9.4.1",
"resolved": "https://registry.npmjs.org/rambda/-/rambda-9.4.1.tgz", "resolved": "https://registry.npmjs.org/rambda/-/rambda-9.4.1.tgz",
@ -29254,54 +28991,71 @@
} }
}, },
"node_modules/replace-in-file": { "node_modules/replace-in-file": {
"version": "7.0.1", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-7.0.1.tgz", "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.3.0.tgz",
"integrity": "sha512-KbhgPq04eA+TxXuUxpgWIH9k/TjF+28ofon2PXP7vq6izAILhxOtksCVcLuuQLtyjouBaPdlH6RJYYcSPVxCOA==", "integrity": "sha512-4VhddQiMCPIuypiwHDTM+XHjZoVu9h7ngBbSCnwGRcwdHwxltjt/m//Ep3GDwqaOx1fDSrKFQ+n7uo4uVcEz9Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^5.3.0",
"glob": "^8.1.0", "glob": "^10.4.2",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"bin": { "bin": {
"replace-in-file": "bin/cli.js" "replace-in-file": "bin/cli.js"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=18"
}
},
"node_modules/replace-in-file/node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/replace-in-file/node_modules/glob": { "node_modules/replace-in-file/node_modules/glob": {
"version": "8.1.0", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "foreground-child": "^3.1.0",
"inflight": "^1.0.4", "jackspeak": "^3.1.2",
"inherits": "2", "minimatch": "^9.0.4",
"minimatch": "^5.0.1", "minipass": "^7.1.2",
"once": "^1.3.0" "package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
}, },
"engines": { "bin": {
"node": ">=12" "glob": "dist/esm/bin.mjs"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/replace-in-file/node_modules/minimatch": { "node_modules/replace-in-file/node_modules/minimatch": {
"version": "5.1.6", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/request-progress": { "node_modules/request-progress": {
@ -29368,12 +29122,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/resolve-cwd": { "node_modules/resolve-cwd": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
@ -29443,18 +29191,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor": { "node_modules/restore-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@ -34053,6 +33789,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/write-file-atomic": { "node_modules/write-file-atomic": {

5
package.json

@ -39,7 +39,7 @@
"postinstall": "prisma generate", "postinstall": "prisma generate",
"prepare": "husky", "prepare": "husky",
"prisma": "prisma", "prisma": "prisma",
"replace-placeholders-in-build": "node ./replace.build.js", "replace-placeholders-in-build": "node ./replace.build.mjs",
"start": "node dist/apps/api/main", "start": "node dist/apps/api/main",
"start:client": "nx run client:copy-assets && nx run client:serve --configuration=development-en --hmr -o", "start:client": "nx run client:copy-assets && nx run client:serve --configuration=development-en --hmr -o",
"start:production": "npm run database:migrate && npm run database:seed && node main", "start:production": "npm run database:migrate && npm run database:seed && node main",
@ -111,7 +111,6 @@
"date-fns": "3.6.0", "date-fns": "3.6.0",
"envalid": "8.0.0", "envalid": "8.0.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",
"got": "11.8.6",
"helmet": "7.0.0", "helmet": "7.0.0",
"http-status-codes": "2.3.0", "http-status-codes": "2.3.0",
"ionicons": "7.4.0", "ionicons": "7.4.0",
@ -199,7 +198,7 @@
"prisma": "6.1.0", "prisma": "6.1.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "7.0.1", "replace-in-file": "8.3.0",
"shx": "0.3.4", "shx": "0.3.4",
"storybook": "8.4.7", "storybook": "8.4.7",
"ts-jest": "29.1.0", "ts-jest": "29.1.0",

14
replace.build.js → replace.build.mjs

@ -1,9 +1,13 @@
const dotenv = require('dotenv'); import dotenv from 'dotenv';
const path = require('path'); import { dirname, resolve } from 'path';
const replace = require('replace-in-file'); import { replaceInFileSync } from 'replace-in-file';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config({ dotenv.config({
path: path.resolve(__dirname, '.env') path: resolve(__dirname, '.env')
}); });
const now = new Date(); const now = new Date();
@ -16,7 +20,7 @@ const buildTimestamp = `${formatWithTwoDigits(
)}:${formatWithTwoDigits(now.getMinutes())}`; )}:${formatWithTwoDigits(now.getMinutes())}`;
try { try {
const changedFiles = replace.sync({ const changedFiles = replaceInFileSync({
files: './dist/apps/client/main.*.js', files: './dist/apps/client/main.*.js',
from: /{BUILD_TIMESTAMP}/g, from: /{BUILD_TIMESTAMP}/g,
to: buildTimestamp, to: buildTimestamp,
Loading…
Cancel
Save