diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0d29fa44..94252a0e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Refactored the language redirect of the root path from the static file serving configuration to a dedicated middleware - Upgraded `yahoo-finance2` from version `3.15.4` to `4.0.0` +### Fixed + +- Fixed the `RangeNotSatisfiableError` for requests with a `Range` header to the root path caused by the empty `index.html` placeholder +- Fixed the unresolved template literal in the page title while the app is loading from the service worker cache + ## 3.30.0 - 2026-07-19 ### Added diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 4bdd50c9e8..bd76ef49b4 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -14,8 +14,6 @@ import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-g import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { BULL_BOARD_ROUTE, - DEFAULT_LANGUAGE_CODE, - SUPPORTED_LANGUAGE_CODES, THROTTLE_DEFAULT_LIMIT, THROTTLE_DEFAULT_TTL } from '@ghostfolio/common/config'; @@ -143,29 +141,7 @@ import { UserModule } from './user/user.module'; '/api/*wildcard', '/sitemap.xml' ], - rootPath: join(__dirname, '..', 'client'), - serveStaticOptions: { - setHeaders: (res) => { - if (res.req?.path === '/') { - let languageCode = DEFAULT_LANGUAGE_CODE; - - try { - const code = res.req.headers['accept-language'] - .split(',')[0] - .split('-')[0]; - - if ( - (SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code) - ) { - languageCode = code; - } - } catch {} - - res.set('Location', `/${languageCode}`); - res.statusCode = StatusCodes.MOVED_PERMANENTLY; - } - } - } + rootPath: join(__dirname, '..', 'client') }), ServeStaticModule.forRoot({ rootPath: join(__dirname, '..', 'client', '.well-known'), diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 33ad032e98..77d571ea0b 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,3 +1,4 @@ +import { languageRedirectMiddleware } from '@ghostfolio/api/middlewares/language-redirect.middleware'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { BULL_BOARD_ROUTE, @@ -100,6 +101,8 @@ async function bootstrap() { }); } + app.use(languageRedirectMiddleware); + const configurationService = app.get(ConfigurationService); const trustProxy = configurationService.get('TRUST_PROXY'); diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts index bdace34024..928a9f22c5 100644 --- a/apps/api/src/middlewares/html-template.middleware.ts +++ b/apps/api/src/middlewares/html-template.middleware.ts @@ -111,7 +111,13 @@ export class HtmlTemplateMiddleware implements NestMiddleware { ); try { - map[languageCode] = readFileSync(indexHtmlPath, 'utf8'); + // Restore the interpolation token which the template replaces with a + // static fallback title to avoid showing an unresolved template + // literal when served without interpolation (e.g. by the service worker) + map[languageCode] = readFileSync(indexHtmlPath, 'utf8').replace( + /.*?<\/title>/, + '<title>${title}' + ); } catch { this.logger.warn( `Skipping language '${languageCode}': ${indexHtmlPath} not found` diff --git a/apps/api/src/middlewares/language-redirect.middleware.ts b/apps/api/src/middlewares/language-redirect.middleware.ts new file mode 100644 index 0000000000..5b6fac6c43 --- /dev/null +++ b/apps/api/src/middlewares/language-redirect.middleware.ts @@ -0,0 +1,37 @@ +import { environment } from '@ghostfolio/api/environments/environment'; +import { + DEFAULT_LANGUAGE_CODE, + SUPPORTED_LANGUAGE_CODES +} from '@ghostfolio/common/config'; + +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +export function languageRedirectMiddleware( + request: Request, + response: Response, + next: NextFunction +) { + if ( + !environment.production || + request.path !== '/' || + !['GET', 'HEAD'].includes(request.method) + ) { + return next(); + } + + let languageCode = DEFAULT_LANGUAGE_CODE; + + try { + const code = request.headers['accept-language'].split(',')[0].split('-')[0]; + + if ((SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)) { + languageCode = code; + } + } catch {} + + return response.redirect( + StatusCodes.MOVED_PERMANENTLY, + `/${languageCode}${request.url.slice(1)}` + ); +} diff --git a/apps/client/project.json b/apps/client/project.json index 9b0e8dafe2..c6367144ee 100644 --- a/apps/client/project.json +++ b/apps/client/project.json @@ -203,9 +203,6 @@ { "command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client" }, - { - "command": "shx cp apps/client/src/assets/index.html dist/apps/client" - }, { "command": "shx cp apps/client/src/assets/robots.txt dist/apps/client" }, diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 62bc78df16..0bed8111fb 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -213,9 +213,16 @@ export class GfAdminOverviewComponent implements OnInit { duration: this.couponDuration }; + const hasCopiedCouponCode = this.clipboard.copy(newCoupon.code); + const coupons = [...this.couponsDataSource.data, newCoupon]; - this.saveCoupons({ coupons, codeToCopy: newCoupon.code }); + this.saveCoupons({ + coupons, + snackBarMessage: hasCopiedCouponCode + ? '✅ ' + $localize`${newCoupon.code} has been copied to the clipboard` + : '✅ ' + $localize`Coupon ${newCoupon.code} has been created` + }); } protected onChangeCouponDuration(aCouponDuration: StringValue) { @@ -374,11 +381,11 @@ export class GfAdminOverviewComponent implements OnInit { } private saveCoupons({ - codeToCopy, - coupons + coupons, + snackBarMessage }: { - codeToCopy?: string; coupons: Coupon[]; + snackBarMessage?: string; }) { this.dataService .putAdminSetting(PROPERTY_COUPONS, { @@ -388,14 +395,10 @@ export class GfAdminOverviewComponent implements OnInit { .subscribe(() => { this.couponsDataSource.data = coupons; - if (codeToCopy) { - this.clipboard.copy(codeToCopy); - - this.snackBar.open( - '✅ ' + $localize`${codeToCopy} has been copied to the clipboard`, - undefined, - { duration: ms('3 seconds') } - ); + if (snackBarMessage) { + this.snackBar.open(snackBarMessage, undefined, { + duration: ms('3 seconds') + }); } this.changeDetectorRef.markForCheck(); diff --git a/apps/client/src/assets/index.html b/apps/client/src/assets/index.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/client/src/index.html b/apps/client/src/index.html index c923e3e0c5..61953bca8d 100644 --- a/apps/client/src/index.html +++ b/apps/client/src/index.html @@ -1,7 +1,7 @@ - ${title} + Ghostfolio