feat: add MaintenanceEphemeralStateRepository

refactor: cache the secret in memory
This commit is contained in:
izzy
2025-11-19 15:41:27 +00:00
parent 56c93a71c0
commit 7c2e8b1d62
4 changed files with 48 additions and 29 deletions

View File

@@ -9,8 +9,9 @@ import { commandsAndQuestions } from 'src/commands';
import { IWorker } from 'src/constants';
import { controllers } from 'src/controllers';
import { StorageCore } from 'src/cores/storage.core';
import { ImmichWorker } from 'src/enum';
import { ImmichWorker, SystemMetadataKey } from 'src/enum';
import { MaintenanceAuthGuard } from 'src/maintenance/maintenance-auth.guard';
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
import { MaintenanceWorkerController } from 'src/maintenance/maintenance-worker.controller';
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
@@ -34,6 +35,7 @@ import { services } from 'src/services';
import { AuthService } from 'src/services/auth.service';
import { CliService } from 'src/services/cli.service';
import { QueueService } from 'src/services/queue.service';
import { MaintenanceModeState } from 'src/types';
import { getKyselyConfig } from 'src/utils/database';
const common = [...repositories, ...services, GlobalExceptionFilter];
@@ -113,6 +115,7 @@ export class ApiModule extends BaseModule {}
SystemMetadataRepository,
AppRepository,
MaintenanceWebsocketRepository,
MaintenanceEphemeralStateRepository,
MaintenanceWorkerService,
...commonMiddleware,
{ provide: APP_GUARD, useClass: MaintenanceAuthGuard },
@@ -123,13 +126,20 @@ export class MaintenanceModule {
constructor(
@Inject(IWorker) private worker: ImmichWorker,
logger: LoggingRepository,
private systemMetadataRepository: SystemMetadataRepository,
private maintenanceWorkerService: MaintenanceWorkerService,
private maintenanceWebsocketRepository: MaintenanceWebsocketRepository,
private maintenanceEphemeralStateRepository: MaintenanceEphemeralStateRepository,
) {
logger.setAppName(this.worker);
}
async onModuleInit() {
const state = (await this.systemMetadataRepository.get(
SystemMetadataKey.MaintenanceMode,
)) as MaintenanceModeState & { isMaintenanceMode: true };
this.maintenanceEphemeralStateRepository.setSecret(state.secret);
StorageCore.setMediaLocation(this.maintenanceWorkerService.detectMediaLocation());
this.maintenanceWebsocketRepository.setAuthFn(async (client) =>

View File

@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class MaintenanceEphemeralStateRepository {
#secret: string = null!;
setSecret(secret: string) {
this.#secret = secret;
}
getSecret() {
return this.#secret;
}
}

View File

@@ -1,6 +1,7 @@
import { UnauthorizedException } from '@nestjs/common';
import { SignJWT } from 'jose';
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
import { automock, getMocks, ServiceMocks } from 'test/utils';
@@ -8,17 +9,28 @@ import { automock, getMocks, ServiceMocks } from 'test/utils';
describe(MaintenanceWorkerService.name, () => {
let sut: MaintenanceWorkerService;
let mocks: ServiceMocks;
let maintenanceWorkerRepositoryMock: MaintenanceWebsocketRepository;
let maintenanceWebsocketRepositoryMock: MaintenanceWebsocketRepository;
let maintenanceEphemeralStateRepositoryMock: MaintenanceEphemeralStateRepository;
beforeEach(() => {
mocks = getMocks();
maintenanceWorkerRepositoryMock = automock(MaintenanceWebsocketRepository, { args: [mocks.logger], strict: false });
maintenanceWebsocketRepositoryMock = automock(MaintenanceWebsocketRepository, {
args: [mocks.logger],
strict: false,
});
maintenanceEphemeralStateRepositoryMock = automock(MaintenanceEphemeralStateRepository, {
args: [mocks.logger],
strict: false,
});
sut = new MaintenanceWorkerService(
mocks.logger as never,
mocks.app,
mocks.config,
mocks.systemMetadata as never,
maintenanceWorkerRepositoryMock,
maintenanceWebsocketRepositoryMock,
maintenanceEphemeralStateRepositoryMock,
mocks.storage as never,
mocks.process,
mocks.database as never,
@@ -144,11 +156,11 @@ describe(MaintenanceWorkerService.name, () => {
isMaintenanceMode: false,
});
expect(maintenanceWorkerRepositoryMock.clientBroadcast).toHaveBeenCalledWith('AppRestartV1', {
expect(maintenanceWebsocketRepositoryMock.clientBroadcast).toHaveBeenCalledWith('AppRestartV1', {
isMaintenanceMode: false,
});
expect(maintenanceWorkerRepositoryMock.serverSend).toHaveBeenCalledWith('AppRestart', {
expect(maintenanceWebsocketRepositoryMock.serverSend).toHaveBeenCalledWith('AppRestart', {
isMaintenanceMode: false,
});
});

View File

@@ -5,7 +5,9 @@ import { jwtVerify } from 'jose';
import { readFileSync } from 'node:fs';
import { IncomingHttpHeaders } from 'node:http';
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
import { ServerConfigDto } from 'src/dtos/server.dto';
import { ImmichCookie, SystemMetadataKey } from 'src/enum';
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
import { AppRepository } from 'src/repositories/app.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
@@ -34,6 +36,7 @@ export class MaintenanceWorkerService {
private configRepository: ConfigRepository,
private systemMetadataRepository: SystemMetadataRepository,
private maintenanceWorkerRepository: MaintenanceWebsocketRepository,
private maintenanceEphemeralStateRepository: MaintenanceEphemeralStateRepository,
private storageRepository: StorageRepository,
private processRepository: ProcessRepository,
private databaseRepository: DatabaseRepository,
@@ -63,21 +66,9 @@ export class MaintenanceWorkerService {
* {@link _ServerService.getSystemConfig}
*/
async getSystemConfig() {
const config = await this.getConfig({ withCache: false });
return {
loginPageMessage: config.server.loginPageMessage,
trashDays: config.trash.days,
userDeleteDelay: config.user.deleteDelay,
oauthButtonText: config.oauth.buttonText,
isInitialized: true,
isOnboarded: true,
externalDomain: config.server.externalDomain,
publicUsers: config.server.publicUsers,
mapDarkStyleUrl: config.map.darkStyle,
mapLightStyleUrl: config.map.lightStyle,
maintenanceMode: true,
};
} as ServerConfigDto;
}
/**
@@ -139,14 +130,6 @@ export class MaintenanceWorkerService {
return '/usr/src/app/upload';
}
private async secret(): Promise<string> {
const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) as {
secret: string;
};
return state.secret;
}
async logSecret(): Promise<void> {
const { server } = await this.getConfig({ withCache: true });
@@ -156,7 +139,7 @@ export class MaintenanceWorkerService {
{
username: 'immich-admin',
},
await this.secret(),
this.maintenanceEphemeralStateRepository.getSecret(),
);
this.logger.log(`\n\n🚧 Immich is in maintenance mode, you can log in using the following URL:\n${url}\n`);
@@ -172,7 +155,7 @@ export class MaintenanceWorkerService {
throw new UnauthorizedException('Missing JWT Token');
}
const secret = await this.secret();
const secret = this.maintenanceEphemeralStateRepository.getSecret();
try {
const result = await jwtVerify<MaintenanceAuthDto>(jwt, new TextEncoder().encode(secret));