From 998675f48cfb80e1b74a26df9ed6647b7d13a1be Mon Sep 17 00:00:00 2001 From: izzy Date: Fri, 7 Nov 2025 15:36:51 +0000 Subject: [PATCH] feat: add SSR to maintenance API --- server/src/app.common.ts | 16 ++++------ server/src/services/api.service.ts | 2 +- .../services/maintenance-worker.service.ts | 32 +++++++++++++++++++ server/src/workers/api.ts | 5 ++- server/src/workers/maintenance.ts | 2 +- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/server/src/app.common.ts b/server/src/app.common.ts index 1071372742..da3078de5d 100644 --- a/server/src/app.common.ts +++ b/server/src/app.common.ts @@ -10,6 +10,7 @@ import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { bootstrapTelemetry } from 'src/repositories/telemetry.repository'; import { ApiService } from 'src/services/api.service'; +import { MaintenanceWorkerService } from 'src/services/maintenance-worker.service'; import { useSwagger } from 'src/utils/misc'; export function configureTelemetry() { @@ -23,7 +24,7 @@ export async function configureExpress( app: NestExpressApplication, { permitSwaggerWrite = true, - ssr = true, + ssr, }: { /** * Whether to allow swagger module to write to the specs.json @@ -32,12 +33,10 @@ export async function configureExpress( */ permitSwaggerWrite?: boolean; /** - * Whether to enable server-side rendering - * @requires ApiService - * @default true + * Service to use for server-side rendering */ - ssr?: boolean; - } = {}, + ssr: typeof ApiService | typeof MaintenanceWorkerService; + }, ) { const configRepository = app.get(ConfigRepository); const { environment, host, port, resourcePaths, network } = configRepository.getEnv(); @@ -78,10 +77,7 @@ export async function configureExpress( ); } - if (ssr) { - app.use(app.get(ApiService).ssr(excludePaths)); - } - + app.use(app.get(ssr).ssr(excludePaths)); app.use(compression()); const server = await (host ? app.listen(port, host) : app.listen(port)); diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts index 143b470750..44c0c76fc2 100644 --- a/server/src/services/api.service.ts +++ b/server/src/services/api.service.ts @@ -12,7 +12,7 @@ import { SharedLinkService } from 'src/services/shared-link.service'; import { VersionService } from 'src/services/version.service'; import { OpenGraphTags } from 'src/utils/misc'; -const render = (index: string, meta: OpenGraphTags) => { +export const render = (index: string, meta: OpenGraphTags) => { const [title, description, imageUrl] = [meta.title, meta.description, meta.imageUrl].map((item) => item ? sanitizeHtml(item, { allowedTags: [] }) : '', ); diff --git a/server/src/services/maintenance-worker.service.ts b/server/src/services/maintenance-worker.service.ts index 471787d8b2..77efbdbba2 100644 --- a/server/src/services/maintenance-worker.service.ts +++ b/server/src/services/maintenance-worker.service.ts @@ -1,6 +1,8 @@ import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; import { parse } from 'cookie'; +import { NextFunction, Request, Response } from 'express'; import { jwtVerify } from 'jose'; +import { readFileSync } from 'node:fs'; import { IncomingHttpHeaders } from 'node:http'; import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { ImmichCookie, SystemMetadataKey } from 'src/enum'; @@ -109,4 +111,34 @@ export class MaintenanceWorkerService { await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state); this.maintenanceWorkerRepository.restartApp(state); } + + ssr(excludePaths: string[]) { + const { resourcePaths } = this.configRepository.getEnv(); + + let index = ''; + try { + index = readFileSync(resourcePaths.web.indexHtml).toString(); + } catch { + this.logger.warn(`Unable to open ${resourcePaths.web.indexHtml}, skipping SSR.`); + } + + return async (request: Request, res: Response, next: NextFunction) => { + if ( + request.url.startsWith('/api') || + request.method.toLowerCase() !== 'get' || + excludePaths.some((item) => request.url.startsWith(item)) + ) { + return next(); + } + + const shareKey = request.url.match(/^\/share\/(.+)$/); + const shareSlug = request.url.match(/^\/s\/(.+)$/); + + res + .status(shareKey || shareSlug ? 500 : 200) + .type('text/html') + .header('Cache-Control', 'no-store') + .send(index); + }; + } } diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index 1cf4786851..330005e2b2 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -3,6 +3,7 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { configureExpress, configureTelemetry } from 'src/app.common'; import { ApiModule } from 'src/app.module'; import { MaintenanceRepository } from 'src/repositories/maintenance.repository'; +import { ApiService } from 'src/services/api.service'; import { isStartUpError } from 'src/utils/misc'; async function bootstrap() { @@ -13,7 +14,9 @@ async function bootstrap() { const app = await NestFactory.create(ApiModule, { bufferLogs: true }); app.get(MaintenanceRepository).setCloseFn(() => app.close()); - void configureExpress(app); + void configureExpress(app, { + ssr: ApiService, + }); } bootstrap().catch((error) => { diff --git a/server/src/workers/maintenance.ts b/server/src/workers/maintenance.ts index 8f0db7bbb5..21707e992b 100644 --- a/server/src/workers/maintenance.ts +++ b/server/src/workers/maintenance.ts @@ -14,7 +14,7 @@ async function bootstrap() { app.get(MaintenanceRepository).setCloseFn(() => app.close()); void configureExpress(app, { permitSwaggerWrite: false, - ssr: false, + ssr: MaintenanceWorkerService, }); void app.get(MaintenanceWorkerService).logSecret();