From 2df27100f04f1592aaf83130f901f9809126f8e6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:27:19 +0200 Subject: [PATCH] Add middleware (#2239) * Add middleware * Update changelog --- CHANGELOG.md | 4 + apps/api/src/app/app.module.ts | 17 +- apps/api/src/app/frontend.middleware.ts | 232 ------------------ apps/api/src/main.ts | 3 + .../middlewares/html-template.middleware.ts | 128 ++++++++++ .../configuration/configuration.service.ts | 4 +- apps/client/src/index.html | 32 +-- apps/client/src/index.template.html | 63 ----- libs/common/src/lib/config.ts | 1 + libs/common/src/lib/helper.ts | 2 +- 10 files changed, 151 insertions(+), 335 deletions(-) delete mode 100644 apps/api/src/app/frontend.middleware.ts create mode 100644 apps/api/src/middlewares/html-template.middleware.ts delete mode 100644 apps/client/src/index.template.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 8481f343a..3efc198cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the data export feature to the user account page - Added a currencies preset to the historical market data table of the admin control panel +### Changed + +- Improved the localized meta data in `html` files + ### Fixed - Fixed the rows with cash positions in the holdings table diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 4b78a1e7c..a521e7fa9 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -12,7 +12,7 @@ import { SUPPORTED_LANGUAGE_CODES } from '@ghostfolio/common/config'; import { BullModule } from '@nestjs/bull'; -import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { ServeStaticModule } from '@nestjs/serve-static'; @@ -28,7 +28,6 @@ import { BenchmarkModule } from './benchmark/benchmark.module'; import { CacheModule } from './cache/cache.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExportModule } from './export/export.module'; -import { FrontendMiddleware } from './frontend.middleware'; import { HealthModule } from './health/health.module'; import { ImportModule } from './import/import.module'; import { InfoModule } from './info/info.module'; @@ -75,12 +74,6 @@ import { UserModule } from './user/user.module'; PrismaModule, RedisCacheModule, ScheduleModule.forRoot(), - ...SUPPORTED_LANGUAGE_CODES.map((languageCode) => { - return ServeStaticModule.forRoot({ - rootPath: join(__dirname, '..', 'client', languageCode), - serveRoot: `/${languageCode}` - }); - }), ServeStaticModule.forRoot({ exclude: ['/api*', '/sitemap.xml'], rootPath: join(__dirname, '..', 'client'), @@ -114,10 +107,4 @@ import { UserModule } from './user/user.module'; controllers: [AppController], providers: [CronService] }) -export class AppModule { - configure(consumer: MiddlewareConsumer) { - consumer - .apply(FrontendMiddleware) - .forRoutes({ path: '*', method: RequestMethod.ALL }); - } -} +export class AppModule {} diff --git a/apps/api/src/app/frontend.middleware.ts b/apps/api/src/app/frontend.middleware.ts deleted file mode 100644 index 9996445a5..000000000 --- a/apps/api/src/app/frontend.middleware.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -import { environment } from '@ghostfolio/api/environments/environment'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; -import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper'; -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { format } from 'date-fns'; -import { NextFunction, Request, Response } from 'express'; - -@Injectable() -export class FrontendMiddleware implements NestMiddleware { - public indexHtmlDe = ''; - public indexHtmlEn = ''; - public indexHtmlEs = ''; - public indexHtmlFr = ''; - public indexHtmlIt = ''; - public indexHtmlNl = ''; - public indexHtmlPt = ''; - - private static readonly DEFAULT_DESCRIPTION = - 'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.'; - - public constructor( - private readonly configurationService: ConfigurationService - ) { - try { - this.indexHtmlDe = fs.readFileSync( - this.getPathOfIndexHtmlFile('de'), - 'utf8' - ); - this.indexHtmlEn = fs.readFileSync( - this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE), - 'utf8' - ); - this.indexHtmlEs = fs.readFileSync( - this.getPathOfIndexHtmlFile('es'), - 'utf8' - ); - this.indexHtmlFr = fs.readFileSync( - this.getPathOfIndexHtmlFile('fr'), - 'utf8' - ); - this.indexHtmlIt = fs.readFileSync( - this.getPathOfIndexHtmlFile('it'), - 'utf8' - ); - this.indexHtmlNl = fs.readFileSync( - this.getPathOfIndexHtmlFile('nl'), - 'utf8' - ); - this.indexHtmlPt = fs.readFileSync( - this.getPathOfIndexHtmlFile('pt'), - 'utf8' - ); - } catch {} - } - - public use(request: Request, response: Response, next: NextFunction) { - const currentDate = format(new Date(), DATE_FORMAT); - let featureGraphicPath = 'assets/cover.png'; - let title = 'Ghostfolio – Open Source Wealth Management Software'; - - if (request.path.startsWith('/en/blog/2022/08/500-stars-on-github')) { - featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg'; - title = `500 Stars - ${title}`; - } else if (request.path.startsWith('/en/blog/2022/10/hacktoberfest-2022')) { - featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png'; - title = `Hacktoberfest 2022 - ${title}`; - } else if (request.path.startsWith('/en/blog/2022/11/black-friday-2022')) { - featureGraphicPath = 'assets/images/blog/black-friday-2022.jpg'; - title = `Black Friday 2022 - ${title}`; - } else if ( - request.path.startsWith( - '/en/blog/2022/12/the-importance-of-tracking-your-personal-finances' - ) - ) { - featureGraphicPath = 'assets/images/blog/20221226.jpg'; - title = `The importance of tracking your personal finances - ${title}`; - } else if ( - request.path.startsWith( - '/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt' - ) - ) { - featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png'; - title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`; - } else if ( - request.path.startsWith('/en/blog/2023/02/ghostfolio-meets-umbrel') - ) { - featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png'; - title = `Ghostfolio meets Umbrel - ${title}`; - } else if ( - request.path.startsWith( - '/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github' - ) - ) { - featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg'; - title = `Ghostfolio reaches 1’000 Stars on GitHub - ${title}`; - } else if ( - request.path.startsWith( - '/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio' - ) - ) { - featureGraphicPath = 'assets/images/blog/20230520.jpg'; - title = `Unlock your Financial Potential with Ghostfolio - ${title}`; - } else if ( - request.path.startsWith('/en/blog/2023/07/exploring-the-path-to-fire') - ) { - featureGraphicPath = 'assets/images/blog/20230701.jpg'; - title = `Exploring the Path to FIRE - ${title}`; - } - - if ( - request.path.startsWith('/api/') || - this.isFileRequest(request.url) || - !environment.production - ) { - // Skip - next(); - } else if (request.path === '/de' || request.path.startsWith('/de/')) { - response.send( - interpolate(this.indexHtmlDe, { - currentDate, - featureGraphicPath, - title, - description: - 'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.', - languageCode: 'de', - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } else if (request.path === '/es' || request.path.startsWith('/es/')) { - response.send( - interpolate(this.indexHtmlEs, { - currentDate, - featureGraphicPath, - title, - description: - 'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.', - languageCode: 'es', - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } else if (request.path === '/fr' || request.path.startsWith('/fr/')) { - response.send( - interpolate(this.indexHtmlFr, { - currentDate, - featureGraphicPath, - title, - description: - 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.', - languageCode: 'fr', - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } else if (request.path === '/it' || request.path.startsWith('/it/')) { - response.send( - interpolate(this.indexHtmlIt, { - currentDate, - featureGraphicPath, - title, - description: - 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.', - languageCode: 'it', - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } else if (request.path === '/nl' || request.path.startsWith('/nl/')) { - response.send( - interpolate(this.indexHtmlNl, { - currentDate, - featureGraphicPath, - title, - description: - 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.', - languageCode: 'nl', - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } else if (request.path === '/pt' || request.path.startsWith('/pt/')) { - response.send( - interpolate(this.indexHtmlPt, { - currentDate, - featureGraphicPath, - title, - description: - 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.', - languageCode: 'pt', - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } else { - response.send( - interpolate(this.indexHtmlEn, { - currentDate, - featureGraphicPath, - title, - description: FrontendMiddleware.DEFAULT_DESCRIPTION, - languageCode: DEFAULT_LANGUAGE_CODE, - path: request.path, - rootUrl: this.configurationService.get('ROOT_URL') - }) - ); - } - } - - private getPathOfIndexHtmlFile(aLocale: string) { - return path.join(__dirname, '..', 'client', aLocale, 'index.html'); - } - - private isFileRequest(filename: string) { - if (filename === '/assets/LICENSE') { - return true; - } else if ( - filename.includes('auth/ey') || - filename.includes( - 'personal-finance-tools/open-source-alternative-to-markets.sh' - ) - ) { - return false; - } - - return filename.split('.').pop() !== filename; - } -} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 5d76776a1..e0e7daf2f 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -7,6 +7,7 @@ import helmet from 'helmet'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; +import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware'; async function bootstrap() { const configApp = await NestFactory.create(AppModule); @@ -52,6 +53,8 @@ async function bootstrap() { ); } + app.use(HtmlTemplateMiddleware); + const BASE_CURRENCY = configService.get('BASE_CURRENCY'); const HOST = configService.get('HOST') || '0.0.0.0'; const PORT = configService.get('PORT') || 3333; diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts new file mode 100644 index 000000000..efbd60fa7 --- /dev/null +++ b/apps/api/src/middlewares/html-template.middleware.ts @@ -0,0 +1,128 @@ +import * as fs from 'fs'; +import { join } from 'path'; + +import { environment } from '@ghostfolio/api/environments/environment'; +import { + DEFAULT_LANGUAGE_CODE, + DEFAULT_ROOT_URL, + SUPPORTED_LANGUAGE_CODES +} from '@ghostfolio/common/config'; +import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper'; +import { format } from 'date-fns'; +import { NextFunction, Request, Response } from 'express'; + +const descriptions = { + de: 'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.', + en: 'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.', + es: 'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.', + fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.', + it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.', + nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.', + pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.' +}; + +const title = 'Ghostfolio – Open Source Wealth Management Software'; +const titleShort = 'Ghostfolio'; + +let indexHtmlMap: { [languageCode: string]: string } = {}; + +try { + indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce( + (map, languageCode) => ({ + ...map, + [languageCode]: fs.readFileSync( + join(__dirname, '..', 'client', languageCode, 'index.html'), + 'utf8' + ) + }), + {} + ); +} catch {} + +const locales = { + '/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt': { + featureGraphicPath: 'assets/images/blog/ghostfolio-x-sackgeld.png', + title: `Ghostfolio auf Sackgeld.com vorgestellt - ${titleShort}` + }, + '/en/blog/2022/08/500-stars-on-github': { + featureGraphicPath: 'assets/images/blog/500-stars-on-github.jpg', + title: `500 Stars - ${titleShort}` + }, + '/en/blog/2022/10/hacktoberfest-2022': { + featureGraphicPath: 'assets/images/blog/hacktoberfest-2022.png', + title: `Hacktoberfest 2022 - ${titleShort}` + }, + '/en/blog/2022/12/the-importance-of-tracking-your-personal-finances': { + featureGraphicPath: 'assets/images/blog/20221226.jpg', + title: `The importance of tracking your personal finances - ${titleShort}` + }, + '/en/blog/2023/02/ghostfolio-meets-umbrel': { + featureGraphicPath: 'assets/images/blog/ghostfolio-x-umbrel.png', + title: `Ghostfolio meets Umbrel - ${titleShort}` + }, + '/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github': { + featureGraphicPath: 'assets/images/blog/1000-stars-on-github.jpg', + title: `Ghostfolio reaches 1’000 Stars on GitHub - ${titleShort}` + }, + '/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio': { + featureGraphicPath: 'assets/images/blog/20230520.jpg', + title: `Unlock your Financial Potential with Ghostfolio - ${titleShort}` + }, + '/en/blog/2023/07/exploring-the-path-to-fire': { + featureGraphicPath: 'assets/images/blog/20230701.jpg', + title: `Exploring the Path to FIRE - ${titleShort}` + } +}; + +const isFileRequest = (filename: string) => { + if (filename === '/assets/LICENSE') { + return true; + } else if ( + filename.includes('auth/ey') || + filename.includes( + 'personal-finance-tools/open-source-alternative-to-markets.sh' + ) + ) { + return false; + } + + return filename.split('.').pop() !== filename; +}; + +export const HtmlTemplateMiddleware = async ( + request: Request, + response: Response, + next: NextFunction +) => { + const path = request.originalUrl.replace(/\/$/, ''); + let languageCode = path.substr(1, 2); + + if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) { + languageCode = DEFAULT_LANGUAGE_CODE; + } + + const currentDate = format(new Date(), DATE_FORMAT); + const rootUrl = process.env.ROOT_URL || DEFAULT_ROOT_URL; + + if ( + path.startsWith('/api/') || + isFileRequest(path) || + !environment.production + ) { + // Skip + next(); + } else { + const indexHtml = interpolate(indexHtmlMap[languageCode], { + currentDate, + languageCode, + path, + rootUrl, + description: descriptions[languageCode], + featureGraphicPath: + locales[path]?.featureGraphicPath ?? 'assets/cover.png', + title: locales[path]?.title ?? title + }); + + return response.send(indexHtml); + } +}; diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index fa9ada3d7..54eedaa4d 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -1,5 +1,5 @@ import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface'; -import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; +import { DEFAULT_CURRENCY, DEFAULT_ROOT_URL } from '@ghostfolio/common/config'; import { Injectable } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { bool, cleanEnv, host, json, num, port, str } from 'envalid'; @@ -47,7 +47,7 @@ export class ConfigurationService { REDIS_HOST: str({ default: 'localhost' }), REDIS_PASSWORD: str({ default: '' }), REDIS_PORT: port({ default: 6379 }), - ROOT_URL: str({ default: 'http://localhost:4200' }), + ROOT_URL: str({ default: DEFAULT_ROOT_URL }), STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }), TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), diff --git a/apps/client/src/index.html b/apps/client/src/index.html index a7d01e481..55120ec08 100644 --- a/apps/client/src/index.html +++ b/apps/client/src/index.html @@ -1,14 +1,11 @@ - + - Ghostfolio – Open Source Wealth Management Software + ${title} - + - - + + - + - - - - + + + + - - - ${title} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index c5c627de8..345641f97 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -40,6 +40,7 @@ export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_REQUEST_TIMEOUT = ms('3 seconds'); +export const DEFAULT_ROOT_URL = 'http://localhost:4200'; export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180'; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 40933e19d..d3a6c9d71 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -235,7 +235,7 @@ export function isCurrency(aSymbol = '') { } export function interpolate(template: string, context: any) { - return template.replace(/[$]{([^}]+)}/g, (_, objectPath) => { + return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => { const properties = objectPath.split('.'); return properties.reduce( (previous, current) => previous?.[current],