diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/api/specs/system-config.e2e-spec.ts index c223df4874..8cf389134b 100644 --- a/e2e/src/api/specs/system-config.e2e-spec.ts +++ b/e2e/src/api/specs/system-config.e2e-spec.ts @@ -1,10 +1,12 @@ -import { LoginResponseDto } from '@immich/sdk'; +import { LoginResponseDto, getConfig } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { app, utils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; +const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }); + describe('/system-config', () => { let admin: LoginResponseDto; let nonAdmin: LoginResponseDto; @@ -60,4 +62,25 @@ describe('/system-config', () => { expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); }); }); + + describe('PUT /system-config', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put('/system-config'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should reject an invalid config entry', async () => { + const { status, body } = await request(app) + .put('/system-config') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + ...(await getSystemConfig(admin.accessToken)), + storageTemplate: { enabled: true, hashVerificationEnabled: true, template: '{{foo}}' }, + }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(expect.stringContaining('Invalid storage template'))); + }); + }); }); diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 33efdafa39..b913fe00f0 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -1,4 +1,6 @@ import { SetMetadata } from '@nestjs/common'; +import { OnEvent, OnEventType } from '@nestjs/event-emitter'; +import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; import _ from 'lodash'; import { setUnion } from 'src/utils/set'; @@ -122,3 +124,6 @@ export interface GenerateSqlQueries { /** Decorator to enable versioning/tracking of generated Sql */ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); + +export const OnEventInternal = (event: OnEventType, options?: OnEventOptions) => + OnEvent(event, { suppressErrors: false, ...options }); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index e432a59404..d46d40edf7 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { Trie } from 'mnemonist'; import { R_OK } from 'node:constants'; import { EventEmitter } from 'node:events'; @@ -8,6 +7,7 @@ import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEventInternal } from 'src/decorators'; import { CreateLibraryDto, LibraryResponseDto, @@ -105,7 +105,7 @@ export class LibraryService extends EventEmitter { }); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) + @OnEventInternal(InternalEvent.VALIDATE_CONFIG) validateConfig({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { const { scan } = newConfig.library; if (!validateCronExpression(scan.cronExpression)) { diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index f2901804fa..39a0196f2b 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import handlebar from 'handlebars'; import { DateTime } from 'luxon'; import path from 'node:path'; @@ -15,6 +14,7 @@ import { } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEventInternal } from 'src/decorators'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; import { SystemConfig } from 'src/entities/system-config.entity'; @@ -86,7 +86,7 @@ export class StorageTemplateService { ); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) + @OnEventInternal(InternalEvent.VALIDATE_CONFIG) validate({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 94661c32e3..0067832a16 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; import { @@ -13,6 +12,7 @@ import { supportedYearTokens, } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEventInternal } from 'src/decorators'; import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config-storage-template.dto'; import { SystemConfigDto, mapConfig } from 'src/dtos/system-config.dto'; import { LogLevel, SystemConfig } from 'src/entities/system-config.entity'; @@ -61,7 +61,7 @@ export class SystemConfigService { return mapConfig(config); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) + @OnEventInternal(InternalEvent.VALIDATE_CONFIG) validateConfig({ newConfig, oldConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.');