From c8f3eef45d875cf9a8175a763eff529a7233eaf9 Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 6 Nov 2025 15:35:17 +0000 Subject: [PATCH] refactor: use service instead of repository chore: read token from cookie if possible chore: rename client event to AppRestartV1 --- .../maintenance-worker.controller.ts | 32 ++++++++----------- server/src/dtos/maintenance.dto.ts | 2 +- .../maintenance-worker.repository.ts | 10 +++--- .../repositories/maintenance.repository.ts | 2 +- .../src/repositories/websocket.repository.ts | 5 ++- .../services/maintenance-worker.service.ts | 15 ++++++++- server/src/services/notification.service.ts | 2 +- server/src/workers/maintenance.ts | 8 ++--- web/src/lib/stores/websocket.ts | 19 +++++------ 9 files changed, 51 insertions(+), 44 deletions(-) diff --git a/server/src/controllers/maintenance-worker.controller.ts b/server/src/controllers/maintenance-worker.controller.ts index 296ce68c8f..42371a47ea 100644 --- a/server/src/controllers/maintenance-worker.controller.ts +++ b/server/src/controllers/maintenance-worker.controller.ts @@ -1,46 +1,42 @@ -import { BadRequestException, Body, Controller, Get, Post, Res } from '@nestjs/common'; +import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Response } from 'express'; -import { MaintenanceAuthDto, MaintenanceLoginDto, MaintenanceModeResponseDto } from 'src/dtos/maintenance.dto'; +import { Request, Response } from 'express'; +import { MaintenanceAuthDto, MaintenanceLoginDto } from 'src/dtos/maintenance.dto'; import { ServerConfigDto } from 'src/dtos/server.dto'; import { ImmichCookie } from 'src/enum'; import { MaintenanceRoute } from 'src/middleware/maintenance-auth.guard'; -import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker.repository'; import { MaintenanceWorkerService } from 'src/services/maintenance-worker.service'; @ApiTags('Maintenance (admin)') -@Controller('') +@Controller() export class MaintenanceWorkerController { - constructor( - private repository: MaintenanceWorkerRepository, - private service: MaintenanceWorkerService, - ) {} + constructor(private service: MaintenanceWorkerService) {} @Get('server/config') getServerConfig(): Promise { return this.service.getSystemConfig(); } - @Post('admin/maintenance/start') - @MaintenanceRoute() - startMaintenance(): Promise { - throw new BadRequestException('Already in maintenance mode'); - } - @Post('admin/maintenance/login') async maintenanceLogin( + @Req() request: Request, @Body() dto: MaintenanceLoginDto, @Res({ passthrough: true }) response: Response, ): Promise { - const auth = await this.repository.decodeToken(dto.token); + const auth = await this.service.login(dto.token ?? request.cookies[ImmichCookie.MaintenanceToken]); response.cookie(ImmichCookie.MaintenanceToken, dto.token); return auth; } + @Post('admin/maintenance/start') + @MaintenanceRoute() + startMaintenance(): void { + this.service.startMaintenance(); + } + @Post('admin/maintenance/end') @MaintenanceRoute() - async endMaintenance(): Promise { + async endMaintenance(): Promise { await this.service.endMaintenance(); - return { isMaintenanceMode: false }; } } diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index ef8cd33761..a4c3724c30 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -7,7 +7,7 @@ export class MaintenanceModeResponseDto { export class MaintenanceLoginDto { @ValidateString() - token!: string; + token?: string; } export class MaintenanceAuthDto { diff --git a/server/src/repositories/maintenance-worker.repository.ts b/server/src/repositories/maintenance-worker.repository.ts index 4049704f2c..b8b4b64cb9 100644 --- a/server/src/repositories/maintenance-worker.repository.ts +++ b/server/src/repositories/maintenance-worker.repository.ts @@ -11,7 +11,7 @@ import { IncomingHttpHeaders } from 'node:http'; import { Server, Socket } from 'socket.io'; import { MaintenanceAuthDto, MaintenanceModeResponseDto } from 'src/dtos/maintenance.dto'; import { ExitCode, ImmichCookie, SystemMetadataKey } from 'src/enum'; -import { ArgsOf } from 'src/repositories/event.repository'; +import { AppRestartEvent, ArgsOf } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { MaintenanceModeState } from 'src/types'; @@ -26,7 +26,7 @@ export const serverEvents = ['AppRestart'] as const; export type ServerEvents = (typeof serverEvents)[number]; export interface ClientEventMap { - on_server_restart: [MaintenanceModeResponseDto]; + AppRestartV1: [AppRestartEvent]; } @WebSocketGateway({ @@ -91,10 +91,10 @@ export class MaintenanceWorkerRepository implements OnGatewayConnection, OnGatew async authenticate(headers: IncomingHttpHeaders): Promise { const jwtToken = parse(headers.cookie || '')[ImmichCookie.MaintenanceToken]; - return this.decodeToken(jwtToken); + return this.authenticateToken(jwtToken); } - async decodeToken(jwtToken?: string): Promise { + async authenticateToken(jwtToken?: string): Promise { if (!jwtToken) { throw new UnauthorizedException('Missing JWT Token'); } @@ -114,7 +114,7 @@ export class MaintenanceWorkerRepository implements OnGatewayConnection, OnGatew } restartApp(state: MaintenanceModeResponseDto) { - this.clientBroadcast('on_server_restart', state); + this.clientBroadcast('AppRestartV1', state); this.serverSend('AppRestart', state); this.exitApp(); } diff --git a/server/src/repositories/maintenance.repository.ts b/server/src/repositories/maintenance.repository.ts index b8832bc0f3..e1b883a830 100644 --- a/server/src/repositories/maintenance.repository.ts +++ b/server/src/repositories/maintenance.repository.ts @@ -43,7 +43,7 @@ export class MaintenanceRepository { server.adapter(createAdapter(pubClient, subClient)); // => corresponds to notification.service.ts#onAppRestart - server.emit('on_server_restart', state, () => { + server.emit('AppRestartV1', state, () => { server.serverSideEmit('AppRestart', state, () => { pubClient.disconnect(); subClient.disconnect(); diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index aacc5455e5..d87bf76351 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -9,11 +9,10 @@ import { import { Server, Socket } from 'socket.io'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MaintenanceModeResponseDto } from 'src/dtos/maintenance.dto'; import { NotificationDto } from 'src/dtos/notification.dto'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto'; -import { ArgsOf, EventRepository } from 'src/repositories/event.repository'; +import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { handlePromiseError } from 'src/utils/misc'; @@ -31,13 +30,13 @@ export interface ClientEventMap { on_asset_stack_update: string[]; on_person_thumbnail: [string]; on_server_version: [ServerVersionResponseDto]; - on_server_restart: [MaintenanceModeResponseDto]; on_config_update: []; on_new_release: [ReleaseNotification]; on_notification: [NotificationDto]; on_session_delete: [string]; AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; + AppRestartV1: [AppRestartEvent]; } export type AuthFn = (client: Socket) => Promise; diff --git a/server/src/services/maintenance-worker.service.ts b/server/src/services/maintenance-worker.service.ts index b3f3350ead..ecdc4ae7f6 100644 --- a/server/src/services/maintenance-worker.service.ts +++ b/server/src/services/maintenance-worker.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker.repository'; @@ -49,6 +50,18 @@ export class MaintenanceWorkerService { }; } + logSecret() { + void this.maintenanceRepository.logSecret(); + } + + login(jwt?: string): Promise { + return this.maintenanceRepository.authenticateToken(jwt); + } + + startMaintenance(): Promise { + throw new BadRequestException('Already in maintenance mode'); + } + async endMaintenance(): Promise { await this.maintenanceRepository.exitMaintenanceMode(); } diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 1218d7ab4c..ee87fcf775 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -116,7 +116,7 @@ export class NotificationService extends BaseService { @OnEvent({ name: 'AppRestart' }) onAppRestart(state: ArgOf<'AppRestart'>) { - this.websocketRepository.clientBroadcast('on_server_restart', { + this.websocketRepository.clientBroadcast('AppRestartV1', { isMaintenanceMode: state.isMaintenanceMode, }); diff --git a/server/src/workers/maintenance.ts b/server/src/workers/maintenance.ts index c287bb6dc5..91be662bcc 100644 --- a/server/src/workers/maintenance.ts +++ b/server/src/workers/maintenance.ts @@ -3,23 +3,21 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { configureExpress, configureTelemetry } from 'src/app.common'; import { MaintenanceModule } from 'src/app.module'; import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker.repository'; +import { MaintenanceWorkerService } from 'src/services/maintenance-worker.service'; import { isStartUpError } from 'src/utils/misc'; async function bootstrap() { process.title = 'immich-maintenance'; - configureTelemetry(); const app = await NestFactory.create(MaintenanceModule, { bufferLogs: true }); - const maintenanceWorkerRepository = app.get(MaintenanceWorkerRepository); - maintenanceWorkerRepository.setCloseFn(() => app.close()); - + app.get(MaintenanceWorkerRepository).setCloseFn(() => app.close()); void configureExpress(app, { permitSwaggerWrite: false, ssr: false, }); - void maintenanceWorkerRepository.logSecret(); + app.get(MaintenanceWorkerService).logSecret(); } bootstrap().catch((error) => { diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index e88cf6d831..9f01c6878e 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -3,12 +3,7 @@ import { AppRoute } from '$lib/constants'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { notificationManager } from '$lib/stores/notification-manager.svelte'; import { createEventEmitter } from '$lib/utils/eventemitter'; -import { - type AssetResponseDto, - type MaintenanceModeResponseDto, - type NotificationDto, - type ServerVersionResponseDto, -} from '@immich/sdk'; +import { type AssetResponseDto, type NotificationDto, type ServerVersionResponseDto } from '@immich/sdk'; import { io, type Socket } from 'socket.io-client'; import { get, writable } from 'svelte/store'; import { user } from './user.store'; @@ -20,6 +15,11 @@ export interface ReleaseEvent { serverVersion: ServerVersionResponseDto; releaseVersion: ServerVersionResponseDto; } + +interface AppRestartEvent { + isMaintenanceMode: boolean; +} + export interface Events { on_upload_success: (asset: AssetResponseDto) => void; on_user_delete: (id: string) => void; @@ -31,11 +31,12 @@ export interface Events { on_asset_stack_update: (assetIds: string[]) => void; on_person_thumbnail: (personId: string) => void; on_server_version: (serverVersion: ServerVersionResponseDto) => void; - on_server_restart: (maintenanceMode: MaintenanceModeResponseDto) => void; on_config_update: () => void; on_new_release: (newRelease: ReleaseEvent) => void; on_session_delete: (sessionId: string) => void; on_notification: (notification: NotificationDto) => void; + + AppRestartV1: (event: AppRestartEvent) => void; } const websocket: Socket = io({ @@ -50,7 +51,7 @@ export const websocketStore = { connected: writable(false), serverVersion: writable(), release: writable(), - serverRestarting: writable(), + serverRestarting: writable(), }; export const websocketEvents = createEventEmitter(websocket); @@ -59,7 +60,7 @@ websocket .on('connect', () => websocketStore.connected.set(true)) .on('disconnect', () => websocketStore.connected.set(false)) .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion)) - .on('on_server_restart', (mode) => websocketStore.serverRestarting.set(mode)) + .on('AppRestartV1', (mode) => websocketStore.serverRestarting.set(mode)) .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion)) .on('on_session_delete', () => authManager.logout()) .on('on_notification', () => notificationManager.refresh())