From 5d1f1b452a22e14e089be0fe308c5753e36fc43b Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Sun, 18 Apr 2021 19:06:54 +0200 Subject: [PATCH] Simplify initial project setup (#12) * Simplify initial project setup * Added a validation for environment variables * Added support for feature flags to simplify the initial project setup * Add configuration service to test * Optimize data gathering and exchange rate calculation (#14) * Clean up changelog --- .env | 7 --- CHANGELOG.md | 4 ++ apps/api/src/app/admin/admin.module.ts | 2 + apps/api/src/app/app.module.ts | 2 + apps/api/src/app/auth/auth.controller.ts | 10 ++-- apps/api/src/app/auth/auth.module.ts | 2 + apps/api/src/app/auth/auth.service.ts | 6 ++- apps/api/src/app/auth/google.strategy.ts | 14 +++-- apps/api/src/app/auth/jwt.strategy.ts | 4 +- .../app/experimental/experimental.module.ts | 2 + apps/api/src/app/info/info.module.ts | 3 +- apps/api/src/app/info/info.service.ts | 10 ++++ .../info/interfaces/info-item.interface.ts | 1 + apps/api/src/app/order/order.module.ts | 2 + .../api/src/app/portfolio/portfolio.module.ts | 2 + .../src/app/redis-cache/redis-cache.module.ts | 13 ++--- .../app/redis-cache/redis-cache.service.ts | 11 +++- apps/api/src/app/symbol/symbol.module.ts | 2 + apps/api/src/app/user/user.module.ts | 3 +- apps/api/src/app/user/user.service.ts | 20 +++++-- apps/api/src/models/portfolio.spec.ts | 4 ++ .../api/src/services/configuration.service.ts | 32 ++++++++++++ .../src/services/data-gathering.service.ts | 50 +++++++++++------- .../alpha-vantage/alpha-vantage.service.ts | 19 ++++--- .../rakuten-rapid-api.service.ts | 9 +++- .../services/exchange-rate-data.service.ts | 52 +++++++++++++------ .../interfaces/environment.interface.ts | 18 +++++++ apps/client/src/app/app.component.html | 1 + apps/client/src/app/app.component.ts | 10 ++-- .../app/components/header/header.component.ts | 13 ++++- .../performance-chart-dialog.html | 2 +- .../app/pages/admin/admin-page.component.ts | 16 ++++-- .../src/app/pages/admin/admin-page.html | 11 ++-- .../src/app/pages/home/home-page.component.ts | 21 +++++--- .../app/pages/login/login-page.component.ts | 6 ++- ...ogin-with-access-token-dialog.component.ts | 2 - .../login-with-access-token-dialog.html | 16 +++--- .../resources/resources-page.component.ts | 35 ++----------- .../app/pages/resources/resources-page.html | 11 +--- libs/helper/src/lib/config.ts | 13 +---- libs/helper/src/lib/permissions.ts | 4 +- package.json | 1 + yarn.lock | 5 ++ 43 files changed, 310 insertions(+), 161 deletions(-) create mode 100644 apps/api/src/services/configuration.service.ts create mode 100644 apps/api/src/services/interfaces/environment.interface.ts diff --git a/.env b/.env index 9c4f9789a..86953fa9b 100644 --- a/.env +++ b/.env @@ -1,8 +1,6 @@ COMPOSE_PROJECT_NAME=ghostfolio-development # CACHE -CACHE_TTL=1 -MAX_ITEM_IN_CACHE=9999 REDIS_HOST=localhost REDIS_PORT=6379 @@ -14,10 +12,5 @@ POSTGRES_DB=ghostfolio-db ACCESS_TOKEN_SALT=GHOSTFOLIO ALPHA_VANTAGE_API_KEY= DATABASE_URL=postgresql://user:password@localhost:5432/ghostfolio-db?sslmode=prefer -GOOGLE_CLIENT_ID=test -GOOGLE_SECRET=test -IS_DEVELOPMENT_MODE=true JWT_SECRET_KEY=123456 PORT=3333 -RAKUTEN_RAPID_API_KEY= -ROOT_URL=http://localhost:4200 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb8736c0..9326c2eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the license to the about page +- Added a validation for environment variables +- Added support for feature flags to simplify the initial project setup ### Changed - Changed the about page for the new license +- Optimized the data management for historical data +- Optimized the exchange rate service ## 0.85.0 - 16.04.2021 diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts index 8d34f8eea..91d9d9789 100644 --- a/apps/api/src/app/admin/admin.module.ts +++ b/apps/api/src/app/admin/admin.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ConfigurationService } from '../../services/configuration.service'; import { DataGatheringService } from '../../services/data-gathering.service'; import { DataProviderService } from '../../services/data-provider.service'; import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service'; @@ -16,6 +17,7 @@ import { AdminService } from './admin.service'; providers: [ AdminService, AlphaVantageService, + ConfigurationService, DataGatheringService, DataProviderService, ExchangeRateDataService, diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 244dddeca..53a30f3b1 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { ServeStaticModule } from '@nestjs/serve-static'; +import { ConfigurationService } from '../services/configuration.service'; import { CronService } from '../services/cron.service'; import { DataGatheringService } from '../services/data-gathering.service'; import { DataProviderService } from '../services/data-provider.service'; @@ -59,6 +60,7 @@ import { UserModule } from './user/user.module'; controllers: [AppController], providers: [ AlphaVantageService, + ConfigurationService, CronService, DataGatheringService, DataProviderService, diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index bf68294ec..c01d11064 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -10,11 +10,15 @@ import { import { AuthGuard } from '@nestjs/passport'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { ConfigurationService } from '../../services/configuration.service'; import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { - public constructor(private readonly authService: AuthService) {} + public constructor( + private readonly authService: AuthService, + private readonly configurationService: ConfigurationService + ) {} @Get('anonymous/:accessToken') public async accessTokenLogin(@Param('accessToken') accessToken: string) { @@ -44,9 +48,9 @@ export class AuthController { const jwt: string = req.user.jwt; if (jwt) { - res.redirect(`${process.env.ROOT_URL}/auth/${jwt}`); + res.redirect(`${this.configurationService.get('ROOT_URL')}/auth/${jwt}`); } else { - res.redirect(`${process.env.ROOT_URL}/auth`); + res.redirect(`${this.configurationService.get('ROOT_URL')}/auth`); } } } diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index 4d419757b..664aa7915 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import { ConfigurationService } from '../../services/configuration.service'; import { PrismaService } from '../../services/prisma.service'; import { UserService } from '../user/user.service'; import { AuthController } from './auth.controller'; @@ -18,6 +19,7 @@ import { JwtStrategy } from './jwt.strategy'; ], providers: [ AuthService, + ConfigurationService, GoogleStrategy, JwtStrategy, PrismaService, diff --git a/apps/api/src/app/auth/auth.service.ts b/apps/api/src/app/auth/auth.service.ts index ba248be14..946e44f4d 100644 --- a/apps/api/src/app/auth/auth.service.ts +++ b/apps/api/src/app/auth/auth.service.ts @@ -1,13 +1,15 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { ConfigurationService } from '../../services/configuration.service'; import { UserService } from '../user/user.service'; import { ValidateOAuthLoginParams } from './interfaces/interfaces'; @Injectable() export class AuthService { public constructor( - private jwtService: JwtService, + private readonly configurationService: ConfigurationService, + private readonly jwtService: JwtService, private readonly userService: UserService ) {} @@ -16,7 +18,7 @@ export class AuthService { try { const hashedAccessToken = this.userService.createAccessToken( accessToken, - process.env.ACCESS_TOKEN_SALT + this.configurationService.get('ACCESS_TOKEN_SALT') ); const [user] = await this.userService.users({ diff --git a/apps/api/src/app/auth/google.strategy.ts b/apps/api/src/app/auth/google.strategy.ts index d3c7b0a07..8412dc37c 100644 --- a/apps/api/src/app/auth/google.strategy.ts +++ b/apps/api/src/app/auth/google.strategy.ts @@ -3,15 +3,21 @@ import { PassportStrategy } from '@nestjs/passport'; import { Provider } from '@prisma/client'; import { Strategy } from 'passport-google-oauth20'; +import { ConfigurationService } from '../../services/configuration.service'; import { AuthService } from './auth.service'; @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { - public constructor(private readonly authService: AuthService) { + public constructor( + private readonly authService: AuthService, + readonly configurationService: ConfigurationService + ) { super({ - callbackURL: `${process.env.ROOT_URL}/api/auth/google/callback`, - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_SECRET, + callbackURL: `${configurationService.get( + 'ROOT_URL' + )}/api/auth/google/callback`, + clientID: configurationService.get('GOOGLE_CLIENT_ID'), + clientSecret: configurationService.get('GOOGLE_SECRET'), passReqToCallback: true, scope: ['email', 'profile'] }); diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts index 039ed15dc..967dbcce9 100644 --- a/apps/api/src/app/auth/jwt.strategy.ts +++ b/apps/api/src/app/auth/jwt.strategy.ts @@ -2,18 +2,20 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigurationService } from '../../services/configuration.service'; import { PrismaService } from '../../services/prisma.service'; import { UserService } from '../user/user.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { public constructor( + readonly configurationService: ConfigurationService, private prisma: PrismaService, private readonly userService: UserService ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: process.env.JWT_SECRET_KEY + secretOrKey: configurationService.get('JWT_SECRET_KEY') }); } diff --git a/apps/api/src/app/experimental/experimental.module.ts b/apps/api/src/app/experimental/experimental.module.ts index 155893f7f..e92fd0073 100644 --- a/apps/api/src/app/experimental/experimental.module.ts +++ b/apps/api/src/app/experimental/experimental.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ConfigurationService } from '../../services/configuration.service'; import { DataProviderService } from '../../services/data-provider.service'; import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service'; import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; @@ -15,6 +16,7 @@ import { ExperimentalService } from './experimental.service'; controllers: [ExperimentalController], providers: [ AlphaVantageService, + ConfigurationService, DataProviderService, ExchangeRateDataService, ExperimentalService, diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index 723fed724..3c82b8a13 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import { ConfigurationService } from '../../services/configuration.service'; import { PrismaService } from '../../services/prisma.service'; import { InfoController } from './info.controller'; import { InfoService } from './info.service'; @@ -13,6 +14,6 @@ import { InfoService } from './info.service'; }) ], controllers: [InfoController], - providers: [InfoService, PrismaService] + providers: [ConfigurationService, InfoService, PrismaService] }) export class InfoModule {} diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index c34fb2be2..88057a348 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -1,7 +1,9 @@ +import { permissions } from '@ghostfolio/helper'; import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Currency } from '@prisma/client'; +import { ConfigurationService } from '../../services/configuration.service'; import { PrismaService } from '../../services/prisma.service'; import { InfoItem } from './interfaces/info-item.interface'; @@ -10,6 +12,7 @@ export class InfoService { private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f'; public constructor( + private readonly configurationService: ConfigurationService, private jwtService: JwtService, private prisma: PrismaService ) {} @@ -20,7 +23,14 @@ export class InfoService { select: { id: true, name: true } }); + const globalPermissions: string[] = []; + + if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { + globalPermissions.push(permissions.useSocialLogin); + } + return { + globalPermissions, platforms, currencies: Object.values(Currency), demoAuthToken: this.getDemoAuthToken(), diff --git a/apps/api/src/app/info/interfaces/info-item.interface.ts b/apps/api/src/app/info/interfaces/info-item.interface.ts index 8a39325a9..58712e297 100644 --- a/apps/api/src/app/info/interfaces/info-item.interface.ts +++ b/apps/api/src/app/info/interfaces/info-item.interface.ts @@ -3,6 +3,7 @@ import { Currency } from '@prisma/client'; export interface InfoItem { currencies: Currency[]; demoAuthToken: string; + globalPermissions: string[]; lastDataGathering?: Date; message?: { text: string; diff --git a/apps/api/src/app/order/order.module.ts b/apps/api/src/app/order/order.module.ts index 9702bb8d6..c430a8ebf 100644 --- a/apps/api/src/app/order/order.module.ts +++ b/apps/api/src/app/order/order.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ConfigurationService } from '../../services/configuration.service'; import { DataGatheringService } from '../../services/data-gathering.service'; import { DataProviderService } from '../../services/data-provider.service'; import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service'; @@ -18,6 +19,7 @@ import { OrderService } from './order.service'; providers: [ AlphaVantageService, CacheService, + ConfigurationService, DataGatheringService, DataProviderService, ImpersonationService, diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index fc078da23..e740259c7 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ConfigurationService } from '../../services/configuration.service'; import { DataGatheringService } from '../../services/data-gathering.service'; import { DataProviderService } from '../../services/data-provider.service'; import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service'; @@ -22,6 +23,7 @@ import { PortfolioService } from './portfolio.service'; providers: [ AlphaVantageService, CacheService, + ConfigurationService, DataGatheringService, DataProviderService, ExchangeRateDataService, diff --git a/apps/api/src/app/redis-cache/redis-cache.module.ts b/apps/api/src/app/redis-cache/redis-cache.module.ts index b363bb10e..9e79a7b25 100644 --- a/apps/api/src/app/redis-cache/redis-cache.module.ts +++ b/apps/api/src/app/redis-cache/redis-cache.module.ts @@ -2,6 +2,7 @@ import { CacheModule, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import * as redisStore from 'cache-manager-redis-store'; +import { ConfigurationService } from '../../services/configuration.service'; import { RedisCacheService } from './redis-cache.service'; @Module({ @@ -9,16 +10,16 @@ import { RedisCacheService } from './redis-cache.service'; CacheModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - host: configService.get('REDIS_HOST'), - max: configService.get('MAX_ITEM_IN_CACHE'), - port: configService.get('REDIS_PORT'), + useFactory: async (configurationService: ConfigurationService) => ({ + host: configurationService.get('REDIS_HOST'), + max: configurationService.get('MAX_ITEM_IN_CACHE'), + port: configurationService.get('REDIS_PORT'), store: redisStore, - ttl: configService.get('CACHE_TTL') + ttl: configurationService.get('CACHE_TTL') }) }) ], - providers: [RedisCacheService], + providers: [ConfigurationService, RedisCacheService], exports: [RedisCacheService] }) export class RedisCacheModule {} diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts index 446db9967..1da9f06a9 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.ts @@ -1,9 +1,14 @@ import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common'; import { Cache } from 'cache-manager'; +import { ConfigurationService } from '../../services/configuration.service'; + @Injectable() export class RedisCacheService { - public constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {} + public constructor( + @Inject(CACHE_MANAGER) private readonly cache: Cache, + private readonly configurationService: ConfigurationService + ) {} public async get(key: string): Promise { return await this.cache.get(key); @@ -18,6 +23,8 @@ export class RedisCacheService { } public async set(key: string, value: string) { - await this.cache.set(key, value, { ttl: Number(process.env.CACHE_TTL) }); + await this.cache.set(key, value, { + ttl: this.configurationService.get('CACHE_TTL') + }); } } diff --git a/apps/api/src/app/symbol/symbol.module.ts b/apps/api/src/app/symbol/symbol.module.ts index c8d2ad515..26164c0a1 100644 --- a/apps/api/src/app/symbol/symbol.module.ts +++ b/apps/api/src/app/symbol/symbol.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ConfigurationService } from '../../services/configuration.service'; import { DataProviderService } from '../../services/data-provider.service'; import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service'; import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; @@ -13,6 +14,7 @@ import { SymbolService } from './symbol.service'; controllers: [SymbolController], providers: [ AlphaVantageService, + ConfigurationService, DataProviderService, PrismaService, RakutenRapidApiService, diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index e149bd789..7dfc3b4fe 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import { ConfigurationService } from '../../services/configuration.service'; import { PrismaService } from '../../services/prisma.service'; import { UserController } from './user.controller'; import { UserService } from './user.service'; @@ -13,6 +14,6 @@ import { UserService } from './user.service'; }) ], controllers: [UserController], - providers: [PrismaService, UserService] + providers: [ConfigurationService, PrismaService, UserService] }) export class UserModule {} diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index f586bd4a3..bd4ac80a3 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; import { Currency, Prisma, Provider, User } from '@prisma/client'; import { add } from 'date-fns'; -import { locale, resetHours } from 'libs/helper/src'; +import { locale, permissions, resetHours } from 'libs/helper/src'; import { getPermissions } from 'libs/helper/src'; +import { ConfigurationService } from '../../services/configuration.service'; import { PrismaService } from '../../services/prisma.service'; import { UserWithSettings } from '../interfaces/user-with-settings'; import { User as IUser } from './interfaces/user.interface'; @@ -14,7 +15,10 @@ const crypto = require('crypto'); export class UserService { public static DEFAULT_CURRENCY = Currency.USD; - public constructor(private prisma: PrismaService) {} + public constructor( + private readonly configurationService: ConfigurationService, + private prisma: PrismaService + ) {} public async getUser({ alias, @@ -30,6 +34,16 @@ export class UserService { where: { GranteeUser: { id } } }); + const currentPermissions = getPermissions(role); + + if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + currentPermissions.push(permissions.accessFearAndGreedIndex); + } + + if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { + currentPermissions.push(permissions.useSocialLogin); + } + return { alias, id, @@ -39,7 +53,7 @@ export class UserService { id: accessItem.id }; }), - permissions: getPermissions(role), + permissions: currentPermissions, settings: { baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY, locale diff --git a/apps/api/src/models/portfolio.spec.ts b/apps/api/src/models/portfolio.spec.ts index ad9dad36e..fbaea9329 100644 --- a/apps/api/src/models/portfolio.spec.ts +++ b/apps/api/src/models/portfolio.spec.ts @@ -4,6 +4,7 @@ import { baseCurrency } from 'libs/helper/src'; import { getYesterday } from 'libs/helper/src'; import { getUtc } from 'libs/helper/src'; +import { ConfigurationService } from '../services/configuration.service'; import { DataProviderService } from '../services/data-provider.service'; import { AlphaVantageService } from '../services/data-provider/alpha-vantage/alpha-vantage.service'; import { RakutenRapidApiService } from '../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; @@ -15,6 +16,7 @@ import { Portfolio } from './portfolio'; describe('Portfolio', () => { let alphaVantageService: AlphaVantageService; + let configurationService: ConfigurationService; let dataProviderService: DataProviderService; let exchangeRateDataService: ExchangeRateDataService; let portfolio: Portfolio; @@ -28,6 +30,7 @@ describe('Portfolio', () => { imports: [], providers: [ AlphaVantageService, + ConfigurationService, DataProviderService, ExchangeRateDataService, PrismaService, @@ -38,6 +41,7 @@ describe('Portfolio', () => { }).compile(); alphaVantageService = app.get(AlphaVantageService); + configurationService = app.get(ConfigurationService); dataProviderService = app.get(DataProviderService); exchangeRateDataService = app.get( ExchangeRateDataService diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts new file mode 100644 index 000000000..2ffa7cbf7 --- /dev/null +++ b/apps/api/src/services/configuration.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { bool, cleanEnv, num, port, str } from 'envalid'; + +import { Environment } from './interfaces/environment.interface'; + +@Injectable() +export class ConfigurationService { + private readonly environmentConfiguration: Environment; + + public constructor() { + this.environmentConfiguration = cleanEnv(process.env, { + ACCESS_TOKEN_SALT: str(), + ALPHA_VANTAGE_API_KEY: str({ default: '' }), + CACHE_TTL: num({ default: 1 }), + ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), + ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), + GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }), + GOOGLE_SECRET: str({ default: 'dummySecret' }), + JWT_SECRET_KEY: str({}), + MAX_ITEM_IN_CACHE: num({ default: 9999 }), + PORT: port({ default: 3333 }), + RAKUTEN_RAPID_API_KEY: str({ default: '' }), + REDIS_HOST: str({ default: 'localhost' }), + REDIS_PORT: port({ default: 6379 }), + ROOT_URL: str({ default: 'http://localhost:4200' }) + }); + } + + public get(key: K): Environment[K] { + return this.environmentConfiguration[key]; + } +} diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 1eef1d258..d403b83d2 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -11,13 +11,15 @@ import { import { benchmarks, currencyPairs } from 'libs/helper/src'; import { getUtc, resetHours } from 'libs/helper/src'; +import { ConfigurationService } from './configuration.service'; import { DataProviderService } from './data-provider.service'; import { PrismaService } from './prisma.service'; @Injectable() export class DataGatheringService { public constructor( - private dataProviderService: DataProviderService, + private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, private prisma: PrismaService ) {} @@ -64,9 +66,11 @@ export class DataGatheringService { } public async gatherMax() { - const isDataGatheringNeeded = await this.isDataGatheringNeeded(); + const isDataGatheringLocked = await this.prisma.property.findUnique({ + where: { key: 'LOCKED_DATA_GATHERING' } + }); - if (isDataGatheringNeeded) { + if (!isDataGatheringLocked) { console.log('Max data gathering has been started.'); console.time('data-gathering'); @@ -174,6 +178,24 @@ export class DataGatheringService { } } + private getBenchmarksToGather(startDate: Date) { + const benchmarksToGather = benchmarks.map((symbol) => { + return { + symbol, + date: startDate + }; + }); + + if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + benchmarksToGather.push({ + date: startDate, + symbol: 'GF.FEAR_AND_GREED_INDEX' + }); + } + + return benchmarksToGather; + } + private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> { const startDate = subDays(resetHours(new Date()), 7); @@ -190,13 +212,6 @@ export class DataGatheringService { }; }); - const benchmarksToGather = benchmarks.map((symbol) => { - return { - symbol, - date: startDate - }; - }); - const currencyPairsToGather = currencyPairs.map((symbol) => { return { symbol, @@ -205,7 +220,7 @@ export class DataGatheringService { }); return [ - ...benchmarksToGather, + ...this.getBenchmarksToGather(startDate), ...currencyPairsToGather, ...distinctOrdersWithDate ]; @@ -220,13 +235,6 @@ export class DataGatheringService { select: { date: true, symbol: true } }); - const benchmarksToGather = benchmarks.map((symbol) => { - return { - symbol, - date: startDate - }; - }); - const currencyPairsToGather = currencyPairs.map((symbol) => { return { symbol, @@ -234,7 +242,11 @@ export class DataGatheringService { }; }); - return [...benchmarksToGather, ...currencyPairsToGather, ...distinctOrders]; + return [ + ...this.getBenchmarksToGather(startDate), + ...currencyPairsToGather, + ...distinctOrders + ]; } private async isDataGatheringNeeded() { diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 9fcb87130..c08c0528e 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { isAfter, isBefore, parse } from 'date-fns'; +import { ConfigurationService } from '../../configuration.service'; import { DataProviderInterface } from '../../interfaces/data-provider.interface'; import { Granularity } from '../../interfaces/granularity.type'; import { @@ -9,13 +10,17 @@ import { } from '../../interfaces/interfaces'; import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; -const alphaVantage = require('alphavantage')({ - key: process.env.ALPHA_VANTAGE_API_KEY -}); - @Injectable() export class AlphaVantageService implements DataProviderInterface { - public constructor() {} + public alphaVantage; + + public constructor( + private readonly configurationService: ConfigurationService + ) { + this.alphaVantage = require('alphavantage')({ + key: this.configurationService.get('ALPHA_VANTAGE_API_KEY') + }); + } public async get( aSymbols: string[] @@ -40,7 +45,7 @@ export class AlphaVantageService implements DataProviderInterface { try { const historicalData: { [symbol: string]: IAlphaVantageHistoricalResponse[]; - } = await alphaVantage.crypto.daily( + } = await this.alphaVantage.crypto.daily( symbol.substring(0, symbol.length - 3).toLowerCase(), 'usd' ); @@ -73,6 +78,6 @@ export class AlphaVantageService implements DataProviderInterface { } public search(aSymbol: string) { - return alphaVantage.data.search(aSymbol); + return this.alphaVantage.data.search(aSymbol); } } diff --git a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts index 1d8eb9380..b5786674f 100644 --- a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts @@ -3,6 +3,7 @@ import * as bent from 'bent'; import { format, subMonths, subWeeks, subYears } from 'date-fns'; import { getToday, getYesterday } from 'libs/helper/src'; +import { ConfigurationService } from '../../configuration.service'; import { DataProviderInterface } from '../../interfaces/data-provider.interface'; import { Granularity } from '../../interfaces/granularity.type'; import { @@ -17,7 +18,9 @@ export class RakutenRapidApiService implements DataProviderInterface { private prisma: PrismaService; - public constructor() {} + public constructor( + private readonly configurationService: ConfigurationService + ) {} public async get( aSymbols: string[] @@ -127,7 +130,9 @@ export class RakutenRapidApiService implements DataProviderInterface { { useQueryString: true, 'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com', - 'x-rapidapi-key': process.env.RAKUTEN_RAPID_API_KEY + 'x-rapidapi-key': this.configurationService.get( + 'RAKUTEN_RAPID_API_KEY' + ) } ); diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index dbca06336..ee63e6729 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -1,7 +1,7 @@ +import { getYesterday } from '@ghostfolio/helper'; import { Injectable } from '@nestjs/common'; import { Currency } from '@prisma/client'; import { format } from 'date-fns'; -import { getYesterday } from 'libs/helper/src'; import { DataProviderService } from './data-provider.service'; @@ -15,6 +15,8 @@ export class ExchangeRateDataService { } public async initialize() { + this.pairs = []; + this.addPairs(Currency.CHF, Currency.EUR); this.addPairs(Currency.CHF, Currency.GBP); this.addPairs(Currency.CHF, Currency.USD); @@ -25,11 +27,6 @@ export class ExchangeRateDataService { await this.loadCurrencies(); } - private addPairs(aCurrency1: Currency, aCurrency2: Currency) { - this.pairs.push(`${aCurrency1}${aCurrency2}`); - this.pairs.push(`${aCurrency2}${aCurrency1}`); - } - public async loadCurrencies() { const result = await this.dataProviderService.getHistorical( this.pairs, @@ -38,19 +35,34 @@ export class ExchangeRateDataService { getYesterday() ); + const resultExtended = result; + + Object.keys(result).forEach((pair) => { + const [currency1, currency2] = pair.match(/.{1,3}/g); + const [date] = Object.keys(result[pair]); + + // Calculate the opposite direction + resultExtended[`${currency2}${currency1}`] = { + [date]: { + marketPrice: 1 / result[pair][date].marketPrice + } + }; + }); + this.pairs.forEach((pair) => { - this.currencies[pair] = - result[pair]?.[format(getYesterday(), 'yyyy-MM-dd')]?.marketPrice || 1; + const [currency1, currency2] = pair.match(/.{1,3}/g); + const date = format(getYesterday(), 'yyyy-MM-dd'); - if (this.currencies[pair] === 1) { - // Calculate the other direction - const [currency1, currency2] = pair.match(/.{1,3}/g); + this.currencies[pair] = resultExtended[pair]?.[date]?.marketPrice; + if (!this.currencies[pair]) { + // Not found, calculate indirectly via USD this.currencies[pair] = - 1 / - result[`${currency2}${currency1}`]?.[ - format(getYesterday(), 'yyyy-MM-dd') - ]?.marketPrice; + resultExtended[`${currency1}${Currency.USD}`][date].marketPrice * + resultExtended[`${Currency.USD}${currency2}`][date].marketPrice; + + // Calculate the opposite direction + this.currencies[`${currency2}${currency1}`] = 1 / this.currencies[pair]; } }); } @@ -60,6 +72,11 @@ export class ExchangeRateDataService { aFromCurrency: Currency, aToCurrency: Currency ) { + if (isNaN(this.currencies[`${Currency.USD}${Currency.CHF}`])) { + // Reinitialize if data is not loaded correctly + this.initialize(); + } + let factor = 1; if (aFromCurrency !== aToCurrency) { @@ -68,4 +85,9 @@ export class ExchangeRateDataService { return factor * aValue; } + + private addPairs(aCurrency1: Currency, aCurrency2: Currency) { + this.pairs.push(`${aCurrency1}${aCurrency2}`); + this.pairs.push(`${aCurrency2}${aCurrency1}`); + } } diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts new file mode 100644 index 000000000..de041c5d1 --- /dev/null +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -0,0 +1,18 @@ +import { CleanedEnvAccessors } from 'envalid'; + +export interface Environment extends CleanedEnvAccessors { + ACCESS_TOKEN_SALT: string; + ALPHA_VANTAGE_API_KEY: string; + CACHE_TTL: number; + ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; + ENABLE_FEATURE_SOCIAL_LOGIN: boolean; + GOOGLE_CLIENT_ID: string; + GOOGLE_SECRET: string; + JWT_SECRET_KEY: string; + MAX_ITEM_IN_CACHE: number; + PORT: number; + RAKUTEN_RAPID_API_KEY: string; + REDIS_HOST: string; + REDIS_PORT: number; + ROOT_URL: string; +} diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html index 2321a456d..c0c5d9b53 100644 --- a/apps/client/src/app/app.component.html +++ b/apps/client/src/app/app.component.html @@ -2,6 +2,7 @@ diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index ba85746b9..6ee8ca950 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -7,8 +7,8 @@ import { } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { MaterialCssVarsService } from 'angular-material-css-vars'; +import { InfoItem } from 'apps/api/src/app/info/interfaces/info-item.interface'; import { User } from 'apps/api/src/app/user/interfaces/user.interface'; -import { formatDistanceToNow } from 'date-fns'; import { primaryColorHex, secondaryColorHex } from 'libs/helper/src'; import { hasPermission, permissions } from 'libs/helper/src'; import { Subject } from 'rxjs'; @@ -28,8 +28,8 @@ export class AppComponent implements OnDestroy, OnInit { public canCreateAccount: boolean; public currentRoute: string; public currentYear = new Date().getFullYear(); + public info: InfoItem; public isLoggedIn = false; - public lastDataGathering: string; public user: User; public version = environment.version; @@ -47,10 +47,8 @@ export class AppComponent implements OnDestroy, OnInit { } public ngOnInit() { - this.dataService.fetchInfo().subscribe(({ lastDataGathering }) => { - this.lastDataGathering = lastDataGathering - ? formatDistanceToNow(new Date(lastDataGathering), { addSuffix: true }) - : ''; + this.dataService.fetchInfo().subscribe((info) => { + this.info = info; }); this.router.events diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 899401aa5..5640d73d7 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -6,6 +6,7 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; +import { InfoItem } from 'apps/api/src/app/info/interfaces/info-item.interface'; import { User } from 'apps/api/src/app/user/interfaces/user.interface'; import { hasPermission, permissions } from 'libs/helper/src'; import { EMPTY, Subject } from 'rxjs'; @@ -24,9 +25,11 @@ import { TokenStorageService } from '../../services/token-storage.service'; }) export class HeaderComponent implements OnChanges { @Input() currentRoute: string; + @Input() info: InfoItem; @Input() user: User; public canAccessAdminAccessControl: boolean; + public hasPermissionToUseSocialLogin: boolean; public impersonationId: string; private unsubscribeSubject = new Subject(); @@ -52,6 +55,11 @@ export class HeaderComponent implements OnChanges { permissions.accessAdminControl ); } + + this.hasPermissionToUseSocialLogin = hasPermission( + this.info?.globalPermissions, + permissions.useSocialLogin + ); } public impersonateAccount(aId: string) { @@ -72,7 +80,10 @@ export class HeaderComponent implements OnChanges { public openLoginDialog(): void { const dialogRef = this.dialog.open(LoginWithAccessTokenDialog, { autoFocus: false, - data: { accessToken: '' }, + data: { + accessToken: '', + hasPermissionToUseSocialLogin: this.hasPermissionToUseSocialLogin + }, width: '30rem' }); diff --git a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html index 0dc8c5650..2dbd136a5 100644 --- a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html +++ b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html @@ -19,7 +19,7 @@ > -
+
{ - setTimeout(() => { - window.location.reload(); - }, 300); - }); + const confirmation = confirm( + 'This action may take some time. Do you want to proceed?' + ); + + if (confirmation === true) { + this.adminService.gatherMax().subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); + } } public formatDistanceToNow(aDateString: string) { diff --git a/apps/client/src/app/pages/admin/admin-page.html b/apps/client/src/app/pages/admin/admin-page.html index be2ed17b3..832d5ef18 100644 --- a/apps/client/src/app/pages/admin/admin-page.html +++ b/apps/client/src/app/pages/admin/admin-page.html @@ -34,11 +34,16 @@ (click)="onFlushCache()" > - Reset + Reset Data Gathering -
diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 460e28ebb..48e1ed79e 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -39,6 +39,7 @@ export class HomePageComponent implements OnDestroy, OnInit { public deviceType: string; public fearAndGreedIndex: number; public hasImpersonationId: boolean; + public hasPermissionToAccessFearAndGreedIndex: boolean; public hasPermissionToReadForeignPortfolio: boolean; public hasPositions = false; public historicalDataItems: LineChartItem[]; @@ -80,6 +81,10 @@ export class HomePageComponent implements OnDestroy, OnInit { .subscribe(() => { this.dataService.fetchUser().subscribe((user) => { this.user = user; + this.hasPermissionToAccessFearAndGreedIndex = hasPermission( + user.permissions, + permissions.accessFearAndGreedIndex + ); this.hasPermissionToReadForeignPortfolio = hasPermission( user.permissions, permissions.readForeignPortfolio @@ -175,14 +180,16 @@ export class HomePageComponent implements OnDestroy, OnInit { this.cd.markForCheck(); }); - this.dataService - .fetchSymbolItem('GF.FEAR_AND_GREED_INDEX') - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ marketPrice }) => { - this.fearAndGreedIndex = marketPrice; + if (this.hasPermissionToAccessFearAndGreedIndex) { + this.dataService + .fetchSymbolItem('GF.FEAR_AND_GREED_INDEX') + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ marketPrice }) => { + this.fearAndGreedIndex = marketPrice; - this.cd.markForCheck(); - }); + this.cd.markForCheck(); + }); + } this.cd.markForCheck(); } diff --git a/apps/client/src/app/pages/login/login-page.component.ts b/apps/client/src/app/pages/login/login-page.component.ts index 119cd13fc..e7615008d 100644 --- a/apps/client/src/app/pages/login/login-page.component.ts +++ b/apps/client/src/app/pages/login/login-page.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; +import { hasPermission, permissions } from '@ghostfolio/helper'; import { format } from 'date-fns'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -276,7 +277,10 @@ export class LoginPageComponent implements OnDestroy, OnInit { authToken: string ): void { const dialogRef = this.dialog.open(ShowAccessTokenDialog, { - data: { accessToken, authToken }, + data: { + accessToken, + authToken + }, disableClose: true, width: '30rem' }); diff --git a/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component.ts b/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component.ts index 74aa52f2b..add1a42b8 100644 --- a/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component.ts +++ b/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component.ts @@ -1,8 +1,6 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { DataService } from '../../../services/data.service'; - @Component({ selector: 'login-with-access-token-dialog', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.html b/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.html index 1f83fd389..56fe52173 100644 --- a/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.html +++ b/apps/client/src/app/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -1,13 +1,15 @@

Sign in

- -
or
+ + +
or
+
Security Token