From b0e274e4cbd147cf302d568009309ab2a49a61ce Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Sun, 18 Apr 2021 12:02:28 +0200 Subject: [PATCH] Simplify initial project setup * Added a validation for environment variables * Added support for feature flags to simplify the initial project setup --- .env | 7 --- CHANGELOG.md | 7 +++ 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 +++++++-- .../api/src/services/configuration.service.ts | 32 ++++++++++++++ .../src/services/data-gathering.service.ts | 44 ++++++++++++------- .../alpha-vantage/alpha-vantage.service.ts | 19 +++++--- .../rakuten-rapid-api.service.ts | 9 +++- .../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 +- .../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 | 10 +---- libs/helper/src/lib/permissions.ts | 4 +- package.json | 1 + yarn.lock | 5 +++ 39 files changed, 249 insertions(+), 133 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 6ceab2780..79422434d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added a validation for environment variables +- Added support for feature flags to simplify the initial project setup + ## 0.85.0 - 16.04.2021 ### Changed 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 971b42007..64ea8ee4a 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/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..dcf3d0fa5 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 ) {} @@ -174,6 +176,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 +210,6 @@ export class DataGatheringService { }; }); - const benchmarksToGather = benchmarks.map((symbol) => { - return { - symbol, - date: startDate - }; - }); - const currencyPairsToGather = currencyPairs.map((symbol) => { return { symbol, @@ -205,7 +218,7 @@ export class DataGatheringService { }); return [ - ...benchmarksToGather, + ...this.getBenchmarksToGather(startDate), ...currencyPairsToGather, ...distinctOrdersWithDate ]; @@ -220,13 +233,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 +240,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/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 2272723e7..b9125bf6b 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 3cebc907e..dd225dc69 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'; @@ -27,8 +27,8 @@ import { TokenStorageService } from './services/token-storage.service'; export class AppComponent implements OnDestroy, OnInit { public canCreateAccount: boolean; public currentRoute: string; + public info: InfoItem; public isLoggedIn = false; - public lastDataGathering: string; public user: User; public version = environment.version; @@ -46,10 +46,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 @@ > -
+
{ 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