Compare commits
107 Commits
Author | SHA1 | Date | |
---|---|---|---|
24bcc15b6a | |||
ff121243e4 | |||
70e633b997 | |||
0780ee4adb | |||
09613f9324 | |||
8642b1a7af | |||
f96f861341 | |||
a201fc7a97 | |||
a97110348c | |||
a25d5b9dc0 | |||
6c2acf2aa6 | |||
519827045a | |||
79a7e12a9f | |||
bf20a5de82 | |||
0adefe14e1 | |||
f24561cc3d | |||
873fd53715 | |||
e5d8faf2dc | |||
65d3bd2802 | |||
ad60373813 | |||
b725e6e2ec | |||
88c420ca5e | |||
118e17f78c | |||
cc92592d86 | |||
46eb3254a9 | |||
2477491f18 | |||
5fc9fde129 | |||
00e50c6abe | |||
8131a7ad03 | |||
f5e6f7dcfe | |||
87501e094d | |||
d3bfdf78c3 | |||
fc4e6ae6db | |||
23e4d5454d | |||
fdcf5fd396 | |||
8a9ae9bb33 | |||
3fb7e746df | |||
137e8e090a | |||
6d941500cd | |||
9c4e22978d | |||
af65a99398 | |||
1a0cb561cd | |||
9f875adf0c | |||
6b0dadb895 | |||
98a9523eee | |||
dfb3365efb | |||
0e08d8830e | |||
69d85eadfd | |||
c009f8c12f | |||
60ef46accf | |||
b12ac1fe84 | |||
4355c96ab6 | |||
fb326fe0cc | |||
02cfebd98c | |||
dd2936d703 | |||
918d0b85d4 | |||
a061595101 | |||
dcd496ac50 | |||
6b9ec549da | |||
b5bd4df483 | |||
766a2d7c2f | |||
6dabf7516a | |||
5d49ff7a4a | |||
8998c18836 | |||
741a0e36d2 | |||
e31b4c64cb | |||
812ff5cbdc | |||
035b90a689 | |||
5ea3a187f4 | |||
5d9c38663d | |||
5616bc4956 | |||
9ad1c2177c | |||
15bf9f2f9c | |||
ebc5008569 | |||
37759ba03f | |||
8319b216bb | |||
782d131b0d | |||
72e75208df | |||
4b1c27c245 | |||
61f0da35bc | |||
80464c7846 | |||
74f4323903 | |||
127dbf9dcd | |||
66bdb374e8 | |||
4ad4fa2b30 | |||
1fd836194f | |||
2090db1199 | |||
053c7e591e | |||
9b5e350e3b | |||
378e57c3bc | |||
6765191a8c | |||
8438a45bcf | |||
30a64e7fc1 | |||
f2cb671c7f | |||
3f41e5c5de | |||
c1ad483f33 | |||
f3d961bc16 | |||
42b70ef568 | |||
77beaaba08 | |||
d9c07456cd | |||
0a53df4293 | |||
4416ba0c88 | |||
486de968a2 | |||
a5833566a8 | |||
261f5844dd | |||
2173c418a7 | |||
4efd5cefd8 |
@ -24,12 +24,18 @@
|
|||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx"],
|
"files": ["*.ts", "*.tsx"],
|
||||||
"extends": ["plugin:@nx/typescript"],
|
"extends": ["plugin:@nx/typescript"],
|
||||||
"rules": {}
|
"rules": {
|
||||||
|
"@typescript-eslint/no-extra-semi": "error",
|
||||||
|
"no-extra-semi": "off"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.js", "*.jsx"],
|
"files": ["*.js", "*.jsx"],
|
||||||
"extends": ["plugin:@nx/javascript"],
|
"extends": ["plugin:@nx/javascript"],
|
||||||
"rules": {}
|
"rules": {
|
||||||
|
"@typescript-eslint/no-extra-semi": "error",
|
||||||
|
"no-extra-semi": "off"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.ts"],
|
"files": ["*.ts"],
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@
|
|||||||
.env
|
.env
|
||||||
.env.prod
|
.env.prod
|
||||||
.nx/cache
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
/coverage
|
/coverage
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/.nx/cache
|
/.nx/cache
|
||||||
|
/.nx/workspace-data
|
||||||
/apps/client/src/polyfills.ts
|
/apps/client/src/polyfills.ts
|
||||||
/dist
|
/dist
|
||||||
/test/import
|
/test/import
|
||||||
|
192
CHANGELOG.md
192
CHANGELOG.md
@ -5,6 +5,198 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 2.90.0 - 2024-06-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a dialog for the benchmarks in the markets overview
|
||||||
|
- Extended the asset profile details dialog of the admin control for currencies
|
||||||
|
- Extended the content of the _Self-Hosting_ section by the mobile app question on the Frequently Asked Questions (FAQ) page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the indicator for active filters from experimental to general availability
|
||||||
|
- Improved the error handling in the biometric authentication registration
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Set up SSL for local development
|
||||||
|
- Upgraded the _Stripe_ dependencies
|
||||||
|
- Upgraded `marked` from version `9.1.6` to `13.0.0`
|
||||||
|
- Upgraded `ngx-device-detector` from version `5.0.1` to `8.0.0`
|
||||||
|
- Upgraded `ngx-markdown` from version `17.1.1` to `18.0.0`
|
||||||
|
- Upgraded `zone.js` from version `0.14.5` to `0.14.7`
|
||||||
|
|
||||||
|
## 2.89.0 - 2024-06-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the historical market data table with currencies preset by date and activities count in the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the date validation in the create, import and update activities endpoints
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
## 2.88.0 - 2024-06-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Set the image source label in `Dockerfile`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the style of the blog post list
|
||||||
|
- Migrated the `@ghostfolio/client` components to control flow
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `angular` from version `17.3.10` to `18.0.2`
|
||||||
|
- Upgraded `Nx` from version `19.0.5` to `19.2.2`
|
||||||
|
|
||||||
|
## 2.87.0 - 2024-06-08
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the portfolio summary
|
||||||
|
- Improved the allocations by ETF holding on the allocations page (experimental)
|
||||||
|
- Improved the error handling in the `HttpResponseInterceptor`
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `prisma` from version `5.14.0` to `5.15.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the _FIRE_ calculator
|
||||||
|
|
||||||
|
## 2.86.0 - 2024-06-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced the allocations by ETF holding on the allocations page (experimental)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `prettier` from version `3.2.5` to `3.3.1`
|
||||||
|
|
||||||
|
## 2.85.0 - 2024-06-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the ability to close a user account
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `ng-extract-i18n-merge` from version `2.10.0` to `2.12.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the default locale in the value component
|
||||||
|
|
||||||
|
## 2.84.0 - 2024-06-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the data provider information to the asset profile details dialog of the admin control
|
||||||
|
- Added the cascading on delete for various relations in the database schema
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the initial annual interest rate in the _FIRE_ calculator
|
||||||
|
- Fixed the state handling in the currency selector
|
||||||
|
- Fixed the deletion of an asset profile with symbol profile overrides in the asset profile details dialog of the admin control
|
||||||
|
|
||||||
|
## 2.83.0 - 2024-05-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `@nestjs/passport` from version `10.0.0` to `10.0.3`
|
||||||
|
- Upgraded `angular` from version `17.3.5` to `17.3.10`
|
||||||
|
- Upgraded `class-validator` from version `0.14.0` to `0.14.1`
|
||||||
|
- Upgraded `countup.js` from version `2.3.2` to `2.8.0`
|
||||||
|
- Upgraded `Nx` from version `19.0.2` to `19.0.5`
|
||||||
|
- Upgraded `passport` from version `0.6.0` to `0.7.0`
|
||||||
|
- Upgraded `passport-jwt` from version `4.0.0` to `4.0.1`
|
||||||
|
- Upgraded `prisma` from version `5.13.0` to `5.14.0`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.11.2` to `2.11.3`
|
||||||
|
|
||||||
|
## 2.82.0 - 2024-05-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the create or update activity dialog by preselecting the (only) account
|
||||||
|
- Improved the usability of the date range selector in the assistant
|
||||||
|
- Refactored the holding detail dialog to a standalone component
|
||||||
|
- Refreshed the cryptocurrencies list
|
||||||
|
- Refactored various pages to standalone components
|
||||||
|
- Upgraded `@internationalized/number` from version `3.5.0` to `3.5.2`
|
||||||
|
- Upgraded `body-parser` from version `1.20.1` to `1.20.2`
|
||||||
|
- Upgraded `zone.js` from version `0.14.4` to `0.14.5`
|
||||||
|
|
||||||
|
## 2.81.0 - 2024-05-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an indicator for active filters (experimental)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the delete all activities functionality on the portfolio activities page to work with the filters of the assistant
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Improved the language localization for Türkçe (`tr`)
|
||||||
|
- Upgraded `Nx` from version `18.3.3` to `19.0.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the position detail dialog close functionality
|
||||||
|
|
||||||
|
## 2.80.0 - 2024-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the absolute change column to the holdings table on the home page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Increased the spacing around the floating action buttons (FAB)
|
||||||
|
- Set the icon column of the activities table to stick at the beginning
|
||||||
|
- Set the icon column of the holdings table to stick at the beginning
|
||||||
|
- Increased the number of attempts of queue jobs from `10` to `12` (fail later)
|
||||||
|
- Upgraded `ionicons` from version `7.3.0` to `7.4.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the position detail dialog open functionality when searching for a holding in the assistant
|
||||||
|
|
||||||
|
## 2.79.0 - 2024-05-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the holdings table to the holdings tab of the home page
|
||||||
|
- Improved the performance labels (with and without currency effects) in the position detail dialog
|
||||||
|
- Optimized the calculations of the portfolio details endpoint
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the benchmarks in the markets overview
|
||||||
|
- Fixed an issue with the _Fear & Greed Index_ (market mood) in the markets overview
|
||||||
|
|
||||||
|
## 2.78.0 - 2024-05-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a form validation against the DTO in the create or update access dialog
|
||||||
|
- Added a form validation against the DTO in the asset profile details dialog of the admin control
|
||||||
|
- Added a form validation against the DTO in the platform management of the admin control panel
|
||||||
|
- Added a form validation against the DTO in the tag management of the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Set the performance column of the holdings table to stick at the end
|
||||||
|
- Skipped the caching in the portfolio calculator if there are active filters (experimental)
|
||||||
|
- Improved the `INACTIVE` user role
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the calculation of the portfolio summary caused by future liabilities
|
||||||
|
- Fixed a division by zero error in the dividend yield calculation (experimental)
|
||||||
|
|
||||||
## 2.77.1 - 2024-04-27
|
## 2.77.1 - 2024-04-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -51,6 +51,9 @@ RUN yarn database:generate-typings
|
|||||||
|
|
||||||
# Image to run, copy everything needed from builder
|
# Image to run, copy everything needed from builder
|
||||||
FROM node:18-slim
|
FROM node:18-slim
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/ghostfolio/ghostfolio"
|
||||||
|
|
||||||
RUN apt update && apt install -y \
|
RUN apt update && apt install -y \
|
||||||
curl \
|
curl \
|
||||||
openssl \
|
openssl \
|
||||||
|
42
README.md
42
README.md
@ -85,23 +85,23 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
|
|||||||
|
|
||||||
### Supported Environment Variables
|
### Supported Environment Variables
|
||||||
|
|
||||||
| Name | Default Value | Description |
|
| Name | Type | Default Value | Description |
|
||||||
| ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `ACCESS_TOKEN_SALT` | | A random string used as salt for access tokens |
|
| `ACCESS_TOKEN_SALT` | string | | A random string used as salt for access tokens |
|
||||||
| `API_KEY_COINGECKO_DEMO` | | The _CoinGecko_ Demo API key |
|
| `API_KEY_COINGECKO_DEMO` | string (`optional`) | | The _CoinGecko_ Demo API key |
|
||||||
| `API_KEY_COINGECKO_PRO` | | The _CoinGecko_ Pro API |
|
| `API_KEY_COINGECKO_PRO` | string (`optional`) | | The _CoinGecko_ Pro API |
|
||||||
| `DATABASE_URL` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
| `DATABASE_URL` | string | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
||||||
| `HOST` | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
| `HOST` | string (`optional`) | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
||||||
| `JWT_SECRET_KEY` | | A random string used for _JSON Web Tokens_ (JWT) |
|
| `JWT_SECRET_KEY` | string | | A random string used for _JSON Web Tokens_ (JWT) |
|
||||||
| `PORT` | `3333` | The port where the Ghostfolio application will run on |
|
| `PORT` | number (`optional`) | `3333` | The port where the Ghostfolio application will run on |
|
||||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
| `POSTGRES_DB` | string | | The name of the _PostgreSQL_ database |
|
||||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
| `POSTGRES_PASSWORD` | string | | The password of the _PostgreSQL_ database |
|
||||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
| `POSTGRES_USER` | string | | The user of the _PostgreSQL_ database |
|
||||||
| `REDIS_DB` | `0` | The database index of _Redis_ |
|
| `REDIS_DB` | number (`optional`) | `0` | The database index of _Redis_ |
|
||||||
| `REDIS_HOST` | | The host where _Redis_ is running |
|
| `REDIS_HOST` | string | | The host where _Redis_ is running |
|
||||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
| `REDIS_PASSWORD` | string | | The password of _Redis_ |
|
||||||
| `REDIS_PORT` | | The port where _Redis_ is running |
|
| `REDIS_PORT` | number | | The port where _Redis_ is running |
|
||||||
| `REQUEST_TIMEOUT` | `2000` | The timeout of network requests to data providers in milliseconds |
|
| `REQUEST_TIMEOUT` | number (`optional`) | `2000` | The timeout of network requests to data providers in milliseconds |
|
||||||
|
|
||||||
### Run with Docker Compose
|
### Run with Docker Compose
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
|||||||
|
|
||||||
### Home Server Systems (Community)
|
### Home Server Systems (Community)
|
||||||
|
|
||||||
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), Home Assistant, [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Home Assistant](https://github.com/lildude/ha-addon-ghostfolio), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ Ghostfolio is available for various home server systems, including [CasaOS](http
|
|||||||
1. Run `yarn database:setup` to initialize the database schema
|
1. Run `yarn database:setup` to initialize the database schema
|
||||||
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
|
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
|
||||||
1. Start the server and the client (see [_Development_](#Development))
|
1. Start the server and the client (see [_Development_](#Development))
|
||||||
1. Open http://localhost:4200/en in your browser
|
1. Open https://localhost:4200/en in your browser
|
||||||
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
||||||
|
|
||||||
### Start Server
|
### Start Server
|
||||||
@ -176,7 +176,7 @@ Run `yarn start:server`
|
|||||||
|
|
||||||
### Start Client
|
### Start Client
|
||||||
|
|
||||||
Run `yarn start:client` and open http://localhost:4200/en in your browser
|
Run `yarn start:client` and open https://localhost:4200/en in your browser
|
||||||
|
|
||||||
### Start _Storybook_
|
### Start _Storybook_
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ r
|
|||||||
|
|
||||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||||
|
|
||||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
|
Not sure what to work on? We have [some ideas](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), even for [newcomers](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
|
||||||
|
|
||||||
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
|
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
|
|||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
|
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
|
||||||
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
@ -19,13 +17,11 @@ import { AccountService } from './account.service';
|
|||||||
imports: [
|
imports: [
|
||||||
AccountBalanceModule,
|
AccountBalanceModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedactValuesInResponseModule
|
||||||
UserModule
|
|
||||||
],
|
],
|
||||||
providers: [AccountService]
|
providers: [AccountService]
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
@ -23,11 +25,13 @@ import { QueueModule } from './queue/queue.module';
|
|||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
|
OrderModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
QueueModule,
|
QueueModule,
|
||||||
SubscriptionModule,
|
SubscriptionModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [AdminService],
|
providers: [AdminService],
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
import { environment } from '@ghostfolio/api/environments/environment';
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
@ -13,11 +14,13 @@ import {
|
|||||||
PROPERTY_IS_READ_ONLY_MODE,
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
PROPERTY_IS_USER_SIGNUP_ENABLED
|
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
|
import { isCurrency, getCurrencyFromSymbol } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
AdminMarketDataItem,
|
AdminMarketDataItem,
|
||||||
|
EnhancedSymbolProfile,
|
||||||
Filter,
|
Filter,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -42,6 +45,7 @@ export class AdminService {
|
|||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
|
private readonly orderService: OrderService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly subscriptionService: SubscriptionService,
|
private readonly subscriptionService: SubscriptionService,
|
||||||
@ -295,6 +299,16 @@ export class AdminService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
}: UniqueAsset): Promise<AdminMarketDataDetails> {
|
}: UniqueAsset): Promise<AdminMarketDataDetails> {
|
||||||
|
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
||||||
|
let currency: EnhancedSymbolProfile['currency'] = '-';
|
||||||
|
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||||
|
|
||||||
|
if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
||||||
|
currency = getCurrencyFromSymbol(symbol);
|
||||||
|
({ activitiesCount, dateOfFirstActivity } =
|
||||||
|
await this.orderService.getStatisticsByCurrency(currency));
|
||||||
|
}
|
||||||
|
|
||||||
const [[assetProfile], marketData] = await Promise.all([
|
const [[assetProfile], marketData] = await Promise.all([
|
||||||
this.symbolProfileService.getSymbolProfiles([
|
this.symbolProfileService.getSymbolProfiles([
|
||||||
{
|
{
|
||||||
@ -313,11 +327,20 @@ export class AdminService {
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (assetProfile) {
|
||||||
|
assetProfile.dataProviderInfo = this.dataProviderService
|
||||||
|
.getDataProvider(assetProfile.dataSource)
|
||||||
|
.getDataProviderInfo();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketData,
|
marketData,
|
||||||
assetProfile: assetProfile ?? {
|
assetProfile: assetProfile ?? {
|
||||||
symbol,
|
activitiesCount,
|
||||||
currency: '-'
|
currency,
|
||||||
|
dataSource,
|
||||||
|
dateOfFirstActivity,
|
||||||
|
symbol
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -329,6 +352,7 @@ export class AdminService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
holdings,
|
||||||
name,
|
name,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
@ -349,6 +373,7 @@ export class AdminService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
holdings,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
symbol,
|
symbol,
|
||||||
@ -401,30 +426,45 @@ export class AdminService {
|
|||||||
by: ['dataSource', 'symbol']
|
by: ['dataSource', 'symbol']
|
||||||
});
|
});
|
||||||
|
|
||||||
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
|
const marketDataPromise: Promise<AdminMarketDataItem>[] =
|
||||||
.getCurrencyPairs()
|
this.exchangeRateDataService
|
||||||
.map(({ dataSource, symbol }) => {
|
.getCurrencyPairs()
|
||||||
const marketDataItemCount =
|
.map(async ({ dataSource, symbol }) => {
|
||||||
marketDataItems.find((marketDataItem) => {
|
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
||||||
return (
|
let currency: EnhancedSymbolProfile['currency'] = '-';
|
||||||
marketDataItem.dataSource === dataSource &&
|
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||||
marketDataItem.symbol === symbol
|
|
||||||
);
|
|
||||||
})?._count ?? 0;
|
|
||||||
|
|
||||||
return {
|
if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
||||||
dataSource,
|
currency = getCurrencyFromSymbol(symbol);
|
||||||
marketDataItemCount,
|
({ activitiesCount, dateOfFirstActivity } =
|
||||||
symbol,
|
await this.orderService.getStatisticsByCurrency(currency));
|
||||||
assetClass: AssetClass.LIQUIDITY,
|
}
|
||||||
countriesCount: 0,
|
|
||||||
currency: symbol.replace(DEFAULT_CURRENCY, ''),
|
|
||||||
id: undefined,
|
|
||||||
name: symbol,
|
|
||||||
sectorsCount: 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const marketDataItemCount =
|
||||||
|
marketDataItems.find((marketDataItem) => {
|
||||||
|
return (
|
||||||
|
marketDataItem.dataSource === dataSource &&
|
||||||
|
marketDataItem.symbol === symbol
|
||||||
|
);
|
||||||
|
})?._count ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activitiesCount,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
marketDataItemCount,
|
||||||
|
symbol,
|
||||||
|
assetClass: AssetClass.LIQUIDITY,
|
||||||
|
assetSubClass: AssetSubClass.CASH,
|
||||||
|
countriesCount: 0,
|
||||||
|
date: dateOfFirstActivity,
|
||||||
|
id: undefined,
|
||||||
|
name: symbol,
|
||||||
|
sectorsCount: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketData = await Promise.all(marketDataPromise);
|
||||||
return { marketData, count: marketData.length };
|
return { marketData, count: marketData.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import { AccessModule } from './access/access.module';
|
|||||||
import { AccountModule } from './account/account.module';
|
import { AccountModule } from './account/account.module';
|
||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
|
import { AssetModule } from './asset/asset.module';
|
||||||
import { AuthDeviceModule } from './auth-device/auth-device.module';
|
import { AuthDeviceModule } from './auth-device/auth-device.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
import { BenchmarkModule } from './benchmark/benchmark.module';
|
||||||
@ -51,6 +52,7 @@ import { UserModule } from './user/user.module';
|
|||||||
AdminModule,
|
AdminModule,
|
||||||
AccessModule,
|
AccessModule,
|
||||||
AccountModule,
|
AccountModule,
|
||||||
|
AssetModule,
|
||||||
AuthDeviceModule,
|
AuthDeviceModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
BenchmarkModule,
|
BenchmarkModule,
|
||||||
|
29
apps/api/src/app/asset/asset.controller.ts
Normal file
29
apps/api/src/app/asset/asset.controller.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
|
||||||
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
|
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
|
@Controller('asset')
|
||||||
|
export class AssetController {
|
||||||
|
public constructor(private readonly adminService: AdminService) {}
|
||||||
|
|
||||||
|
@Get(':dataSource/:symbol')
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getAsset(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<AdminMarketDataDetails> {
|
||||||
|
const { assetProfile, marketData } =
|
||||||
|
await this.adminService.getMarketDataBySymbol({ dataSource, symbol });
|
||||||
|
|
||||||
|
return {
|
||||||
|
marketData,
|
||||||
|
assetProfile: pick(assetProfile, ['dataSource', 'name', 'symbol'])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
17
apps/api/src/app/asset/asset.module.ts
Normal file
17
apps/api/src/app/asset/asset.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AssetController } from './asset.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AssetController],
|
||||||
|
imports: [
|
||||||
|
AdminModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AssetModule {}
|
@ -1,6 +1,5 @@
|
|||||||
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
|
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
|
||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -9,7 +8,6 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
@Module({
|
@Module({
|
||||||
controllers: [AuthDeviceController],
|
controllers: [AuthDeviceController],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '180 days' }
|
signOptions: { expiresIn: '180 days' }
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -6,10 +5,7 @@ import { AuthDevice, Prisma } from '@prisma/client';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthDeviceService {
|
export class AuthDeviceService {
|
||||||
public constructor(
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly prismaService: PrismaService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async authDevice(
|
public async authDevice(
|
||||||
where: Prisma.AuthDeviceWhereUniqueInput
|
where: Prisma.AuthDeviceWhereUniqueInput
|
||||||
|
@ -3,7 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider } from '@prisma/client';
|
||||||
import { Strategy } from 'passport-google-oauth20';
|
import { Profile, Strategy } from 'passport-google-oauth20';
|
||||||
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ import { AuthService } from './auth.service';
|
|||||||
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
callbackURL: `${configurationService.get(
|
callbackURL: `${configurationService.get(
|
||||||
@ -20,7 +20,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
|
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: configurationService.get('GOOGLE_SECRET'),
|
clientSecret: configurationService.get('GOOGLE_SECRET'),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
scope: ['email', 'profile']
|
scope: ['profile']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,20 +28,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
request: any,
|
request: any,
|
||||||
token: string,
|
token: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile,
|
profile: Profile,
|
||||||
done: Function,
|
done: Function,
|
||||||
done2: Function
|
done2: Function
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const jwt: string = await this.authService.validateOAuthLogin({
|
const jwt = await this.authService.validateOAuthLogin({
|
||||||
provider: Provider.GOOGLE,
|
provider: Provider.GOOGLE,
|
||||||
thirdPartyId: profile.id
|
thirdPartyId: profile.id
|
||||||
});
|
});
|
||||||
const user = {
|
|
||||||
jwt
|
|
||||||
};
|
|
||||||
|
|
||||||
done(null, user);
|
done(null, { jwt });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'GoogleStrategy');
|
Logger.error(error, 'GoogleStrategy');
|
||||||
done(error, false);
|
done(error, false);
|
||||||
|
@ -2,10 +2,12 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
|
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
|
||||||
|
import { hasRole } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import * as countriesAndTimezones from 'countries-and-timezones';
|
import * as countriesAndTimezones from 'countries-and-timezones';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -29,6 +31,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
|
if (hasRole(user, 'INACTIVE')) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
||||||
|
StatusCodes.TOO_MANY_REQUESTS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const country =
|
const country =
|
||||||
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
|
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
|
||||||
|
|
||||||
@ -45,10 +54,20 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
} else {
|
} else {
|
||||||
throw '';
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.getStatus() === StatusCodes.TOO_MANY_REQUESTS) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.UNAUTHORIZED),
|
||||||
|
StatusCodes.UNAUTHORIZED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
throw new UnauthorizedException('unauthorized', err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import type {
|
import type {
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
@ -105,7 +105,7 @@ export class BenchmarkController {
|
|||||||
@Get(':dataSource/:symbol/:startDateString')
|
@Get(':dataSource/:symbol/:startDateString')
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
public async getBenchmarkMarketDataBySymbol(
|
public async getBenchmarkMarketDataForUser(
|
||||||
@Param('dataSource') dataSource: DataSource,
|
@Param('dataSource') dataSource: DataSource,
|
||||||
@Param('startDateString') startDateString: string,
|
@Param('startDateString') startDateString: string,
|
||||||
@Param('symbol') symbol: string,
|
@Param('symbol') symbol: string,
|
||||||
@ -117,7 +117,7 @@ export class BenchmarkController {
|
|||||||
);
|
);
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
|
||||||
return this.benchmarkService.getMarketDataBySymbol({
|
return this.benchmarkService.getMarketDataForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
endDate,
|
endDate,
|
||||||
startDate,
|
startDate,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
@ -17,7 +18,6 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
controllers: [BenchmarkController],
|
controllers: [BenchmarkController],
|
||||||
exports: [BenchmarkService],
|
exports: [BenchmarkService],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
@ -25,7 +25,9 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
PropertyModule,
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [BenchmarkService]
|
providers: [BenchmarkService]
|
||||||
})
|
})
|
||||||
|
@ -153,6 +153,7 @@ export class BenchmarkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
dataSource: benchmarkAssetProfiles[index].dataSource,
|
||||||
marketCondition: this.getMarketCondition(
|
marketCondition: this.getMarketCondition(
|
||||||
performancePercentFromAllTimeHigh
|
performancePercentFromAllTimeHigh
|
||||||
),
|
),
|
||||||
@ -163,6 +164,7 @@ export class BenchmarkService {
|
|||||||
performancePercent: performancePercentFromAllTimeHigh
|
performancePercent: performancePercentFromAllTimeHigh
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
symbol: benchmarkAssetProfiles[index].symbol,
|
||||||
trend50d: benchmarkTrends[index].trend50d,
|
trend50d: benchmarkTrends[index].trend50d,
|
||||||
trend200d: benchmarkTrends[index].trend200d
|
trend200d: benchmarkTrends[index].trend200d
|
||||||
};
|
};
|
||||||
@ -213,7 +215,7 @@ export class BenchmarkService {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMarketDataBySymbol({
|
public async getMarketDataForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
endDate = new Date(),
|
endDate = new Date(),
|
||||||
startDate,
|
startDate,
|
||||||
|
16
apps/api/src/app/cache/cache.module.ts
vendored
16
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,10 +1,4 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -12,14 +6,6 @@ import { CacheController } from './cache.controller';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [CacheController],
|
controllers: [CacheController],
|
||||||
imports: [
|
imports: [RedisCacheModule]
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
|
||||||
DataProviderModule,
|
|
||||||
ExchangeRateDataModule,
|
|
||||||
PrismaModule,
|
|
||||||
RedisCacheModule,
|
|
||||||
SymbolProfileModule
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class CacheModule {}
|
export class CacheModule {}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -12,15 +8,7 @@ import { ExportController } from './export.controller';
|
|||||||
import { ExportService } from './export.service';
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [AccountModule, ApiModule, OrderModule],
|
||||||
AccountModule,
|
|
||||||
ApiModule,
|
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
|
||||||
DataProviderModule,
|
|
||||||
OrderModule,
|
|
||||||
RedisCacheModule
|
|
||||||
],
|
|
||||||
controllers: [ExportController],
|
controllers: [ExportController],
|
||||||
providers: [ExportService]
|
providers: [ExportService]
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
|
||||||
@ -9,7 +9,11 @@ import { HealthService } from './health.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
imports: [ConfigurationModule, DataEnhancerModule, DataProviderModule],
|
imports: [
|
||||||
|
DataEnhancerModule,
|
||||||
|
DataProviderModule,
|
||||||
|
TransformDataSourceInRequestModule
|
||||||
|
],
|
||||||
providers: [HealthService]
|
providers: [HealthService]
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
export class HealthModule {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
@ -4,6 +4,8 @@ import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
|||||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -30,7 +32,9 @@ import { ImportService } from './import.service';
|
|||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [ImportService]
|
providers: [ImportService]
|
||||||
})
|
})
|
||||||
|
@ -13,10 +13,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
|
||||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
|
||||||
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
|
|
||||||
} from '@ghostfolio/common/config';
|
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getAssetProfileIdentifier,
|
getAssetProfileIdentifier,
|
||||||
@ -295,6 +292,7 @@ export class ImportService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
id,
|
id,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
@ -367,6 +365,7 @@ export class ImportService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
id,
|
id,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
@ -538,6 +537,7 @@ export class ImportService {
|
|||||||
assetSubClass: undefined,
|
assetSubClass: undefined,
|
||||||
countries: undefined,
|
countries: undefined,
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
|
holdings: undefined,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
sectors: undefined,
|
sectors: undefined,
|
||||||
updatedAt: undefined
|
updatedAt: undefined
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||||
|
@ -2,6 +2,7 @@ import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'
|
|||||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -34,6 +35,7 @@ import { InfoService } from './info.service';
|
|||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
TagModule,
|
TagModule,
|
||||||
|
TransformDataSourceInResponseModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [InfoService]
|
providers: [InfoService]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
@ -8,7 +9,11 @@ import { LogoService } from './logo.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [LogoController],
|
controllers: [LogoController],
|
||||||
imports: [ConfigurationModule, SymbolProfileModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule
|
||||||
|
],
|
||||||
providers: [LogoService]
|
providers: [LogoService]
|
||||||
})
|
})
|
||||||
export class LogoModule {}
|
export class LogoModule {}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AssetClass,
|
AssetClass,
|
||||||
AssetSubClass,
|
AssetSubClass,
|
||||||
@ -15,7 +17,8 @@ import {
|
|||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Min
|
Min,
|
||||||
|
Validate
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ export class CreateOrderDto {
|
|||||||
dataSource?: DataSource;
|
dataSource?: DataSource;
|
||||||
|
|
||||||
@IsISO8601()
|
@IsISO8601()
|
||||||
|
@Validate(IsAfter1970Constraint)
|
||||||
date: string;
|
date: string;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||||
HEADER_KEY_IMPERSONATION
|
HEADER_KEY_IMPERSONATION
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -53,8 +53,20 @@ export class OrderController {
|
|||||||
@Delete()
|
@Delete()
|
||||||
@HasPermission(permissions.deleteOrder)
|
@HasPermission(permissions.deleteOrder)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async deleteOrders(): Promise<number> {
|
public async deleteOrders(
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('tags') filterByTags?: string
|
||||||
|
): Promise<number> {
|
||||||
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
return this.orderService.deleteOrders({
|
return this.orderService.deleteOrders({
|
||||||
|
filters,
|
||||||
|
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -88,21 +100,26 @@ export class OrderController {
|
|||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange?: DateRange,
|
||||||
@Query('skip') skip?: number,
|
@Query('skip') skip?: number,
|
||||||
@Query('sortColumn') sortColumn?: string,
|
@Query('sortColumn') sortColumn?: string,
|
||||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('take') take?: number
|
@Query('take') take?: number
|
||||||
): Promise<Activities> {
|
): Promise<Activities> {
|
||||||
|
let endDate: Date;
|
||||||
|
let startDate: Date;
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
({ endDate, startDate } = getInterval(dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
|
||||||
|
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
@ -2,9 +2,10 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
@ -23,15 +24,16 @@ import { OrderService } from './order.service';
|
|||||||
imports: [
|
imports: [
|
||||||
ApiModule,
|
ApiModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
RedactValuesInResponseModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
UserModule
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [AccountBalanceService, AccountService, OrderService]
|
providers: [AccountBalanceService, AccountService, OrderService]
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,11 @@ import {
|
|||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
EnhancedSymbolProfile,
|
||||||
|
Filter,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -194,16 +198,36 @@ export class OrderService {
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteOrders(where: Prisma.OrderWhereInput): Promise<number> {
|
public async deleteOrders({
|
||||||
|
filters,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
|
userCurrency: string;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<number> {
|
||||||
|
const { activities } = await this.getOrders({
|
||||||
|
filters,
|
||||||
|
userId,
|
||||||
|
userCurrency,
|
||||||
|
includeDrafts: true,
|
||||||
|
withExcludedAccounts: true
|
||||||
|
});
|
||||||
|
|
||||||
const { count } = await this.prismaService.order.deleteMany({
|
const { count } = await this.prismaService.order.deleteMany({
|
||||||
where
|
where: {
|
||||||
|
id: {
|
||||||
|
in: activities.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.emit(
|
this.eventEmitter.emit(
|
||||||
PortfolioChangedEvent.getName(),
|
PortfolioChangedEvent.getName(),
|
||||||
new PortfolioChangedEvent({
|
new PortfolioChangedEvent({ userId })
|
||||||
userId: <string>where.userId
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
@ -409,6 +433,26 @@ export class OrderService {
|
|||||||
return { activities, count };
|
return { activities, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getStatisticsByCurrency(
|
||||||
|
currency: EnhancedSymbolProfile['currency']
|
||||||
|
): Promise<{
|
||||||
|
activitiesCount: EnhancedSymbolProfile['activitiesCount'];
|
||||||
|
dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||||
|
}> {
|
||||||
|
const { _count, _min } = await this.prismaService.order.aggregate({
|
||||||
|
_count: true,
|
||||||
|
_min: {
|
||||||
|
date: true
|
||||||
|
},
|
||||||
|
where: { SymbolProfile: { currency } }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
activitiesCount: _count as number,
|
||||||
|
dateOfFirstActivity: _min.date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async order(
|
public async order(
|
||||||
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
||||||
): Promise<Order | null> {
|
): Promise<Order | null> {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AssetClass,
|
AssetClass,
|
||||||
AssetSubClass,
|
AssetSubClass,
|
||||||
@ -14,7 +16,8 @@ import {
|
|||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Min
|
Min,
|
||||||
|
Validate
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
@ -49,6 +52,7 @@ export class UpdateOrderDto {
|
|||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
|
||||||
@IsISO8601()
|
@IsISO8601()
|
||||||
|
@Validate(IsAfter1970Constraint)
|
||||||
date: string;
|
date: string;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@ -20,6 +20,7 @@ export const symbolProfileDummyData = {
|
|||||||
assetSubClass: undefined,
|
assetSubClass: undefined,
|
||||||
countries: [],
|
countries: [],
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
|
holdings: [],
|
||||||
id: undefined,
|
id: undefined,
|
||||||
sectors: [],
|
sectors: [],
|
||||||
updatedAt: undefined
|
updatedAt: undefined
|
||||||
|
@ -32,6 +32,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
calculationType,
|
calculationType,
|
||||||
currency,
|
currency,
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
|
hasFilters,
|
||||||
isExperimentalFeatures = false,
|
isExperimentalFeatures = false,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
@ -40,9 +41,12 @@ export class PortfolioCalculatorFactory {
|
|||||||
calculationType: PerformanceCalculationType;
|
calculationType: PerformanceCalculationType;
|
||||||
currency: string;
|
currency: string;
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
|
hasFilters: boolean;
|
||||||
isExperimentalFeatures?: boolean;
|
isExperimentalFeatures?: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
}): PortfolioCalculator {
|
}): PortfolioCalculator {
|
||||||
|
const useCache = !hasFilters && isExperimentalFeatures;
|
||||||
|
|
||||||
switch (calculationType) {
|
switch (calculationType) {
|
||||||
case PerformanceCalculationType.MWR:
|
case PerformanceCalculationType.MWR:
|
||||||
return new MWRPortfolioCalculator({
|
return new MWRPortfolioCalculator({
|
||||||
@ -50,7 +54,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
dateRange,
|
dateRange,
|
||||||
isExperimentalFeatures,
|
useCache,
|
||||||
userId,
|
userId,
|
||||||
configurationService: this.configurationService,
|
configurationService: this.configurationService,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
@ -64,7 +68,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
currency,
|
currency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
dateRange,
|
dateRange,
|
||||||
isExperimentalFeatures,
|
useCache,
|
||||||
userId,
|
userId,
|
||||||
configurationService: this.configurationService,
|
configurationService: this.configurationService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService,
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
eachDayOfInterval,
|
eachDayOfInterval,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
format,
|
format,
|
||||||
|
isAfter,
|
||||||
isBefore,
|
isBefore,
|
||||||
isSameDay,
|
isSameDay,
|
||||||
max,
|
max,
|
||||||
@ -44,26 +45,26 @@ import {
|
|||||||
subDays
|
subDays
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { first, last, uniq, uniqBy } from 'lodash';
|
import { first, last, uniq, uniqBy } from 'lodash';
|
||||||
import ms from 'ms';
|
|
||||||
|
|
||||||
export abstract class PortfolioCalculator {
|
export abstract class PortfolioCalculator {
|
||||||
protected static readonly ENABLE_LOGGING = false;
|
protected static readonly ENABLE_LOGGING = false;
|
||||||
|
|
||||||
protected accountBalanceItems: HistoricalDataItem[];
|
protected accountBalanceItems: HistoricalDataItem[];
|
||||||
protected orders: PortfolioOrder[];
|
protected activities: PortfolioOrder[];
|
||||||
|
|
||||||
private configurationService: ConfigurationService;
|
private configurationService: ConfigurationService;
|
||||||
private currency: string;
|
private currency: string;
|
||||||
private currentRateService: CurrentRateService;
|
private currentRateService: CurrentRateService;
|
||||||
private dataProviderInfos: DataProviderInfo[];
|
private dataProviderInfos: DataProviderInfo[];
|
||||||
|
private dateRange: DateRange;
|
||||||
private endDate: Date;
|
private endDate: Date;
|
||||||
private exchangeRateDataService: ExchangeRateDataService;
|
private exchangeRateDataService: ExchangeRateDataService;
|
||||||
private isExperimentalFeatures: boolean;
|
|
||||||
private redisCacheService: RedisCacheService;
|
private redisCacheService: RedisCacheService;
|
||||||
private snapshot: PortfolioSnapshot;
|
private snapshot: PortfolioSnapshot;
|
||||||
private snapshotPromise: Promise<void>;
|
private snapshotPromise: Promise<void>;
|
||||||
private startDate: Date;
|
private startDate: Date;
|
||||||
private transactionPoints: TransactionPoint[];
|
private transactionPoints: TransactionPoint[];
|
||||||
|
private useCache: boolean;
|
||||||
private userId: string;
|
private userId: string;
|
||||||
|
|
||||||
public constructor({
|
public constructor({
|
||||||
@ -74,8 +75,8 @@ export abstract class PortfolioCalculator {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
dateRange,
|
dateRange,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
isExperimentalFeatures,
|
|
||||||
redisCacheService,
|
redisCacheService,
|
||||||
|
useCache,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
accountBalanceItems: HistoricalDataItem[];
|
accountBalanceItems: HistoricalDataItem[];
|
||||||
@ -85,18 +86,18 @@ export abstract class PortfolioCalculator {
|
|||||||
currentRateService: CurrentRateService;
|
currentRateService: CurrentRateService;
|
||||||
dateRange: DateRange;
|
dateRange: DateRange;
|
||||||
exchangeRateDataService: ExchangeRateDataService;
|
exchangeRateDataService: ExchangeRateDataService;
|
||||||
isExperimentalFeatures: boolean;
|
|
||||||
redisCacheService: RedisCacheService;
|
redisCacheService: RedisCacheService;
|
||||||
|
useCache: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
this.accountBalanceItems = accountBalanceItems;
|
this.accountBalanceItems = accountBalanceItems;
|
||||||
this.configurationService = configurationService;
|
this.configurationService = configurationService;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.currentRateService = currentRateService;
|
this.currentRateService = currentRateService;
|
||||||
|
this.dateRange = dateRange;
|
||||||
this.exchangeRateDataService = exchangeRateDataService;
|
this.exchangeRateDataService = exchangeRateDataService;
|
||||||
this.isExperimentalFeatures = isExperimentalFeatures;
|
|
||||||
|
|
||||||
this.orders = activities
|
this.activities = activities
|
||||||
.map(
|
.map(
|
||||||
({
|
({
|
||||||
date,
|
date,
|
||||||
@ -107,6 +108,12 @@ export abstract class PortfolioCalculator {
|
|||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
}) => {
|
}) => {
|
||||||
|
if (isAfter(date, new Date(Date.now()))) {
|
||||||
|
// Adapt date to today if activity is in future (e.g. liability)
|
||||||
|
// to include it in the interval
|
||||||
|
date = endOfDay(new Date(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
tags,
|
tags,
|
||||||
@ -123,6 +130,7 @@ export abstract class PortfolioCalculator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.redisCacheService = redisCacheService;
|
this.redisCacheService = redisCacheService;
|
||||||
|
this.useCache = useCache;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
const { endDate, startDate } = getInterval(dateRange);
|
||||||
@ -917,7 +925,7 @@ export abstract class PortfolioCalculator {
|
|||||||
tags,
|
tags,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of this.orders) {
|
} of this.activities) {
|
||||||
let currentTransactionPointItem: TransactionPointSymbol;
|
let currentTransactionPointItem: TransactionPointSymbol;
|
||||||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||||
|
|
||||||
@ -1041,11 +1049,13 @@ export abstract class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initialize() {
|
private async initialize() {
|
||||||
if (this.isExperimentalFeatures) {
|
if (this.useCache) {
|
||||||
const startTimeTotal = performance.now();
|
const startTimeTotal = performance.now();
|
||||||
|
|
||||||
const cachedSnapshot = await this.redisCacheService.get(
|
const cachedSnapshot = await this.redisCacheService.get(
|
||||||
this.redisCacheService.getPortfolioSnapshotKey(this.userId)
|
this.redisCacheService.getPortfolioSnapshotKey({
|
||||||
|
userId: this.userId
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedSnapshot) {
|
if (cachedSnapshot) {
|
||||||
@ -1068,7 +1078,9 @@ export abstract class PortfolioCalculator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.redisCacheService.set(
|
this.redisCacheService.set(
|
||||||
this.redisCacheService.getPortfolioSnapshotKey(this.userId),
|
this.redisCacheService.getPortfolioSnapshotKey({
|
||||||
|
userId: this.userId
|
||||||
|
}),
|
||||||
JSON.stringify(this.snapshot),
|
JSON.stringify(this.snapshot),
|
||||||
this.configurationService.get('CACHE_QUOTES_TTL')
|
this.configurationService.get('CACHE_QUOTES_TTL')
|
||||||
);
|
);
|
||||||
|
@ -123,6 +123,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,6 +106,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
date: new Date('2022-01-01'),
|
date: new Date('2023-01-01'), // Date in future
|
||||||
fee: 0,
|
fee: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
@ -93,64 +93,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
|
||||||
parseDate('2022-01-01')
|
|
||||||
);
|
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(portfolioSnapshot).toEqual({
|
const liabilitiesInBaseCurrency =
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
await portfolioCalculator.getLiabilitiesInBaseCurrency();
|
||||||
errors: [],
|
|
||||||
grossPerformance: new Big('0'),
|
expect(liabilitiesInBaseCurrency).toEqual(new Big(3000));
|
||||||
grossPerformancePercentage: new Big('0'),
|
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
|
||||||
grossPerformanceWithCurrencyEffect: new Big('0'),
|
|
||||||
hasErrors: true,
|
|
||||||
netPerformance: new Big('0'),
|
|
||||||
netPerformancePercentage: new Big('0'),
|
|
||||||
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
|
||||||
netPerformanceWithCurrencyEffect: new Big('0'),
|
|
||||||
positions: [
|
|
||||||
{
|
|
||||||
averagePrice: new Big('3000'),
|
|
||||||
currency: 'USD',
|
|
||||||
dataSource: 'MANUAL',
|
|
||||||
dividend: new Big('0'),
|
|
||||||
dividendInBaseCurrency: new Big('0'),
|
|
||||||
fee: new Big('0'),
|
|
||||||
firstBuyDate: '2022-01-01',
|
|
||||||
grossPerformance: null,
|
|
||||||
grossPerformancePercentage: null,
|
|
||||||
grossPerformancePercentageWithCurrencyEffect: null,
|
|
||||||
grossPerformanceWithCurrencyEffect: null,
|
|
||||||
investment: new Big('0'),
|
|
||||||
investmentWithCurrencyEffect: new Big('0'),
|
|
||||||
marketPrice: null,
|
|
||||||
marketPriceInBaseCurrency: 3000,
|
|
||||||
netPerformance: null,
|
|
||||||
netPerformancePercentage: null,
|
|
||||||
netPerformancePercentageWithCurrencyEffect: null,
|
|
||||||
netPerformanceWithCurrencyEffect: null,
|
|
||||||
quantity: new Big('0'),
|
|
||||||
symbol: '55196015-1365-4560-aa60-8751ae6d18f8',
|
|
||||||
tags: [],
|
|
||||||
timeWeightedInvestment: new Big('0'),
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
|
||||||
transactionCount: 1,
|
|
||||||
valueInBaseCurrency: new Big('0')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
totalFeesWithCurrencyEffect: new Big('0'),
|
|
||||||
totalInterestWithCurrencyEffect: new Big('0'),
|
|
||||||
totalInvestment: new Big('0'),
|
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
|
||||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
|
||||||
totalValuablesWithCurrencyEffect: new Big('0')
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities: [],
|
activities: [],
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
let valueAtStartDateWithCurrencyEffect: Big;
|
let valueAtStartDateWithCurrencyEffect: Big;
|
||||||
|
|
||||||
// Clone orders to keep the original values in this.orders
|
// Clone orders to keep the original values in this.orders
|
||||||
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
|
let orders: PortfolioOrderItem[] = cloneDeep(this.activities).filter(
|
||||||
({ SymbolProfile }) => {
|
({ SymbolProfile }) => {
|
||||||
return SymbolProfile.symbol === symbol;
|
return SymbolProfile.symbol === symbol;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,13 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
|
|||||||
|
|
||||||
function mockGetValue(symbol: string, date: Date) {
|
function mockGetValue(symbol: string, date: Date) {
|
||||||
switch (symbol) {
|
switch (symbol) {
|
||||||
|
case '55196015-1365-4560-aa60-8751ae6d18f8':
|
||||||
|
if (isSameDay(parseDate('2022-01-31'), date)) {
|
||||||
|
return { marketPrice: 3000 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
case 'BALN.SW':
|
case 'BALN.SW':
|
||||||
if (isSameDay(parseDate('2021-11-12'), date)) {
|
if (isSameDay(parseDate('2021-11-12'), date)) {
|
||||||
return { marketPrice: 146 };
|
return { marketPrice: 146 };
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
|
|
||||||
import { Account, Tag } from '@prisma/client';
|
import { Account, Tag } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioHoldingDetail {
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
dataProviderInfo: DataProviderInfo;
|
dataProviderInfo: DataProviderInfo;
|
@ -1,5 +0,0 @@
|
|||||||
import { Position } from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
export interface PortfolioPositions {
|
|
||||||
positions: Position[];
|
|
||||||
}
|
|
@ -7,9 +7,9 @@ import {
|
|||||||
nullifyValuesInObject
|
nullifyValuesInObject
|
||||||
} from '@ghostfolio/api/helper/object.helper';
|
} from '@ghostfolio/api/helper/object.helper';
|
||||||
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -27,6 +27,10 @@ import {
|
|||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport
|
PortfolioReport
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import {
|
||||||
|
hasReadRestrictedAccessPermission,
|
||||||
|
isRestrictedView
|
||||||
|
} from '@ghostfolio/common/permissions';
|
||||||
import type {
|
import type {
|
||||||
DateRange,
|
DateRange,
|
||||||
GroupBy,
|
GroupBy,
|
||||||
@ -51,8 +55,7 @@ import { AssetClass, AssetSubClass } from '@prisma/client';
|
|||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
|
||||||
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
|
||||||
@Controller('portfolio')
|
@Controller('portfolio')
|
||||||
@ -85,11 +88,6 @@ export class PortfolioController {
|
|||||||
|
|
||||||
let hasDetails = true;
|
let hasDetails = true;
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
const hasReadRestrictedAccessPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user: this.request.user
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
hasDetails = this.request.user.subscription.type === 'Premium';
|
hasDetails = this.request.user.subscription.type === 'Premium';
|
||||||
@ -118,8 +116,11 @@ export class PortfolioController {
|
|||||||
let portfolioSummary = summary;
|
let portfolioSummary = summary;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const totalInvestment = Object.values(holdings)
|
const totalInvestment = Object.values(holdings)
|
||||||
.map(({ investment }) => {
|
.map(({ investment }) => {
|
||||||
@ -159,27 +160,30 @@ export class PortfolioController {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
hasDetails === false ||
|
hasDetails === false ||
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
portfolioSummary = nullifyValuesInObject(summary, [
|
portfolioSummary = nullifyValuesInObject(summary, [
|
||||||
'cash',
|
'cash',
|
||||||
'committedFunds',
|
'committedFunds',
|
||||||
'currentGrossPerformance',
|
|
||||||
'currentGrossPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetPerformance',
|
|
||||||
'currentNetPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetWorth',
|
'currentNetWorth',
|
||||||
'currentValue',
|
'currentValueInBaseCurrency',
|
||||||
'dividendInBaseCurrency',
|
'dividendInBaseCurrency',
|
||||||
'emergencyFund',
|
'emergencyFund',
|
||||||
'excludedAccountsAndActivities',
|
'excludedAccountsAndActivities',
|
||||||
'fees',
|
'fees',
|
||||||
'filteredValueInBaseCurrency',
|
'filteredValueInBaseCurrency',
|
||||||
'fireWealth',
|
'fireWealth',
|
||||||
|
'grossPerformance',
|
||||||
|
'grossPerformanceWithCurrencyEffect',
|
||||||
'interest',
|
'interest',
|
||||||
'items',
|
'items',
|
||||||
'liabilities',
|
'liabilities',
|
||||||
|
'netPerformance',
|
||||||
|
'netPerformanceWithCurrencyEffect',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
'totalInvestment',
|
'totalInvestment',
|
||||||
'totalSell',
|
'totalSell',
|
||||||
@ -200,6 +204,7 @@ export class PortfolioController {
|
|||||||
: undefined,
|
: undefined,
|
||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||||
|
holdings: hasDetails ? portfolioPosition.holdings : [],
|
||||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||||
marketsAdvanced: hasDetails
|
marketsAdvanced: hasDetails
|
||||||
? portfolioPosition.marketsAdvanced
|
? portfolioPosition.marketsAdvanced
|
||||||
@ -227,12 +232,6 @@ export class PortfolioController {
|
|||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<PortfolioDividends> {
|
): Promise<PortfolioDividends> {
|
||||||
const hasReadRestrictedAccessPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user: this.request.user
|
|
||||||
});
|
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
@ -260,8 +259,11 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const maxDividend = dividends.reduce(
|
const maxDividend = dividends.reduce(
|
||||||
(investment, item) => Math.max(investment, item.investment),
|
(investment, item) => Math.max(investment, item.investment),
|
||||||
@ -327,12 +329,6 @@ export class PortfolioController {
|
|||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<PortfolioInvestments> {
|
): Promise<PortfolioInvestments> {
|
||||||
const hasReadRestrictedAccessPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user: this.request.user
|
|
||||||
});
|
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
@ -348,8 +344,11 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const maxInvestment = investments.reduce(
|
const maxInvestment = investments.reduce(
|
||||||
(investment, item) => Math.max(investment, item.investment),
|
(investment, item) => Math.max(investment, item.investment),
|
||||||
@ -398,12 +397,6 @@ export class PortfolioController {
|
|||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
|
|
||||||
const hasReadRestrictedAccessPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user: this.request.user
|
|
||||||
});
|
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
@ -419,9 +412,12 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
impersonationId,
|
||||||
this.userService.isRestrictedView(this.request.user)
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user) ||
|
||||||
|
this.request.user.Settings.settings.viewMode === 'ZEN'
|
||||||
) {
|
) {
|
||||||
performanceInformation.chart = performanceInformation.chart.map(
|
performanceInformation.chart = performanceInformation.chart.map(
|
||||||
({
|
({
|
||||||
@ -449,10 +445,14 @@ export class PortfolioController {
|
|||||||
.div(performanceInformation.performance.totalInvestment)
|
.div(performanceInformation.performance.totalInvestment)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
valueInPercentage:
|
valueInPercentage:
|
||||||
performanceInformation.performance.currentValue === 0
|
performanceInformation.performance.currentValueInBaseCurrency ===
|
||||||
|
0
|
||||||
? 0
|
? 0
|
||||||
: new Big(value)
|
: new Big(value)
|
||||||
.div(performanceInformation.performance.currentValue)
|
.div(
|
||||||
|
performanceInformation.performance
|
||||||
|
.currentValueInBaseCurrency
|
||||||
|
)
|
||||||
.toNumber()
|
.toNumber()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -461,12 +461,12 @@ export class PortfolioController {
|
|||||||
performanceInformation.performance = nullifyValuesInObject(
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
performanceInformation.performance,
|
performanceInformation.performance,
|
||||||
[
|
[
|
||||||
'currentGrossPerformance',
|
|
||||||
'currentGrossPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetPerformance',
|
|
||||||
'currentNetPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetWorth',
|
'currentNetWorth',
|
||||||
'currentValue',
|
'currentValueInBaseCurrency',
|
||||||
|
'grossPerformance',
|
||||||
|
'grossPerformanceWithCurrencyEffect',
|
||||||
|
'netPerformance',
|
||||||
|
'netPerformanceWithCurrencyEffect',
|
||||||
'totalInvestment'
|
'totalInvestment'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -483,39 +483,13 @@ export class PortfolioController {
|
|||||||
);
|
);
|
||||||
performanceInformation.performance = nullifyValuesInObject(
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
performanceInformation.performance,
|
performanceInformation.performance,
|
||||||
['currentNetPerformance', 'currentNetPerformancePercent']
|
['netPerformance']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return performanceInformation;
|
return performanceInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('positions')
|
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
|
||||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
|
||||||
public async getPositions(
|
|
||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
|
||||||
@Query('accounts') filterByAccounts?: string,
|
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
|
||||||
@Query('query') filterBySearchQuery?: string,
|
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
|
||||||
@Query('tags') filterByTags?: string
|
|
||||||
): Promise<PortfolioPositions> {
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
|
||||||
filterByAccounts,
|
|
||||||
filterByAssetClasses,
|
|
||||||
filterBySearchQuery,
|
|
||||||
filterByTags
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.portfolioService.getPositions({
|
|
||||||
dateRange,
|
|
||||||
filters,
|
|
||||||
impersonationId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('public/:accessId')
|
@Get('public/:accessId')
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPublic(
|
public async getPublic(
|
||||||
@ -592,7 +566,7 @@ export class PortfolioController {
|
|||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||||
@Param('dataSource') dataSource,
|
@Param('dataSource') dataSource,
|
||||||
@Param('symbol') symbol
|
@Param('symbol') symbol
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioHoldingDetail> {
|
||||||
const position = await this.portfolioService.getPosition(
|
const position = await this.portfolioService.getPosition(
|
||||||
dataSource,
|
dataSource,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
@ -4,6 +4,9 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
@ -36,8 +39,11 @@ import { RulesService } from './rules.service';
|
|||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
RedactValuesInResponseModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -27,7 +27,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||||
netPerformancePercent: new Big(0)
|
netPerformancePercentage: new Big(0)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toEqual(0);
|
).toEqual(0);
|
||||||
@ -36,7 +36,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 0,
|
daysInMarket: 0,
|
||||||
netPerformancePercent: new Big(0)
|
netPerformancePercentage: new Big(0)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toEqual(0);
|
).toEqual(0);
|
||||||
@ -48,7 +48,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 65, // < 1 year
|
daysInMarket: 65, // < 1 year
|
||||||
netPerformancePercent: new Big(0.1025)
|
netPerformancePercentage: new Big(0.1025)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toBeCloseTo(0.729705);
|
).toBeCloseTo(0.729705);
|
||||||
@ -57,7 +57,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 365, // 1 year
|
daysInMarket: 365, // 1 year
|
||||||
netPerformancePercent: new Big(0.05)
|
netPerformancePercentage: new Big(0.05)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toBeCloseTo(0.05);
|
).toBeCloseTo(0.05);
|
||||||
@ -69,7 +69,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 575, // > 1 year
|
daysInMarket: 575, // > 1 year
|
||||||
netPerformancePercent: new Big(0.2374)
|
netPerformancePercentage: new Big(0.2374)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toBeCloseTo(0.145);
|
).toBeCloseTo(0.145);
|
||||||
|
@ -77,7 +77,7 @@ import {
|
|||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
PortfolioCalculatorFactory
|
PortfolioCalculatorFactory
|
||||||
} from './calculator/portfolio-calculator.factory';
|
} from './calculator/portfolio-calculator.factory';
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
|
||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
||||||
@ -208,16 +208,16 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public getAnnualizedPerformancePercent({
|
public getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent
|
netPerformancePercentage
|
||||||
}: {
|
}: {
|
||||||
daysInMarket: number;
|
daysInMarket: number;
|
||||||
netPerformancePercent: Big;
|
netPerformancePercentage: Big;
|
||||||
}): Big {
|
}): Big {
|
||||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||||
|
|
||||||
return new Big(
|
return new Big(
|
||||||
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
|
||||||
).minus(1);
|
).minus(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,9 +277,11 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
hasFilters: filters?.length > 0,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -358,6 +360,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
|
hasFilters: true, // disable cache
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user?.Settings.settings.isExperimentalFeatures
|
this.request.user?.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -496,6 +499,7 @@ export class PortfolioService {
|
|||||||
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
grossPerformanceWithCurrencyEffect:
|
grossPerformanceWithCurrencyEffect:
|
||||||
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
|
holdings: assetProfile.holdings,
|
||||||
investment: investment.toNumber(),
|
investment: investment.toNumber(),
|
||||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||||
name: assetProfile.name,
|
name: assetProfile.name,
|
||||||
@ -599,7 +603,7 @@ export class PortfolioService {
|
|||||||
aDataSource: DataSource,
|
aDataSource: DataSource,
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioHoldingDetail> {
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
const userCurrency = this.getUserCurrency(user);
|
const userCurrency = this.getUserCurrency(user);
|
||||||
@ -660,6 +664,7 @@ export class PortfolioService {
|
|||||||
}),
|
}),
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
|
hasFilters: true,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -689,7 +694,7 @@ export class PortfolioService {
|
|||||||
transactionCount
|
transactionCount
|
||||||
} = position;
|
} = position;
|
||||||
|
|
||||||
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
|
const accounts: PortfolioHoldingDetail['accounts'] = uniqBy(
|
||||||
orders.filter(({ Account }) => {
|
orders.filter(({ Account }) => {
|
||||||
return Account;
|
return Account;
|
||||||
}),
|
}),
|
||||||
@ -700,17 +705,21 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||||
netPerformancePercent: dividendInBaseCurrency.div(
|
netPerformancePercentage: timeWeightedInvestment.eq(0)
|
||||||
timeWeightedInvestment
|
? new Big(0)
|
||||||
)
|
: dividendInBaseCurrency.div(timeWeightedInvestment)
|
||||||
});
|
});
|
||||||
|
|
||||||
const dividendYieldPercentWithCurrencyEffect =
|
const dividendYieldPercentWithCurrencyEffect =
|
||||||
this.getAnnualizedPerformancePercent({
|
this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||||
netPerformancePercent: dividendInBaseCurrency.div(
|
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
|
||||||
timeWeightedInvestmentWithCurrencyEffect
|
0
|
||||||
)
|
)
|
||||||
|
? new Big(0)
|
||||||
|
: dividendInBaseCurrency.div(
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
@ -931,6 +940,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
hasFilters: filters?.length > 0,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -1085,7 +1095,7 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
const { endDate } = getInterval(dateRange);
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
endDate,
|
endDate,
|
||||||
@ -1101,16 +1111,16 @@ export class PortfolioService {
|
|||||||
firstOrderDate: undefined,
|
firstOrderDate: undefined,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance: 0,
|
|
||||||
currentGrossPerformancePercent: 0,
|
|
||||||
currentGrossPerformancePercentWithCurrencyEffect: 0,
|
|
||||||
currentGrossPerformanceWithCurrencyEffect: 0,
|
|
||||||
currentNetPerformance: 0,
|
|
||||||
currentNetPerformancePercent: 0,
|
|
||||||
currentNetPerformancePercentWithCurrencyEffect: 0,
|
|
||||||
currentNetPerformanceWithCurrencyEffect: 0,
|
|
||||||
currentNetWorth: 0,
|
currentNetWorth: 0,
|
||||||
currentValue: 0,
|
currentValueInBaseCurrency: 0,
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercentage: 0,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: 0,
|
||||||
|
grossPerformanceWithCurrencyEffect: 0,
|
||||||
|
netPerformance: 0,
|
||||||
|
netPerformancePercentage: 0,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: 0,
|
||||||
|
netPerformanceWithCurrencyEffect: 0,
|
||||||
totalInvestment: 0
|
totalInvestment: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1123,6 +1133,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
|
hasFilters: filters?.length > 0,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -1144,9 +1155,9 @@ export class PortfolioService {
|
|||||||
|
|
||||||
let currentNetPerformance = netPerformance;
|
let currentNetPerformance = netPerformance;
|
||||||
|
|
||||||
let currentNetPerformancePercent = netPerformancePercentage;
|
let currentNetPerformancePercentage = netPerformancePercentage;
|
||||||
|
|
||||||
let currentNetPerformancePercentWithCurrencyEffect =
|
let currentNetPerformancePercentageWithCurrencyEffect =
|
||||||
netPerformancePercentageWithCurrencyEffect;
|
netPerformancePercentageWithCurrencyEffect;
|
||||||
|
|
||||||
let currentNetPerformanceWithCurrencyEffect =
|
let currentNetPerformanceWithCurrencyEffect =
|
||||||
@ -1165,11 +1176,11 @@ export class PortfolioService {
|
|||||||
if (itemOfToday) {
|
if (itemOfToday) {
|
||||||
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
||||||
|
|
||||||
currentNetPerformancePercent = new Big(
|
currentNetPerformancePercentage = new Big(
|
||||||
itemOfToday.netPerformanceInPercentage
|
itemOfToday.netPerformanceInPercentage
|
||||||
).div(100);
|
).div(100);
|
||||||
|
|
||||||
currentNetPerformancePercentWithCurrencyEffect = new Big(
|
currentNetPerformancePercentageWithCurrencyEffect = new Big(
|
||||||
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
|
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
|
||||||
).div(100);
|
).div(100);
|
||||||
|
|
||||||
@ -1187,19 +1198,19 @@ export class PortfolioService {
|
|||||||
firstOrderDate: parseDate(items[0]?.date),
|
firstOrderDate: parseDate(items[0]?.date),
|
||||||
performance: {
|
performance: {
|
||||||
currentNetWorth,
|
currentNetWorth,
|
||||||
currentGrossPerformance: grossPerformance.toNumber(),
|
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
|
||||||
currentGrossPerformancePercent: grossPerformancePercentage.toNumber(),
|
grossPerformance: grossPerformance.toNumber(),
|
||||||
currentGrossPerformancePercentWithCurrencyEffect:
|
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect:
|
||||||
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
currentGrossPerformanceWithCurrencyEffect:
|
grossPerformanceWithCurrencyEffect:
|
||||||
grossPerformanceWithCurrencyEffect.toNumber(),
|
grossPerformanceWithCurrencyEffect.toNumber(),
|
||||||
currentNetPerformance: currentNetPerformance.toNumber(),
|
netPerformance: currentNetPerformance.toNumber(),
|
||||||
currentNetPerformancePercent: currentNetPerformancePercent.toNumber(),
|
netPerformancePercentage: currentNetPerformancePercentage.toNumber(),
|
||||||
currentNetPerformancePercentWithCurrencyEffect:
|
netPerformancePercentageWithCurrencyEffect:
|
||||||
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
|
currentNetPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
currentNetPerformanceWithCurrencyEffect:
|
netPerformanceWithCurrencyEffect:
|
||||||
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
||||||
currentValue: currentValueInBaseCurrency.toNumber(),
|
|
||||||
totalInvestment: totalInvestment.toNumber()
|
totalInvestment: totalInvestment.toNumber()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1220,6 +1231,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
hasFilters: false,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -1454,6 +1466,7 @@ export class PortfolioService {
|
|||||||
grossPerformancePercent: 0,
|
grossPerformancePercent: 0,
|
||||||
grossPerformancePercentWithCurrencyEffect: 0,
|
grossPerformancePercentWithCurrencyEffect: 0,
|
||||||
grossPerformanceWithCurrencyEffect: 0,
|
grossPerformanceWithCurrencyEffect: 0,
|
||||||
|
holdings: [],
|
||||||
investment: balance,
|
investment: balance,
|
||||||
marketPrice: 0,
|
marketPrice: 0,
|
||||||
marketState: 'open',
|
marketState: 'open',
|
||||||
@ -1595,11 +1608,6 @@ export class PortfolioService {
|
|||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const performanceInformation = await this.getPerformance({
|
|
||||||
impersonationId,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
@ -1617,6 +1625,19 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentValueInBaseCurrency,
|
||||||
|
grossPerformance,
|
||||||
|
grossPerformancePercentage,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
|
grossPerformanceWithCurrencyEffect,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercentage,
|
||||||
|
netPerformancePercentageWithCurrencyEffect,
|
||||||
|
netPerformanceWithCurrencyEffect,
|
||||||
|
totalInvestment
|
||||||
|
} = await portfolioCalculator.getSnapshot();
|
||||||
|
|
||||||
const dividendInBaseCurrency =
|
const dividendInBaseCurrency =
|
||||||
await portfolioCalculator.getDividendInBaseCurrency();
|
await portfolioCalculator.getDividendInBaseCurrency();
|
||||||
|
|
||||||
@ -1685,7 +1706,7 @@ export class PortfolioService {
|
|||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
const netWorth = new Big(balanceInBaseCurrency)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
.plus(performanceInformation.performance.currentValue)
|
.plus(currentValueInBaseCurrency)
|
||||||
.plus(valuables)
|
.plus(valuables)
|
||||||
.plus(excludedAccountsAndActivities)
|
.plus(excludedAccountsAndActivities)
|
||||||
.minus(liabilities)
|
.minus(liabilities)
|
||||||
@ -1695,21 +1716,18 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent: new Big(
|
netPerformancePercentage: new Big(netPerformancePercentage)
|
||||||
performanceInformation.performance.currentNetPerformancePercent
|
|
||||||
)
|
|
||||||
})?.toNumber();
|
})?.toNumber();
|
||||||
|
|
||||||
const annualizedPerformancePercentWithCurrencyEffect =
|
const annualizedPerformancePercentWithCurrencyEffect =
|
||||||
this.getAnnualizedPerformancePercent({
|
this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent: new Big(
|
netPerformancePercentage: new Big(
|
||||||
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
|
netPerformancePercentageWithCurrencyEffect
|
||||||
)
|
)
|
||||||
})?.toNumber();
|
})?.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
|
||||||
annualizedPerformancePercent,
|
annualizedPerformancePercent,
|
||||||
annualizedPerformancePercentWithCurrencyEffect,
|
annualizedPerformancePercentWithCurrencyEffect,
|
||||||
cash,
|
cash,
|
||||||
@ -1718,6 +1736,7 @@ export class PortfolioService {
|
|||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
|
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
|
||||||
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
||||||
emergencyFund: {
|
emergencyFund: {
|
||||||
assets: emergencyFundPositionsValueInBaseCurrency,
|
assets: emergencyFundPositionsValueInBaseCurrency,
|
||||||
@ -1731,15 +1750,28 @@ export class PortfolioService {
|
|||||||
filteredValueInPercentage: netWorth
|
filteredValueInPercentage: netWorth
|
||||||
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||||
: undefined,
|
: undefined,
|
||||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
fireWealth: new Big(currentValueInBaseCurrency)
|
||||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
|
grossPerformance: grossPerformance.toNumber(),
|
||||||
|
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect:
|
||||||
|
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
|
grossPerformanceWithCurrencyEffect:
|
||||||
|
grossPerformanceWithCurrencyEffect.toNumber(),
|
||||||
interest: interest.toNumber(),
|
interest: interest.toNumber(),
|
||||||
items: valuables.toNumber(),
|
items: valuables.toNumber(),
|
||||||
liabilities: liabilities.toNumber(),
|
liabilities: liabilities.toNumber(),
|
||||||
|
netPerformance: netPerformance.toNumber(),
|
||||||
|
netPerformancePercentage: netPerformancePercentage.toNumber(),
|
||||||
|
netPerformancePercentageWithCurrencyEffect:
|
||||||
|
netPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
|
netPerformanceWithCurrencyEffect:
|
||||||
|
netPerformanceWithCurrencyEffect.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return ['BUY', 'SELL'].includes(type);
|
||||||
}).length,
|
}).length,
|
||||||
|
totalInvestment: totalInvestment.toNumber(),
|
||||||
totalValueInBaseCurrency: netWorth
|
totalValueInBaseCurrency: netWorth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export class RedisCacheService {
|
|||||||
return this.cache.get(key);
|
return this.cache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPortfolioSnapshotKey(userId: string) {
|
public getPortfolioSnapshotKey({ userId }: { userId: string }) {
|
||||||
return `portfolio-snapshot-${userId}`;
|
return `portfolio-snapshot-${userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getYesterday,
|
getYesterday,
|
||||||
interpolate
|
interpolate
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
|
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
|
||||||
|
|
||||||
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
|
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
@ -14,7 +16,9 @@ import * as path from 'path';
|
|||||||
export class SitemapController {
|
export class SitemapController {
|
||||||
public sitemapXml = '';
|
public sitemapXml = '';
|
||||||
|
|
||||||
public constructor() {
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
this.sitemapXml = fs.readFileSync(
|
this.sitemapXml = fs.readFileSync(
|
||||||
path.join(__dirname, 'assets', 'sitemap.xml'),
|
path.join(__dirname, 'assets', 'sitemap.xml'),
|
||||||
@ -25,11 +29,51 @@ export class SitemapController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Version(VERSION_NEUTRAL)
|
@Version(VERSION_NEUTRAL)
|
||||||
public async flushCache(@Res() response: Response): Promise<void> {
|
public async getSitemapXml(@Res() response: Response): Promise<void> {
|
||||||
|
const currentDate = format(getYesterday(), DATE_FORMAT);
|
||||||
|
|
||||||
response.setHeader('content-type', 'application/xml');
|
response.setHeader('content-type', 'application/xml');
|
||||||
response.send(
|
response.send(
|
||||||
interpolate(this.sitemapXml, {
|
interpolate(this.sitemapXml, {
|
||||||
currentDate: format(getYesterday(), DATE_FORMAT)
|
currentDate,
|
||||||
|
personalFinanceTools: this.configurationService.get(
|
||||||
|
'ENABLE_FEATURE_SUBSCRIPTION'
|
||||||
|
)
|
||||||
|
? personalFinanceTools
|
||||||
|
.map(({ alias, key }) => {
|
||||||
|
return [
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>',
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>',
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/es/recursos/personal-finance-tools/alternativa-de-software-libre-a-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>',
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/fr/ressources/personal-finance-tools/alternative-open-source-a-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>',
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>',
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>',
|
||||||
|
'<url>',
|
||||||
|
` <loc>https://ghostfol.io/pt/recursos/personal-finance-tools/alternativa-de-software-livre-ao-${alias ?? key}</loc>`,
|
||||||
|
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||||
|
'</url>'
|
||||||
|
].join('\n');
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
: ''
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -12,14 +6,6 @@ import { SitemapController } from './sitemap.controller';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [SitemapController],
|
controllers: [SitemapController],
|
||||||
imports: [
|
imports: [ConfigurationModule]
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
|
||||||
DataProviderModule,
|
|
||||||
ExchangeRateDataModule,
|
|
||||||
PrismaModule,
|
|
||||||
RedisCacheModule,
|
|
||||||
SymbolProfileModule
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class SitemapModule {}
|
export class SitemapModule {}
|
||||||
|
@ -22,7 +22,7 @@ export class SubscriptionService {
|
|||||||
this.stripe = new Stripe(
|
this.stripe = new Stripe(
|
||||||
this.configurationService.get('STRIPE_SECRET_KEY'),
|
this.configurationService.get('STRIPE_SECRET_KEY'),
|
||||||
{
|
{
|
||||||
apiVersion: '2022-11-15'
|
apiVersion: '2024-04-10'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
@ -12,10 +13,11 @@ import { SymbolService } from './symbol.service';
|
|||||||
controllers: [SymbolController],
|
controllers: [SymbolController],
|
||||||
exports: [SymbolService],
|
exports: [SymbolService],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PrismaModule
|
PrismaModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [SymbolService]
|
providers: [SymbolService]
|
||||||
})
|
})
|
||||||
|
6
apps/api/src/app/user/delete-own-user.dto.ts
Normal file
6
apps/api/src/app/user/delete-own-user.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeleteOwnUserDto {
|
||||||
|
@IsString()
|
||||||
|
accessToken: string;
|
||||||
|
}
|
@ -1,12 +1,9 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
hasPermission,
|
|
||||||
hasRole,
|
|
||||||
permissions
|
|
||||||
} from '@ghostfolio/common/permissions';
|
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -29,6 +26,7 @@ import { User as UserModel } from '@prisma/client';
|
|||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { size } from 'lodash';
|
import { size } from 'lodash';
|
||||||
|
|
||||||
|
import { DeleteOwnUserDto } from './delete-own-user.dto';
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
@ -36,12 +34,41 @@ import { UserService } from './user.service';
|
|||||||
@Controller('user')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@HasPermission(permissions.deleteOwnUser)
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
public async deleteOwnUser(
|
||||||
|
@Body() data: DeleteOwnUserDto
|
||||||
|
): Promise<UserModel> {
|
||||||
|
const hashedAccessToken = this.userService.createAccessToken(
|
||||||
|
data.accessToken,
|
||||||
|
this.configurationService.get('ACCESS_TOKEN_SALT')
|
||||||
|
);
|
||||||
|
|
||||||
|
const [user] = await this.userService.users({
|
||||||
|
where: { accessToken: hashedAccessToken, id: this.request.user.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userService.deleteUser({
|
||||||
|
accessToken: hashedAccessToken,
|
||||||
|
id: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HasPermission(permissions.deleteUser)
|
@HasPermission(permissions.deleteUser)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
@ -63,13 +90,6 @@ export class UserController {
|
|||||||
public async getUser(
|
public async getUser(
|
||||||
@Headers('accept-language') acceptLanguage: string
|
@Headers('accept-language') acceptLanguage: string
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
if (hasRole(this.request.user, 'INACTIVE')) {
|
|
||||||
throw new HttpException(
|
|
||||||
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
|
||||||
StatusCodes.TOO_MANY_REQUESTS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.userService.getUser(
|
return this.userService.getUser(
|
||||||
this.request.user,
|
this.request.user,
|
||||||
acceptLanguage?.split(',')?.[0]
|
acceptLanguage?.split(',')?.[0]
|
||||||
|
@ -121,28 +121,6 @@ export class UserService {
|
|||||||
return usersWithAdminRole.length > 0;
|
return usersWithAdminRole.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user
|
|
||||||
}: {
|
|
||||||
impersonationId: string;
|
|
||||||
user: UserWithSettings;
|
|
||||||
}) {
|
|
||||||
if (!impersonationId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const access = user.Access?.find(({ id }) => {
|
|
||||||
return id === impersonationId;
|
|
||||||
});
|
|
||||||
|
|
||||||
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isRestrictedView(aUser: UserWithSettings) {
|
|
||||||
return aUser.Settings.settings.isRestrictedView ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async user(
|
public async user(
|
||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||||
): Promise<UserWithSettings | null> {
|
): Promise<UserWithSettings | null> {
|
||||||
@ -262,10 +240,13 @@ export class UserService {
|
|||||||
|
|
||||||
// Reset benchmark
|
// Reset benchmark
|
||||||
user.Settings.settings.benchmark = undefined;
|
user.Settings.settings.benchmark = undefined;
|
||||||
}
|
} else if (user.subscription?.type === 'Premium') {
|
||||||
|
|
||||||
if (user.subscription?.type === 'Premium') {
|
|
||||||
currentPermissions.push(permissions.reportDataGlitch);
|
currentPermissions.push(permissions.reportDataGlitch);
|
||||||
|
|
||||||
|
currentPermissions = without(
|
||||||
|
currentPermissions,
|
||||||
|
permissions.deleteOwnUser
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -54,206 +54,6 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-allinvestview</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-allvue-systems</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-basil-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-capitally</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-capmon</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-compound-planning</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-copilot-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-de.fi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-delta</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-divvydiary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-empower</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-fina</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finwise</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-getquin</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-gospatz</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-intuit-mint</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-magnifi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monarch-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-parqet</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-plannix</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-dividend-tracker</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portseido</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-projectionlab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-rocket-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-seeking-alpha</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sharesight</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-simple-portfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockle</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sumio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-tiller</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-whal</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-ynab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -408,206 +208,6 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-allinvestview</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-allvue-systems</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-basil-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-capitally</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-capmon</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-compound-planning</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-copilot-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-de.fi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-delta</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-divvydiary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-empower</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-fina</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finwise</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-getquin</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-gospatz</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-intuit-mint</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-magnifi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monarch-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-parqet</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-plannix</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portseido</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-rocket-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sharesight</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-simple-portfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockle</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-tiller</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-whal</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-ynab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es</loc>
|
<loc>https://ghostfol.io/es</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -638,6 +238,10 @@
|
|||||||
<loc>https://ghostfol.io/es/recursos</loc>
|
<loc>https://ghostfol.io/es/recursos</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/recursos/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/registro</loc>
|
<loc>https://ghostfol.io/es/registro</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -716,6 +320,10 @@
|
|||||||
<loc>https://ghostfol.io/fr/ressources</loc>
|
<loc>https://ghostfol.io/fr/ressources</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/ressources/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it</loc>
|
<loc>https://ghostfol.io/it</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -774,206 +382,6 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-allinvestview</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-allvue-systems</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-basil-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-capitally</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-capmon</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-compound-planning</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-copilot-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-de.fi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-delta</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-divvydiary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-empower</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-fina</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finwise</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-getquin</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-intuit-mint</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-magnifi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monarch-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-parqet</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-plannix</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portseido</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-rocket-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sharesight</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-simple-portfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockle</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-tiller</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-whal</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-ynab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl</loc>
|
<loc>https://ghostfol.io/nl</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -986,206 +394,6 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-allinvestview</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-allvue-systems</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-basil-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-capitally</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-capmon</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-compound-planning</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-copilot-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-de.fi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-delta</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-divvydiary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-empower</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-fina</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finwise</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-getquin</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-intuit-mint</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-magnifi</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monarch-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-parqet</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-plannix</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portseido</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-rocket-money</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sharesight</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-simple-portfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockle</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-tiller</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthfolio</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-whal</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-ynab</loc>
|
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
|
||||||
</url>
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1270,6 +478,10 @@
|
|||||||
<loc>https://ghostfol.io/pt/recursos</loc>
|
<loc>https://ghostfol.io/pt/recursos</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/recursos/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/registo</loc>
|
<loc>https://ghostfol.io/pt/registo</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1304,4 +516,5 @@
|
|||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
-->
|
-->
|
||||||
|
${personalFinanceTools}
|
||||||
</urlset>
|
</urlset>
|
||||||
|
@ -12,12 +12,14 @@ export class PortfolioChangedListener {
|
|||||||
@OnEvent(PortfolioChangedEvent.getName())
|
@OnEvent(PortfolioChangedEvent.getName())
|
||||||
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
|
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
|
||||||
Logger.log(
|
Logger.log(
|
||||||
`Portfolio of user with id ${event.getUserId()} has changed`,
|
`Portfolio of user '${event.getUserId()}' has changed`,
|
||||||
'PortfolioChangedListener'
|
'PortfolioChangedListener'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.redisCacheService.remove(
|
this.redisCacheService.remove(
|
||||||
this.redisCacheService.getPortfolioSnapshotKey(event.getUserId())
|
this.redisCacheService.getPortfolioSnapshotKey({
|
||||||
|
userId: event.getUserId()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
|
||||||
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
|
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
|
||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
|
import {
|
||||||
|
hasReadRestrictedAccessPermission,
|
||||||
|
isRestrictedView
|
||||||
|
} from '@ghostfolio/common/permissions';
|
||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,7 +19,7 @@ import { map } from 'rxjs/operators';
|
|||||||
export class RedactValuesInResponseInterceptor<T>
|
export class RedactValuesInResponseInterceptor<T>
|
||||||
implements NestInterceptor<T, any>
|
implements NestInterceptor<T, any>
|
||||||
{
|
{
|
||||||
public constructor(private userService: UserService) {}
|
public constructor() {}
|
||||||
|
|
||||||
public intercept(
|
public intercept(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
@ -29,15 +32,13 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
|
|
||||||
const impersonationId =
|
const impersonationId =
|
||||||
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
||||||
const hasReadRestrictedPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(user)
|
impersonationId,
|
||||||
|
user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(user)
|
||||||
) {
|
) {
|
||||||
data = redactAttributes({
|
data = redactAttributes({
|
||||||
object: data,
|
object: data,
|
@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class RedactValuesInResponseModule {}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [ConfigurationService],
|
||||||
|
imports: [ConfigurationModule],
|
||||||
|
providers: [ConfigurationService]
|
||||||
|
})
|
||||||
|
export class TransformDataSourceInRequestModule {}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [ConfigurationService],
|
||||||
|
imports: [ConfigurationModule],
|
||||||
|
providers: [ConfigurationService]
|
||||||
|
})
|
||||||
|
export class TransformDataSourceInResponseModule {}
|
@ -2,7 +2,7 @@ import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import * as bodyParser from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
@ -34,7 +34,7 @@ async function bootstrap() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Support 10mb csv/json files for importing activities
|
// Support 10mb csv/json files for importing activities
|
||||||
app.use(bodyParser.json({ limit: '10mb' }));
|
app.use(json({ limit: '10mb' }));
|
||||||
|
|
||||||
if (configService.get<string>('ENABLE_FEATURE_SUBSCRIPTION') === 'true') {
|
if (configService.get<string>('ENABLE_FEATURE_SUBSCRIPTION') === 'true') {
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -181,6 +181,7 @@ export class DataGatheringService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
@ -198,6 +199,7 @@ export class DataGatheringService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
@ -212,6 +214,7 @@ export class DataGatheringService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
|
@ -50,7 +50,9 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Alpha Vantage',
|
||||||
|
url: 'https://www.alphavantage.co'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ export class DataEnhancerService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||||
|
(assetProfile.holdings as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||||
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
|
import { Holding } from '@ghostfolio/common/interfaces';
|
||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
|
|
||||||
@ -155,11 +156,30 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!response.holdings ||
|
||||||
|
(response.holdings as unknown as Holding[]).length === 0
|
||||||
|
) {
|
||||||
|
response.holdings = [];
|
||||||
|
|
||||||
|
for (const { label, weight } of holdings?.topHoldings ?? []) {
|
||||||
|
if (label?.toLowerCase() === 'other') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.holdings.push({
|
||||||
|
weight,
|
||||||
|
name: label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!response.sectors ||
|
!response.sectors ||
|
||||||
(response.sectors as unknown as Sector[]).length === 0
|
(response.sectors as unknown as Sector[]).length === 0
|
||||||
) {
|
) {
|
||||||
response.sectors = [];
|
response.sectors = [];
|
||||||
|
|
||||||
for (const [name, value] of Object.entries<any>(
|
for (const [name, value] of Object.entries<any>(
|
||||||
holdings?.sectors ?? {}
|
holdings?.sectors ?? {}
|
||||||
)) {
|
)) {
|
||||||
|
@ -598,10 +598,14 @@ export class DataProviderService {
|
|||||||
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
|
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
|
||||||
})
|
})
|
||||||
.map((lookupItem) => {
|
.map((lookupItem) => {
|
||||||
if (
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') ||
|
if (user.subscription.type === 'Premium') {
|
||||||
user.subscription.type === 'Premium'
|
lookupItem.dataProviderInfo.isPremium = false;
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
lookupItem.dataProviderInfo.name = undefined;
|
||||||
|
lookupItem.dataProviderInfo.url = undefined;
|
||||||
|
} else {
|
||||||
lookupItem.dataProviderInfo.isPremium = false;
|
lookupItem.dataProviderInfo.isPremium = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: true
|
isPremium: true,
|
||||||
|
name: 'EOD Historical Data',
|
||||||
|
url: 'https://eodhd.com'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,9 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Google Sheets',
|
||||||
|
url: 'https://docs.google.com/spreadsheets'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Rapid API',
|
||||||
|
url: 'https://rapidapi.com'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Yahoo Finance',
|
||||||
|
url: 'https://finance.yahoo.com'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export class ExchangeRateDataService {
|
|||||||
);
|
);
|
||||||
const lastDateString = dateStrings.reduce((a, b) => {
|
const lastDateString = dateStrings.reduce((a, b) => {
|
||||||
return a > b ? a : b;
|
return a > b ? a : b;
|
||||||
});
|
}, undefined);
|
||||||
|
|
||||||
let previousExchangeRate =
|
let previousExchangeRate =
|
||||||
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[
|
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[
|
||||||
|
@ -2,6 +2,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
|
Holding,
|
||||||
ScraperConfiguration,
|
ScraperConfiguration,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -61,7 +62,9 @@ export class SymbolProfileService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => {
|
||||||
|
return this.enhanceSymbolProfiles(symbolProfiles);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSymbolProfilesByIds(
|
public async getSymbolProfilesByIds(
|
||||||
@ -83,7 +86,9 @@ export class SymbolProfileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => {
|
||||||
|
return this.enhanceSymbolProfiles(symbolProfiles);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSymbolProfile({
|
public updateSymbolProfile({
|
||||||
@ -93,6 +98,7 @@ export class SymbolProfileService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
holdings,
|
||||||
name,
|
name,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
@ -108,6 +114,7 @@ export class SymbolProfileService {
|
|||||||
comment,
|
comment,
|
||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
|
holdings,
|
||||||
name,
|
name,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
@ -119,7 +126,7 @@ export class SymbolProfileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSymbols(
|
private enhanceSymbolProfiles(
|
||||||
symbolProfiles: (SymbolProfile & {
|
symbolProfiles: (SymbolProfile & {
|
||||||
_count: { Order: number };
|
_count: { Order: number };
|
||||||
Order?: {
|
Order?: {
|
||||||
@ -136,6 +143,7 @@ export class SymbolProfileService {
|
|||||||
symbolProfile?.countries as unknown as Prisma.JsonArray
|
symbolProfile?.countries as unknown as Prisma.JsonArray
|
||||||
),
|
),
|
||||||
dateOfFirstActivity: <Date>undefined,
|
dateOfFirstActivity: <Date>undefined,
|
||||||
|
holdings: this.getHoldings(symbolProfile),
|
||||||
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
||||||
sectors: this.getSectors(symbolProfile),
|
sectors: this.getSectors(symbolProfile),
|
||||||
symbolMapping: this.getSymbolMapping(symbolProfile)
|
symbolMapping: this.getSymbolMapping(symbolProfile)
|
||||||
@ -163,6 +171,14 @@ export class SymbolProfileService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(item.SymbolProfileOverrides.holdings as unknown as Holding[])
|
||||||
|
?.length > 0
|
||||||
|
) {
|
||||||
|
item.holdings = item.SymbolProfileOverrides
|
||||||
|
.holdings as unknown as Holding[];
|
||||||
|
}
|
||||||
|
|
||||||
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -199,6 +215,19 @@ export class SymbolProfileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHoldings(symbolProfile: SymbolProfile): Holding[] {
|
||||||
|
return ((symbolProfile?.holdings as Prisma.JsonArray) ?? []).map(
|
||||||
|
(holding) => {
|
||||||
|
const { name, weight } = holding as Prisma.JsonObject;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: (name as string) ?? UNKNOWN_KEY,
|
||||||
|
valueInBaseCurrency: weight as number
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private getScraperConfiguration(
|
private getScraperConfiguration(
|
||||||
symbolProfile: SymbolProfile
|
symbolProfile: SymbolProfile
|
||||||
): ScraperConfiguration {
|
): ScraperConfiguration {
|
||||||
|
18
apps/client/localhost.cert
Normal file
18
apps/client/localhost.cert
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC5TCCAc2gAwIBAgIJAJAMHOFnJ6oyMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
|
||||||
|
BAMMCWxvY2FsaG9zdDAeFw0yNDAyMjcxNTQ2MzFaFw0yNDAzMjgxNTQ2MzFaMBQx
|
||||||
|
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||||
|
ggEBAJ0hRjViikEKVIyukXR4CfuYVvFEFzB6AwAQ9Jrz2MseqpLacLTXFFAS54mp
|
||||||
|
iDuqPBzs9ta40mlSrqSBuAWKikpW5kTNnmqUnDZ6iSJezbYWx9YyULGqqb1q3C4/
|
||||||
|
5pH9m6NHJ+2uaUNKlDxYNKbntqs3drQEdxH9yv672Z53nvogTcf9jz6zjutEQGSV
|
||||||
|
HgVkCTTQmzf3+3st+VJ5D8JeYFR+tpZ6yleqgXFaTMtPZRfKLvTkQ+KeyCJLnsUJ
|
||||||
|
BQvdCKI0PGsG6d6ygXFmSePolD9KW3VTKKDPCsndID89vAnRWDj9UhzvycxmKpcF
|
||||||
|
GrUPH5+Pis1PM1R7OnAvnFygnyECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo
|
||||||
|
b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEAOdzrY3RTAPnBVAd3/GKIEkielYyOgKPnvd+RcOB+je8cXZY+vaxX
|
||||||
|
uEFP0526G00+kRZ2Tx9t0SmjoSpwg3lHUPMko0dIxgRlqISDAohdrEptHpcVujsD
|
||||||
|
ak88LLnAurr60JNjWX2wbEoJ18KLtqGSnATdmCgKwDPIN5a7+ssp44BGyzH6VYCg
|
||||||
|
wV3VjJl0pp4C5M0Jfu0p1FrQjzIVhgqR7JFYmvqIogWrGwYAQK/3XRXq8t48J5X3
|
||||||
|
IsfWiLAA2ZdCoWAnZ6PAGBOoGivtkJm967pHjd/28qYY6mQo4sN2ksEOjx6/YslF
|
||||||
|
2mOJdLV/DzqoggUsTpPEG0dRhzQLTGHKDQ==
|
||||||
|
-----END CERTIFICATE-----
|
28
apps/client/localhost.pem
Normal file
28
apps/client/localhost.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdIUY1YopBClSM
|
||||||
|
rpF0eAn7mFbxRBcwegMAEPSa89jLHqqS2nC01xRQEueJqYg7qjwc7PbWuNJpUq6k
|
||||||
|
gbgFiopKVuZEzZ5qlJw2eokiXs22FsfWMlCxqqm9atwuP+aR/ZujRyftrmlDSpQ8
|
||||||
|
WDSm57arN3a0BHcR/cr+u9med576IE3H/Y8+s47rREBklR4FZAk00Js39/t7LflS
|
||||||
|
eQ/CXmBUfraWespXqoFxWkzLT2UXyi705EPinsgiS57FCQUL3QiiNDxrBunesoFx
|
||||||
|
Zknj6JQ/Slt1UyigzwrJ3SA/PbwJ0Vg4/VIc78nMZiqXBRq1Dx+fj4rNTzNUezpw
|
||||||
|
L5xcoJ8hAgMBAAECggEAPU5YOEgELTA8oM8TjV+wdWuQsH2ilpVkSkhTR4nQkh+a
|
||||||
|
6cU0qDoqgLt/fySYNL9MyPRjso9V+SX7YdAC3paZMjwJh9q57lehQ1g33SMkG+Fz
|
||||||
|
gs0K0ucFZxQkaB8idN+ANAp1N7UO+ORGRe0cTeqmSNNRCxea5XgiFZVxaPS/IFOR
|
||||||
|
vVdXS1DulgwHh4H0ljKmkj7jc9zPBSc9ccW5Ml2q4a26Atu4IC/Mlp/DF4GNELbD
|
||||||
|
ebi9ZOZG33ip2bdhj3WX7NW9GJaaViKtVUpcpR6u+6BfvTXQ6xrqdoxXk9fnPzzf
|
||||||
|
sSoLPTt8yO4RavP1zQU22PhgIcWudhCiy/2Nv+uLqQKBgQDMPh1/xwdFl8yES8dy
|
||||||
|
f0xJyCDWPiB+xzGwcOAC90FrNZaqG0WWs3VHE4cilaWjCflvdR6aAEDEY68sZJhl
|
||||||
|
h+ChT/g61QLMOI+VIDQ1kJXKYgfS/B+BE1PZ0EkuymKFOvbNO8agHodB8CqnZaQh
|
||||||
|
bLuZaDnZ0JLK4im3KPt70+aKYwKBgQDE8s6xl0SLu+yJLo3VklCRD67Z8/jlvx2t
|
||||||
|
h3DF6NG8pB3QmvKdJWFKuLAWLGflJwbUW9Pt3hXkc0649KQrtiIYC0ZMh9KMaVCk
|
||||||
|
WmjS/n2eIUQZ7ZUlwFesi4p4iGynVBavIY8TJ6Y+K3TvsJgXP3IZ96r689PQJo8E
|
||||||
|
KbSeyYzFqwKBgGQTS4EAlJ+U8bEhMGj51veQCAbyChoUoFRD+n95h6RwbZKMKlzd
|
||||||
|
MenRt7VKfg6VJJNoX8Y1uYaBEaQ+5i1Zlsdz1718AhLu4+u+C9bzMXIo9ox63TTx
|
||||||
|
s3RWioVSxVNiwOtvDrQGQWAdvcioFPQLwyA34aDIgiTHDImimxbhjWThAoGAWOqW
|
||||||
|
Tq9QjxWk0Lpn5ohMP3GpK1VuhasnJvUDARb/uf8ORuPtrOz3Y9jGBvy9W0OnXbCn
|
||||||
|
mbiugZldbTtl8yYjdl+AuYSIlkPl2I3IzZl/9Shnqp0MvSJ9crT9KzXMeC8Knr6z
|
||||||
|
7Z30/BR6ksxTngtS5E5grzPt6Qe/gc2ich3kpEkCgYBfBHUhVIKVrDW/8a7U2679
|
||||||
|
Wj4i9us/M3iPMxcTv7/ZAg08TEvNBQYtvVQLu0NfDKXx8iGKJJ6evIYgNXVm2sPq
|
||||||
|
VzSgwGCALi1X0443amZU+mnX+/9RnBqyM+PJU8570mM83jqKlY/BEsi0aqxTioFG
|
||||||
|
an3xbjjN+Rq9iKLzmPxIMg==
|
||||||
|
-----END PRIVATE KEY-----
|
@ -163,8 +163,11 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"executor": "@nx/angular:dev-server",
|
"executor": "@nx/angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
|
"buildTarget": "client:build",
|
||||||
"proxyConfig": "apps/client/proxy.conf.json",
|
"proxyConfig": "apps/client/proxy.conf.json",
|
||||||
"buildTarget": "client:build"
|
"ssl": true,
|
||||||
|
"sslCert": "apps/client/localhost.cert",
|
||||||
|
"sslKey": "apps/client/localhost.pem"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development-de": {
|
"development-de": {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
import { paths } from '@ghostfolio/client/core/paths';
|
||||||
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -5,18 +7,6 @@ import { RouterModule, Routes, TitleStrategy } from '@angular/router';
|
|||||||
|
|
||||||
import { ModulePreloadService } from './core/module-preload.service';
|
import { ModulePreloadService } from './core/module-preload.service';
|
||||||
|
|
||||||
export const paths = {
|
|
||||||
about: $localize`about`,
|
|
||||||
faq: $localize`faq`,
|
|
||||||
features: $localize`features`,
|
|
||||||
license: $localize`license`,
|
|
||||||
markets: $localize`markets`,
|
|
||||||
pricing: $localize`pricing`,
|
|
||||||
privacyPolicy: $localize`privacy-policy`,
|
|
||||||
register: $localize`register`,
|
|
||||||
resources: $localize`resources`
|
|
||||||
};
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: paths.about,
|
path: paths.about,
|
||||||
@ -53,9 +43,12 @@ const routes: Routes = [
|
|||||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'demo',
|
canActivate: [AuthGuard],
|
||||||
loadChildren: () =>
|
loadComponent: () =>
|
||||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
import('./pages/demo/demo-page.component').then(
|
||||||
|
(c) => c.GfDemoPageComponent
|
||||||
|
),
|
||||||
|
path: 'demo'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.faq,
|
path: paths.faq,
|
||||||
@ -63,11 +56,13 @@ const routes: Routes = [
|
|||||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/features/features-page.component').then(
|
||||||
|
(c) => c.GfFeaturesPageComponent
|
||||||
|
),
|
||||||
path: paths.features,
|
path: paths.features,
|
||||||
loadChildren: () =>
|
title: $localize`Features`
|
||||||
import('./pages/features/features-page.module').then(
|
|
||||||
(m) => m.FeaturesPageModule
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
@ -75,9 +70,13 @@ const routes: Routes = [
|
|||||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/i18n/i18n-page.component').then(
|
||||||
|
(c) => c.GfI18nPageComponent
|
||||||
|
),
|
||||||
path: 'i18n',
|
path: 'i18n',
|
||||||
loadChildren: () =>
|
title: $localize`Internationalization`
|
||||||
import('./pages/i18n/i18n-page.module').then((m) => m.I18nPageModule)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.markets,
|
path: paths.markets,
|
||||||
@ -134,11 +133,12 @@ const routes: Routes = [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/webauthn/webauthn-page.component').then(
|
||||||
|
(c) => c.GfWebauthnPageComponent
|
||||||
|
),
|
||||||
path: 'webauthn',
|
path: 'webauthn',
|
||||||
loadChildren: () =>
|
title: $localize`Sign in`
|
||||||
import('./pages/webauthn/webauthn-page.module').then(
|
|
||||||
(m) => m.WebauthnPageModule
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'zen',
|
path: 'zen',
|
||||||
|
@ -1,33 +1,31 @@
|
|||||||
<header>
|
<header>
|
||||||
<div
|
@if (canCreateAccount || user?.systemMessage) {
|
||||||
*ngIf="canCreateAccount || user?.systemMessage"
|
<div class="info-message-container">
|
||||||
class="info-message-container"
|
<div class="info-message-inner-container position-fixed w-100">
|
||||||
>
|
<div class="align-items-center d-flex h-100 justify-content-center">
|
||||||
<div class="info-message-inner-container position-fixed w-100">
|
@if (canCreateAccount) {
|
||||||
<div class="align-items-center d-flex h-100 justify-content-center">
|
<a class="text-center" [routerLink]="routerLinkRegister">
|
||||||
<a
|
<div
|
||||||
*ngIf="canCreateAccount"
|
class="cursor-pointer d-inline-block info-message"
|
||||||
class="text-center"
|
(click)="onCreateAccount()"
|
||||||
[routerLink]="routerLinkRegister"
|
>
|
||||||
>
|
<span i18n>You are using the Live Demo.</span>
|
||||||
<div
|
<span class="a ml-2" i18n>Create Account</span>
|
||||||
class="cursor-pointer d-inline-block info-message"
|
</div></a
|
||||||
(click)="onCreateAccount()"
|
>
|
||||||
>
|
}
|
||||||
<span i18n>You are using the Live Demo.</span>
|
@if (!canCreateAccount && user?.systemMessage) {
|
||||||
<span class="a ml-2" i18n>Create Account</span>
|
<div
|
||||||
</div></a
|
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||||
>
|
(click)="onClickSystemMessage()"
|
||||||
<div
|
>
|
||||||
*ngIf="!canCreateAccount && user?.systemMessage"
|
{{ user.systemMessage.message }}
|
||||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
</div>
|
||||||
(click)="onClickSystemMessage()"
|
}
|
||||||
>
|
|
||||||
{{ user.systemMessage.message }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<gf-header
|
<gf-header
|
||||||
class="position-fixed w-100"
|
class="position-fixed w-100"
|
||||||
@ -45,146 +43,159 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer *ngIf="showFooter" class="d-flex justify-content-center py-4 w-100">
|
@if (showFooter) {
|
||||||
<div class="container">
|
<footer class="d-flex justify-content-center py-4 w-100">
|
||||||
<div class="mb-3 row">
|
<div class="container">
|
||||||
<div class="col-sm">
|
<div class="mb-3 row">
|
||||||
<a [routerLink]="['/']"><gf-logo /></a>
|
<div class="col-sm">
|
||||||
</div>
|
<a [routerLink]="['/']"><gf-logo /></a>
|
||||||
<div class="col-sm">
|
</div>
|
||||||
<div class="h6 mt-2" i18n>Personal Finance</div>
|
<div class="col-sm">
|
||||||
<ul class="list-unstyled">
|
<div class="h6 mt-2" i18n>Personal Finance</div>
|
||||||
<li *ngIf="hasPermissionToAccessFearAndGreedIndex">
|
<ul class="list-unstyled">
|
||||||
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
|
@if (hasPermissionToAccessFearAndGreedIndex) {
|
||||||
</li>
|
<li>
|
||||||
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
|
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
}
|
||||||
<div class="col-sm">
|
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
|
||||||
<div class="h6 mt-2">Ghostfolio</div>
|
</ul>
|
||||||
<ul class="list-unstyled">
|
</div>
|
||||||
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
|
<div class="col-sm">
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
<div class="h6 mt-2">Ghostfolio</div>
|
||||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
<ul class="list-unstyled">
|
||||||
</li>
|
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
|
||||||
<li>
|
@if (hasPermissionForSubscription) {
|
||||||
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
|
<li>
|
||||||
</li>
|
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||||
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
|
</li>
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
}
|
||||||
<a i18n [routerLink]="routerLinkFaq"
|
|
||||||
>Frequently Asked Questions (FAQ)</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
|
|
||||||
</li>
|
|
||||||
<li *ngIf="hasPermissionForStatistics">
|
|
||||||
<a [routerLink]="['/open']">Open Startup</a>
|
|
||||||
</li>
|
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
|
||||||
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
|
|
||||||
</li>
|
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
|
||||||
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
|
|
||||||
>Privacy Policy</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
|
||||||
<a
|
|
||||||
class="align-items-baseline d-flex"
|
|
||||||
href="https://status.ghostfol.io"
|
|
||||||
target="_blank"
|
|
||||||
title="Ghostfolio Status"
|
|
||||||
>Status<ion-icon class="ml-1" name="open-outline"
|
|
||||||
/></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="h6 mt-2" i18n>Community</div>
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="align-items-baseline d-flex"
|
|
||||||
href="https://github.com/ghostfolio/ghostfolio"
|
|
||||||
target="_blank"
|
|
||||||
title="Find Ghostfolio on GitHub"
|
|
||||||
>GitHub<ion-icon class="ml-1" name="open-outline"
|
|
||||||
/></a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="align-items-baseline d-flex"
|
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
|
||||||
target="_blank"
|
|
||||||
title="Join the Ghostfolio Slack community"
|
|
||||||
>Slack<ion-icon class="ml-1" name="open-outline"
|
|
||||||
/></a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="align-items-baseline d-flex"
|
|
||||||
href="https://twitter.com/ghostfolio_"
|
|
||||||
target="_blank"
|
|
||||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
|
||||||
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
|
|
||||||
/></a>
|
|
||||||
</li>
|
|
||||||
<li> </li>
|
|
||||||
<li>
|
|
||||||
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="../en" title="Ghostfolio in English">English</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="../es" title="Ghostfolio in Español">Español</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="../fr" title="Ghostfolio en Français">Français</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
|
||||||
</li>
|
|
||||||
<!--
|
|
||||||
<li>
|
<li>
|
||||||
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
|
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
|
||||||
|
</li>
|
||||||
|
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
|
||||||
|
@if (hasPermissionForSubscription) {
|
||||||
|
<li>
|
||||||
|
<a i18n [routerLink]="routerLinkFaq"
|
||||||
|
>Frequently Asked Questions (FAQ)</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li>
|
||||||
|
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
|
||||||
|
</li>
|
||||||
|
@if (hasPermissionForStatistics) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="['/open']">Open Startup</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (hasPermissionForSubscription) {
|
||||||
|
<li>
|
||||||
|
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (hasPermissionForSubscription) {
|
||||||
|
<li>
|
||||||
|
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (hasPermissionForSubscription) {
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="align-items-baseline d-flex"
|
||||||
|
href="https://status.ghostfol.io"
|
||||||
|
target="_blank"
|
||||||
|
title="Ghostfolio Status"
|
||||||
|
>Status<ion-icon class="ml-1" name="open-outline"
|
||||||
|
/></a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="h6 mt-2" i18n>Community</div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="align-items-baseline d-flex"
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio"
|
||||||
|
target="_blank"
|
||||||
|
title="Find Ghostfolio on GitHub"
|
||||||
|
>GitHub<ion-icon class="ml-1" name="open-outline"
|
||||||
|
/></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="align-items-baseline d-flex"
|
||||||
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
|
target="_blank"
|
||||||
|
title="Join the Ghostfolio Slack community"
|
||||||
|
>Slack<ion-icon class="ml-1" name="open-outline"
|
||||||
|
/></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="align-items-baseline d-flex"
|
||||||
|
href="https://twitter.com/ghostfolio_"
|
||||||
|
target="_blank"
|
||||||
|
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||||
|
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
|
||||||
|
/></a>
|
||||||
|
</li>
|
||||||
|
<li> </li>
|
||||||
|
<li>
|
||||||
|
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../en" title="Ghostfolio in English">English</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../es" title="Ghostfolio in Español">Español</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../fr" title="Ghostfolio en Français">Français</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
||||||
|
</li>
|
||||||
|
<!--
|
||||||
|
<li>
|
||||||
|
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
|
<li>
|
||||||
|
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||||
</li>
|
</li>
|
||||||
-->
|
|
||||||
<li>
|
|
||||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
|
||||||
</li>
|
|
||||||
<!--
|
|
||||||
<li>
|
<li>
|
||||||
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||||
</li>
|
</li>
|
||||||
-->
|
<!--
|
||||||
<!--
|
<li>
|
||||||
<li>
|
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
||||||
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
</li>
|
||||||
</li>
|
-->
|
||||||
-->
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
© 2021 - {{ currentYear }}
|
||||||
|
<a href="https://ghostfol.io">Ghostfolio</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center text-muted">
|
||||||
|
<div class="col">
|
||||||
|
<small i18n
|
||||||
|
>The risk of loss in trading can be substantial. It is not advisable
|
||||||
|
to invest money you may need in the short term.</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</footer>
|
||||||
<div class="row text-center">
|
}
|
||||||
<div class="col">
|
|
||||||
© 2021 - {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row text-center text-muted">
|
|
||||||
<div class="col">
|
|
||||||
<small i18n
|
|
||||||
>The risk of loss in trading can be substantial. It is not advisable
|
|
||||||
to invest money you may need in the short term.</small
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/holding-detail-dialog/holding-detail-dialog.component';
|
||||||
|
import { HoldingDetailDialogParams } from '@ghostfolio/client/components/holding-detail-dialog/interfaces/interfaces';
|
||||||
import { getCssVariable } from '@ghostfolio/common/helper';
|
import { getCssVariable } from '@ghostfolio/common/helper';
|
||||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -13,13 +15,21 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
NavigationEnd,
|
||||||
|
PRIMARY_OUTLET,
|
||||||
|
Router
|
||||||
|
} from '@angular/router';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { filter, takeUntil } from 'rxjs/operators';
|
import { filter, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataService } from './services/data.service';
|
import { DataService } from './services/data.service';
|
||||||
|
import { ImpersonationStorageService } from './services/impersonation-storage.service';
|
||||||
import { TokenStorageService } from './services/token-storage.service';
|
import { TokenStorageService } from './services/token-storage.service';
|
||||||
import { UserService } from './services/user/user.service';
|
import { UserService } from './services/user/user.service';
|
||||||
|
|
||||||
@ -38,6 +48,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
public currentRoute: string;
|
public currentRoute: string;
|
||||||
public currentYear = new Date().getFullYear();
|
public currentYear = new Date().getFullYear();
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public hasInfoMessage: boolean;
|
public hasInfoMessage: boolean;
|
||||||
public hasPermissionForStatistics: boolean;
|
public hasPermissionForStatistics: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
@ -67,7 +78,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private title: Title,
|
private title: Title,
|
||||||
private tokenStorageService: TokenStorageService,
|
private tokenStorageService: TokenStorageService,
|
||||||
@ -75,6 +89,21 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
) {
|
) {
|
||||||
this.initializeTheme();
|
this.initializeTheme();
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
|
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (
|
||||||
|
params['dataSource'] &&
|
||||||
|
params['holdingDetailDialog'] &&
|
||||||
|
params['symbol']
|
||||||
|
) {
|
||||||
|
this.openHoldingDetailDialog({
|
||||||
|
dataSource: params['dataSource'],
|
||||||
|
symbol: params['symbol']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
@ -96,6 +125,13 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
permissions.enableFearAndGreedIndex
|
permissions.enableFearAndGreedIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((impersonationId) => {
|
||||||
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
});
|
||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
@ -197,6 +233,55 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openHoldingDetailDialog({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(GfHoldingDetailDialogComponent, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: <HoldingDetailDialogParams>{
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
this.user?.permissions,
|
||||||
|
permissions.reportDataGlitch
|
||||||
|
),
|
||||||
|
locale: this.user?.settings?.locale
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: {
|
||||||
|
dataSource: null,
|
||||||
|
holdingDetailDialog: null,
|
||||||
|
symbol: null
|
||||||
|
},
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
relativeTo: this.route
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private toggleTheme(isDarkTheme: boolean) {
|
private toggleTheme(isDarkTheme: boolean) {
|
||||||
const themeColor = getCssVariable(
|
const themeColor = getCssVariable(
|
||||||
isDarkTheme ? '--dark-background' : '--light-background'
|
isDarkTheme ? '--dark-background' : '--light-background'
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { GfLogoComponent } from '@ghostfolio/ui/logo';
|
import { GfLogoComponent } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { Platform } from '@angular/cdk/platform';
|
import { Platform } from '@angular/cdk/platform';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import {
|
||||||
|
provideHttpClient,
|
||||||
|
withInterceptorsFromDi
|
||||||
|
} from '@angular/common/http';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
@ -45,7 +48,6 @@ export function NgxStripeFactory(): string {
|
|||||||
GfHeaderModule,
|
GfHeaderModule,
|
||||||
GfLogoComponent,
|
GfLogoComponent,
|
||||||
GfSubscriptionInterstitialDialogModule,
|
GfSubscriptionInterstitialDialogModule,
|
||||||
HttpClientModule,
|
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
@ -63,6 +65,7 @@ export function NgxStripeFactory(): string {
|
|||||||
authInterceptorProviders,
|
authInterceptorProviders,
|
||||||
httpResponseInterceptorProviders,
|
httpResponseInterceptorProviders,
|
||||||
LanguageService,
|
LanguageService,
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
{
|
{
|
||||||
provide: DateAdapter,
|
provide: DateAdapter,
|
||||||
useClass: CustomDateAdapter,
|
useClass: CustomDateAdapter,
|
||||||
|
@ -1,67 +1,71 @@
|
|||||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
<div class="overflow-x-auto">
|
||||||
<ng-container matColumnDef="alias">
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
<ng-container matColumnDef="alias">
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||||
{{ element.alias }}
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
</td>
|
{{ element.alias }}
|
||||||
</ng-container>
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="grantee">
|
<ng-container matColumnDef="grantee">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
{{ element.grantee }}
|
{{ element.grantee }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||||
<div class="align-items-center d-flex">
|
<div class="align-items-center d-flex">
|
||||||
@if (element.permissions.includes('READ')) {
|
@if (element.permissions.includes('READ')) {
|
||||||
<ion-icon class="mr-1" name="lock-open-outline" />
|
<ion-icon class="mr-1" name="lock-open-outline" />
|
||||||
<ng-container i18n>View</ng-container>
|
<ng-container i18n>View</ng-container>
|
||||||
} @else if (element.permissions.includes('READ_RESTRICTED')) {
|
} @else if (element.permissions.includes('READ_RESTRICTED')) {
|
||||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||||
<ng-container i18n>Restricted view</ng-container>
|
<ng-container i18n>Restricted view</ng-container>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="details">
|
||||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||||
|
@if (element.type === 'PUBLIC') {
|
||||||
|
<div class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-1" name="link-outline" />
|
||||||
|
<a
|
||||||
|
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
|
||||||
|
target="_blank"
|
||||||
|
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="details">
|
<ng-container matColumnDef="actions" stickyEnd>
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
|
||||||
<div *ngIf="element.type === 'PUBLIC'" class="align-items-center d-flex">
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
<ion-icon class="mr-1" name="link-outline" />
|
<button
|
||||||
<a
|
class="mx-1 no-min-width px-2"
|
||||||
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
|
mat-button
|
||||||
target="_blank"
|
[matMenuTriggerFor]="transactionMenu"
|
||||||
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
</div>
|
<ion-icon name="ellipsis-horizontal" />
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="actions" stickyEnd>
|
|
||||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
|
||||||
|
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
|
||||||
<button
|
|
||||||
class="mx-1 no-min-width px-2"
|
|
||||||
mat-button
|
|
||||||
[matMenuTriggerFor]="transactionMenu"
|
|
||||||
(click)="$event.stopPropagation()"
|
|
||||||
>
|
|
||||||
<ion-icon name="ellipsis-horizontal" />
|
|
||||||
</button>
|
|
||||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
|
||||||
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
|
||||||
<ng-container i18n>Revoke</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||||
</td>
|
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||||
</ng-container>
|
<ng-container i18n>Revoke</ng-container>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/create-account-balance.dto';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -95,19 +96,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAddAccountBalance({
|
public onAddAccountBalance(accountBalance: CreateAccountBalanceDto) {
|
||||||
balance,
|
|
||||||
date
|
|
||||||
}: {
|
|
||||||
balance: number;
|
|
||||||
date: Date;
|
|
||||||
}) {
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.postAccountBalance({
|
.postAccountBalance(accountBalance)
|
||||||
balance,
|
|
||||||
date,
|
|
||||||
accountId: this.data.accountId
|
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.fetchAccount();
|
this.fetchAccount();
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="
|
[hasPermissionToExportActivities]="
|
||||||
!data.hasImpersonationId && !user.settings.isRestrictedView
|
!data.hasImpersonationId && !user.settings.isRestrictedView
|
||||||
"
|
"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user