mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
refactor: move static methods into util
This commit is contained in:
parent
db22797ae6
commit
36baeb603f
7 changed files with 92 additions and 95 deletions
|
|
@ -20,11 +20,7 @@ export class MaintenanceController {
|
|||
@Post('start')
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async startMaintenance(@Auth() auth: AuthDto, @Res({ passthrough: true }) response: Response): Promise<void> {
|
||||
const { secret } = await this.service.startMaintenance();
|
||||
const jwt = await this.service.createJwt(secret, {
|
||||
username: auth.user.name,
|
||||
});
|
||||
|
||||
const { jwt } = await this.service.startMaintenance(auth);
|
||||
response.cookie(ImmichCookie.MaintenanceToken, jwt);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import Redis from 'ioredis';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
import { ExitCode } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
|
||||
@Injectable()
|
||||
export class MaintenanceRepository {
|
||||
private closeFn?: () => Promise<void>;
|
||||
|
||||
constructor(private configRepository: ConfigRepository) {}
|
||||
constructor() {}
|
||||
|
||||
exitApp() {
|
||||
/* eslint-disable unicorn/no-process-exit */
|
||||
|
|
@ -24,41 +19,4 @@ export class MaintenanceRepository {
|
|||
setCloseFn(fn: () => Promise<void>) {
|
||||
this.closeFn = fn;
|
||||
}
|
||||
|
||||
sendOneShotAppRestart(state: AppRestartEvent): void {
|
||||
const server = new SocketIO();
|
||||
const pubClient = new Redis(this.configRepository.getEnv().redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
/**
|
||||
* Keep trying until we manage to stop Immich
|
||||
*
|
||||
* Sometimes there appear to be communication
|
||||
* issues between to the other servers.
|
||||
*
|
||||
* This issue only occurs with this method.
|
||||
*/
|
||||
function tryTerminate(cb: () => void) {
|
||||
server.serverSideEmit('AppRestart', state, (arg0: null | any, responses: any[]) => {
|
||||
if (arg0 === null && responses.length === 0) {
|
||||
console.info(
|
||||
"\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.",
|
||||
);
|
||||
|
||||
setTimeout(() => tryTerminate(cb), 1e3);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
server.emit('AppRestartV1', state, () => {
|
||||
tryTerminate(() => {
|
||||
pubClient.disconnect();
|
||||
subClient.disconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,10 +102,6 @@ describe(CliService.name, () => {
|
|||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, {
|
||||
isMaintenanceMode: false,
|
||||
});
|
||||
|
||||
expect(mocks.maintenance.sendOneShotAppRestart).toHaveBeenCalledWith({
|
||||
isMaintenanceMode: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -134,10 +130,6 @@ describe(CliService.name, () => {
|
|||
isMaintenanceMode: true,
|
||||
secret: expect.stringMatching(/^\w{128}$/),
|
||||
});
|
||||
|
||||
expect(mocks.maintenance.sendOneShotAppRestart).toHaveBeenCalledWith({
|
||||
isMaintenanceMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
|||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, sendOneShotAppRestart } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -56,7 +56,7 @@ export class CliService extends BaseService {
|
|||
const state = { isMaintenanceMode: false as const };
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state);
|
||||
|
||||
this.maintenanceRepository.sendOneShotAppRestart(state);
|
||||
sendOneShotAppRestart(state);
|
||||
|
||||
return {
|
||||
alreadyDisabled: false,
|
||||
|
|
@ -77,24 +77,24 @@ export class CliService extends BaseService {
|
|||
|
||||
if (state.isMaintenanceMode) {
|
||||
return {
|
||||
authUrl: await MaintenanceService.createLoginUrl(baseUrl, payload, state.secret),
|
||||
authUrl: await createMaintenanceLoginUrl(baseUrl, payload, state.secret),
|
||||
alreadyEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
const secret = MaintenanceService.generateSecret();
|
||||
const secret = generateMaintenanceSecret();
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
|
||||
isMaintenanceMode: true,
|
||||
secret,
|
||||
});
|
||||
|
||||
this.maintenanceRepository.sendOneShotAppRestart({
|
||||
sendOneShotAppRestart({
|
||||
isMaintenanceMode: true,
|
||||
});
|
||||
|
||||
return {
|
||||
authUrl: await MaintenanceService.createLoginUrl(baseUrl, payload, secret),
|
||||
authUrl: await createMaintenanceLoginUrl(baseUrl, payload, secret),
|
||||
alreadyEnabled: false,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker
|
|||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { type ApiService as _ApiService } from 'src/services/api.service';
|
||||
import { type BaseService as _BaseService } from 'src/services/base.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
import { type ServerService as _ServerService } from 'src/services/server.service';
|
||||
import { MaintenanceModeState } from 'src/types';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
import { createMaintenanceLoginUrl } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
/**
|
||||
|
|
@ -116,7 +116,7 @@ export class MaintenanceWorkerService {
|
|||
const { server } = await this.getConfig({ withCache: true });
|
||||
|
||||
const baseUrl = getExternalDomain(server);
|
||||
const url = await MaintenanceService.createLoginUrl(
|
||||
const url = await createMaintenanceLoginUrl(
|
||||
baseUrl,
|
||||
{
|
||||
username: 'immich-admin',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { SignJWT } from 'jose';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { MaintenanceModeState } from 'src/types';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
/**
|
||||
|
|
@ -23,17 +23,21 @@ export class MaintenanceService extends BaseService {
|
|||
throw new BadRequestException('Not in maintenance mode');
|
||||
}
|
||||
|
||||
async startMaintenance(): Promise<{ secret: string }> {
|
||||
async startMaintenance(auth: AuthDto): Promise<{ jwt: string }> {
|
||||
const { isMaintenanceMode } = await this.getMaintenanceMode();
|
||||
if (isMaintenanceMode) {
|
||||
throw new BadRequestException('Already in maintenance mode');
|
||||
}
|
||||
|
||||
const secret = MaintenanceService.generateSecret();
|
||||
const secret = generateMaintenanceSecret();
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret });
|
||||
await this.eventRepository.emit('AppRestart', { isMaintenanceMode: true });
|
||||
|
||||
return { secret };
|
||||
return {
|
||||
jwt: await signMaintenanceJwt(secret, {
|
||||
username: auth.user.name,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
endMaintenance(): void {
|
||||
|
|
@ -49,36 +53,15 @@ export class MaintenanceService extends BaseService {
|
|||
const { server } = await this.getConfig({ withCache: true });
|
||||
const baseUrl = getExternalDomain(server);
|
||||
|
||||
secret ??= await this.getMaintenanceMode().then((state) => {
|
||||
if (!secret) {
|
||||
const state = await this.getMaintenanceMode();
|
||||
if (!state.isMaintenanceMode) {
|
||||
throw new Error('Not in maintenance mode');
|
||||
}
|
||||
|
||||
return state.secret;
|
||||
});
|
||||
secret = state.secret;
|
||||
}
|
||||
|
||||
return await MaintenanceService.createLoginUrl(baseUrl, auth, secret!);
|
||||
}
|
||||
|
||||
createJwt(secret: string, data: MaintenanceAuthDto): Promise<string> {
|
||||
return MaintenanceService.createJwt(secret, data);
|
||||
}
|
||||
|
||||
static async createLoginUrl(baseUrl: string, auth: MaintenanceAuthDto, secret: string): Promise<string> {
|
||||
return `${baseUrl}/maintenance?token=${await MaintenanceService.createJwt(secret!, auth)}`;
|
||||
}
|
||||
|
||||
static async createJwt(secret: string, data: MaintenanceAuthDto): Promise<string> {
|
||||
const alg = 'HS256';
|
||||
|
||||
return await new SignJWT({ ...data })
|
||||
.setProtectedHeader({ alg })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('4h')
|
||||
.sign(new TextEncoder().encode(secret));
|
||||
}
|
||||
|
||||
static generateSecret(): string {
|
||||
return randomBytes(64).toString('hex');
|
||||
return await createMaintenanceLoginUrl(baseUrl, auth, secret!);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
server/src/utils/maintenance.ts
Normal file
68
server/src/utils/maintenance.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import Redis from 'ioredis';
|
||||
import { SignJWT } from 'jose';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
|
||||
export function sendOneShotAppRestart(state: AppRestartEvent): void {
|
||||
const server = new SocketIO();
|
||||
const { redis } = new ConfigRepository().getEnv();
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
/**
|
||||
* Keep trying until we manage to stop Immich
|
||||
*
|
||||
* Sometimes there appear to be communication
|
||||
* issues between to the other servers.
|
||||
*
|
||||
* This issue only occurs with this method.
|
||||
*/
|
||||
function tryTerminate(cb: () => void) {
|
||||
server.serverSideEmit('AppRestart', state, (arg0: null | any, responses: any[]) => {
|
||||
if (arg0 === null && responses.length === 0) {
|
||||
console.info(
|
||||
"\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.",
|
||||
);
|
||||
|
||||
setTimeout(() => tryTerminate(cb), 1e3);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
server.emit('AppRestartV1', state, () => {
|
||||
tryTerminate(() => {
|
||||
pubClient.disconnect();
|
||||
subClient.disconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMaintenanceLoginUrl(
|
||||
baseUrl: string,
|
||||
auth: MaintenanceAuthDto,
|
||||
secret: string,
|
||||
): Promise<string> {
|
||||
return `${baseUrl}/maintenance?token=${await signMaintenanceJwt(secret!, auth)}`;
|
||||
}
|
||||
|
||||
export async function signMaintenanceJwt(secret: string, data: MaintenanceAuthDto): Promise<string> {
|
||||
const alg = 'HS256';
|
||||
|
||||
return await new SignJWT({ ...data })
|
||||
.setProtectedHeader({ alg })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('4h')
|
||||
.sign(new TextEncoder().encode(secret));
|
||||
}
|
||||
|
||||
export function generateMaintenanceSecret(): string {
|
||||
return randomBytes(64).toString('hex');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue