From 224841328057a7b3f5403421a601836499ec808a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 16 Dec 2025 15:18:24 -0500 Subject: [PATCH] feat: disable admin setup --- docs/docs/install/environment-variables.md | 1 + server/src/dtos/env.dto.ts | 5 ++- .../repositories/config.repository.spec.ts | 31 +++++++++++++++++++ server/src/repositories/config.repository.ts | 20 +++++++++--- server/src/services/auth.service.ts | 5 +++ server/src/services/plugin.service.ts | 4 +-- server/src/services/server.service.ts | 3 +- .../repositories/config.repository.mock.ts | 10 ++++-- 8 files changed, 69 insertions(+), 10 deletions(-) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 76784b285a..e9d95cf3fe 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -43,6 +43,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | +| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index 2a9dd8b662..22f3d4dd32 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -58,7 +58,7 @@ export class EnvDto { IMMICH_MICROSERVICES_METRICS_PORT?: number; @ValidateBoolean({ optional: true }) - IMMICH_PLUGINS_ENABLED?: boolean; + IMMICH_ALLOW_EXTERNAL_PLUGINS?: boolean; @Optional() @Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' }) @@ -113,6 +113,9 @@ export class EnvDto { @Optional() IMMICH_THIRD_PARTY_SUPPORT_URL?: string; + @ValidateBoolean({ optional: true }) + IMMICH_ALLOW_SETUP?: boolean; + @IsIPRange({ requireCIDR: false }, { each: true }) @Transform(({ value }) => value && typeof value === 'string' diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 1641850583..a3dc8ba5cb 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -8,6 +8,8 @@ const getEnv = () => { const resetEnv = () => { for (const env of [ + 'IMMICH_ALLOW_EXTERNAL_PLUGINS', + 'IMMICH_ALLOW_SETUP', 'IMMICH_ENV', 'IMMICH_WORKERS_INCLUDE', 'IMMICH_WORKERS_EXCLUDE', @@ -75,6 +77,9 @@ describe('getEnv', () => { configFile: undefined, logLevel: undefined, }); + + expect(config.plugins.external).toEqual({ allow: false }); + expect(config.setup).toEqual({ allow: true }); }); describe('IMMICH_MEDIA_LOCATION', () => { @@ -84,6 +89,32 @@ describe('getEnv', () => { }); }); + describe('IMMICH_ALLOW_EXTERNAL_PLUGINS', () => { + it('should disable plugins', () => { + process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'false'; + const config = getEnv(); + expect(config.plugins.external).toEqual({ allow: false }); + }); + + it('should throw an error for invalid value', () => { + process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'invalid'; + expect(() => getEnv()).toThrowError('IMMICH_ALLOW_EXTERNAL_PLUGINS must be a boolean value'); + }); + }); + + describe('IMMICH_ALLOW_SETUP', () => { + it('should disable setup', () => { + process.env.IMMICH_ALLOW_SETUP = 'false'; + const { setup } = getEnv(); + expect(setup).toEqual({ allow: false }); + }); + + it('should throw an error for invalid value', () => { + process.env.IMMICH_ALLOW_SETUP = 'invalid'; + expect(() => getEnv()).toThrowError('IMMICH_ALLOW_SETUP must be a boolean value'); + }); + }); + describe('database', () => { it('should use defaults', () => { const { database } = getEnv(); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 60ec021b3b..b87fcd2bb8 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -90,6 +90,10 @@ export interface EnvData { redis: RedisOptions; + setup: { + allow: boolean; + }; + telemetry: { apiPort: number; microservicesPort: number; @@ -104,8 +108,10 @@ export interface EnvData { workers: ImmichWorker[]; plugins: { - enabled: boolean; - installFolder?: string; + external: { + allow: boolean; + installFolder?: string; + }; }; noColor: boolean; @@ -313,6 +319,10 @@ const getEnv = (): EnvData => { corePlugin: join(buildFolder, 'corePlugin'), }, + setup: { + allow: dto.IMMICH_ALLOW_SETUP ?? true, + }, + storage: { ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS, mediaLocation: dto.IMMICH_MEDIA_LOCATION, @@ -327,8 +337,10 @@ const getEnv = (): EnvData => { workers, plugins: { - enabled: !!dto.IMMICH_PLUGINS_ENABLED, - installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER, + external: { + allow: dto.IMMICH_ALLOW_EXTERNAL_PLUGINS ?? false, + installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER, + }, }, noColor: !!dto.NO_COLOR, diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 1a68bbfce7..a6580f89dd 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -165,6 +165,11 @@ export class AuthService extends BaseService { } async adminSignUp(dto: SignUpDto): Promise { + const { setup } = this.configRepository.getEnv(); + if (!setup.allow) { + throw new BadRequestException('Admin setup is disabled'); + } + const adminUser = await this.userRepository.getAdmin(); if (adminUser) { throw new BadRequestException('The server already has an admin'); diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index 9336f0003a..588adadf4b 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -80,8 +80,8 @@ export class PluginService extends BaseService { this.logger.log(`Successfully processed core plugin: ${coreManifest.name} (version ${coreManifest.version})`); // Load external plugins - if (plugins.enabled && plugins.installFolder) { - await this.loadExternalPlugins(plugins.installFolder); + if (plugins.external.allow && plugins.external.installFolder) { + await this.loadExternalPlugins(plugins.external.installFolder); } } diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index af4d706061..30bc1f1f0d 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -115,8 +115,9 @@ export class ServerService extends BaseService { } async getSystemConfig(): Promise { + const { setup } = this.configRepository.getEnv(); const config = await this.getConfig({ withCache: false }); - const isInitialized = await this.userRepository.hasAdmin(); + const isInitialized = !setup.allow || (await this.userRepository.hasAdmin()); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index 656027fab5..01e724529c 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -75,6 +75,10 @@ const envData: EnvData = { corePlugin: '/build/corePlugin', }, + setup: { + allow: true, + }, + storage: { ignoreMountCheckErrors: false, }, @@ -88,8 +92,10 @@ const envData: EnvData = { workers: [ImmichWorker.Api, ImmichWorker.Microservices], plugins: { - enabled: true, - installFolder: '/app/data/plugins', + external: { + allow: true, + installFolder: '/app/data/plugins', + }, }, noColor: false,