diff --git a/CHANGELOG.md b/CHANGELOG.md index dac0c3fa1..9077bc99e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Migrated the `HtmlTemplateMiddleware` to use `@Injectable()` - Improved the language localization for French (`fr`) - Improved the language localization for Polish (`pl`) diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 6f512e89b..ae6d2f40a 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,8 +1,10 @@ import { EventsModule } from '@ghostfolio/api/events/events.module'; +import { HtmlTemplateMiddleware } from '@ghostfolio/api/middlewares/html-template.middleware'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { CronModule } from '@ghostfolio/api/services/cron/cron.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; +import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; @@ -13,7 +15,7 @@ import { } from '@ghostfolio/common/config'; import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule } from '@nestjs/schedule'; @@ -130,6 +132,11 @@ import { UserModule } from './user/user.module'; TagsModule, UserModule, WatchlistModule - ] + ], + providers: [I18nService] }) -export class AppModule {} +export class AppModule implements NestModule { + public configure(consumer: MiddlewareConsumer) { + consumer.apply(HtmlTemplateMiddleware).forRoutes('*'); + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 73502525c..06138fecc 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -18,7 +18,6 @@ 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); @@ -77,8 +76,6 @@ async function bootstrap() { }); } - app.use(HtmlTemplateMiddleware); - const HOST = configService.get('HOST') || DEFAULT_HOST; const PORT = configService.get('PORT') || DEFAULT_PORT; diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts index 403b09610..5cf353e9a 100644 --- a/apps/api/src/middlewares/html-template.middleware.ts +++ b/apps/api/src/middlewares/html-template.middleware.ts @@ -7,30 +7,14 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper'; +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; import { format } from 'date-fns'; import { NextFunction, Request, Response } from 'express'; import * as fs from 'fs'; import { join } from 'path'; -const i18nService = new I18nService(); - -let indexHtmlMap: { [languageCode: string]: string } = {}; - const title = 'Ghostfolio'; -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', @@ -94,71 +78,93 @@ const locales = { } }; -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-de.fi' - ) || - filename.includes( - 'personal-finance-tools/open-source-alternative-to-markets.sh' - ) - ) { - return false; - } +@Injectable() +export class HtmlTemplateMiddleware implements NestMiddleware { + private indexHtmlMap: { [languageCode: string]: string } = {}; - return filename.split('.').pop() !== filename; -}; + public constructor(private readonly i18nService: I18nService) { + try { + this.indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce( + (map, languageCode) => ({ + ...map, + [languageCode]: fs.readFileSync( + join(__dirname, '..', 'client', languageCode, 'index.html'), + 'utf8' + ) + }), + {} + ); + } catch (error) { + Logger.error( + 'Failed to initialize index HTML map', + error, + 'HTMLTemplateMiddleware' + ); + } + } -export const HtmlTemplateMiddleware = async ( - request: Request, - response: Response, - next: NextFunction -) => { - const path = request.originalUrl.replace(/\/$/, ''); - let languageCode = path.substr(1, 2); + public use(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; - } + if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) { + languageCode = DEFAULT_LANGUAGE_CODE; + } - const currentDate = format(new Date(), DATE_FORMAT); - const rootUrl = process.env.ROOT_URL || environment.rootUrl; + const currentDate = format(new Date(), DATE_FORMAT); + const rootUrl = process.env.ROOT_URL || environment.rootUrl; - if ( - path.startsWith('/api/') || - path.startsWith(STORYBOOK_PATH) || - isFileRequest(path) || - !environment.production - ) { - // Skip - next(); - } else { - const indexHtml = interpolate(indexHtmlMap[languageCode], { - currentDate, - languageCode, - path, - rootUrl, - description: i18nService.getTranslation({ + if ( + path.startsWith('/api/') || + path.startsWith(STORYBOOK_PATH) || + this.isFileRequest(path) || + !environment.production + ) { + // Skip + next(); + } else { + const indexHtml = interpolate(this.indexHtmlMap[languageCode], { + currentDate, languageCode, - id: 'metaDescription' - }), - featureGraphicPath: - locales[path]?.featureGraphicPath ?? 'assets/cover.png', - keywords: i18nService.getTranslation({ - languageCode, - id: 'metaKeywords' - }), - title: - locales[path]?.title ?? - `${title} – ${i18nService.getTranslation({ + path, + rootUrl, + description: this.i18nService.getTranslation({ + languageCode, + id: 'metaDescription' + }), + featureGraphicPath: + locales[path]?.featureGraphicPath ?? 'assets/cover.png', + keywords: this.i18nService.getTranslation({ languageCode, - id: 'slogan' - })}` - }); + id: 'metaKeywords' + }), + title: + locales[path]?.title ?? + `${title} – ${this.i18nService.getTranslation({ + languageCode, + id: 'slogan' + })}` + }); - return response.send(indexHtml); + return response.send(indexHtml); + } } -}; + + 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-de.fi' + ) || + filename.includes( + 'personal-finance-tools/open-source-alternative-to-markets.sh' + ) + ) { + return false; + } + + return filename.split('.').pop() !== filename; + } +}