refactor: use service instead of repository

chore: read token from cookie if possible
chore: rename client event to AppRestartV1
This commit is contained in:
izzy 2025-11-06 15:35:17 +00:00
parent f2b941b066
commit c8f3eef45d
No known key found for this signature in database
GPG key ID: 5059F398521BB0F6
9 changed files with 51 additions and 44 deletions

View file

@ -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<ServerConfigDto> {
return this.service.getSystemConfig();
}
@Post('admin/maintenance/start')
@MaintenanceRoute()
startMaintenance(): Promise<MaintenanceModeResponseDto> {
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<MaintenanceAuthDto> {
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<MaintenanceModeResponseDto> {
async endMaintenance(): Promise<void> {
await this.service.endMaintenance();
return { isMaintenanceMode: false };
}
}

View file

@ -7,7 +7,7 @@ export class MaintenanceModeResponseDto {
export class MaintenanceLoginDto {
@ValidateString()
token!: string;
token?: string;
}
export class MaintenanceAuthDto {

View file

@ -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<MaintenanceAuthDto> {
const jwtToken = parse(headers.cookie || '')[ImmichCookie.MaintenanceToken];
return this.decodeToken(jwtToken);
return this.authenticateToken(jwtToken);
}
async decodeToken(jwtToken?: string): Promise<MaintenanceAuthDto> {
async authenticateToken(jwtToken?: string): Promise<MaintenanceAuthDto> {
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();
}

View file

@ -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();

View file

@ -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<AuthDto>;

View file

@ -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<MaintenanceAuthDto> {
return this.maintenanceRepository.authenticateToken(jwt);
}
startMaintenance(): Promise<void> {
throw new BadRequestException('Already in maintenance mode');
}
async endMaintenance(): Promise<void> {
await this.maintenanceRepository.exitMaintenanceMode();
}

View file

@ -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,
});

View file

@ -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<NestExpressApplication>(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) => {

View file

@ -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<Events> = io({
@ -50,7 +51,7 @@ export const websocketStore = {
connected: writable<boolean>(false),
serverVersion: writable<ServerVersionResponseDto>(),
release: writable<ReleaseEvent>(),
serverRestarting: writable<undefined | MaintenanceModeResponseDto>(),
serverRestarting: writable<undefined | AppRestartEvent>(),
};
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())