diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index dbfd2fb112..c10a858ed9 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -45,7 +45,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 | +| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` and `/admin/database-backups/start-restore` endpoints | `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/e2e/src/responses.ts b/e2e/src/responses.ts index 5fd887c44b..1b3447767e 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -38,9 +38,6 @@ export const errorDto = { incorrectLogin: { message: 'Incorrect email or password', }, - alreadyHasAdmin: { - message: 'The server already has an admin', - }, }; export const signupResponseDto = { diff --git a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts index cf6d752561..e757c721d6 100644 --- a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts @@ -108,7 +108,7 @@ describe('/admin/database-backups', () => { const { status, body } = await request(app).post('/admin/database-backups/start-restore').send(); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('The server already has an admin')); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); }); it.sequential('should enter maintenance mode in "database restore mode"', async () => { diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index dad13ef5f5..3b9c719908 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -14,6 +14,7 @@ import { UserFactory } from 'test/factories/user.factory'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -308,15 +309,25 @@ describe(AuthService.name, () => { const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' }; it('should only allow one admin', async () => { - mocks.user.getAdmin.mockResolvedValue({} as UserAdmin); + mocks.user.hasAdmin.mockResolvedValue(true); await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.user.getAdmin).toHaveBeenCalled(); + expect(mocks.user.hasAdmin).toHaveBeenCalled(); + expect(mocks.user.create).not.toHaveBeenCalled(); + }); + + it('should not allow signup when setup is disabled', async () => { + mocks.config.getEnv.mockReturnValue(mockEnvData({ setup: { allow: false } })); + mocks.user.hasAdmin.mockResolvedValue(false); + + await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException); + + expect(mocks.user.create).not.toHaveBeenCalled(); }); it('should sign up the admin', async () => { - mocks.user.getAdmin.mockResolvedValue(void 0); + mocks.user.hasAdmin.mockResolvedValue(false); mocks.user.create.mockResolvedValue({ ...userStub.admin, ...dto, @@ -334,7 +345,7 @@ describe(AuthService.name, () => { name: 'immich admin', }); - expect(mocks.user.getAdmin).toHaveBeenCalled(); + expect(mocks.user.hasAdmin).toHaveBeenCalled(); expect(mocks.user.create).toHaveBeenCalled(); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index f3be40f7dd..0456c21d95 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -197,15 +197,7 @@ 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'); - } + await this.requireSetupAvailable(); const admin = await this.createUser({ isAdmin: true, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 6d17410441..2195c66a22 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -280,6 +280,17 @@ export class BaseService { return checkAccess(this.accessRepository, request); } + async isSetupAvailable(): Promise { + const { setup } = this.configRepository.getEnv(); + return setup.allow && !(await this.userRepository.hasAdmin()); + } + + async requireSetupAvailable(): Promise { + if (!(await this.isSetupAvailable())) { + throw new BadRequestException('Admin setup is not available'); + } + } + async createUser(dto: Insertable & { email: string }): Promise { const exists = await this.userRepository.getByEmail(dto.email); if (exists) { diff --git a/server/src/services/maintenance.service.spec.ts b/server/src/services/maintenance.service.spec.ts index e598f1c71d..dfe7fbf778 100644 --- a/server/src/services/maintenance.service.spec.ts +++ b/server/src/services/maintenance.service.spec.ts @@ -1,5 +1,6 @@ import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { MaintenanceService } from 'src/services/maintenance.service'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTestService, ServiceMocks } from 'test/utils'; describe(MaintenanceService.name, () => { @@ -134,6 +135,42 @@ describe(MaintenanceService.name, () => { }); }); + describe('startRestoreFlow', () => { + it('should fail if setup is disabled', async () => { + mocks.config.getEnv.mockReturnValue(mockEnvData({ setup: { allow: false } })); + mocks.user.hasAdmin.mockResolvedValue(false); + + await expect(sut.startRestoreFlow()).rejects.toThrowError('Admin setup is not available'); + + expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); + expect(mocks.event.emit).not.toHaveBeenCalled(); + }); + + it('should fail if the server already has an admin', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.startRestoreFlow()).rejects.toThrowError('Admin setup is not available'); + + expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); + expect(mocks.event.emit).not.toHaveBeenCalled(); + }); + + it('should start maintenance mode and return a jwt', async () => { + mocks.user.hasAdmin.mockResolvedValue(false); + mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); + + await expect(sut.startRestoreFlow()).resolves.toMatchObject({ jwt: expect.any(String) }); + + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: expect.stringMatching(/^\w{128}$/), + action: { + action: MaintenanceAction.SelectDatabaseRestore, + }, + }); + }); + }); + describe('createLoginUrl', () => { it('should fail outside of maintenance mode without secret', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index ca1e05d93f..f8780bad32 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; import { MaintenanceAuthDto, @@ -59,10 +59,7 @@ export class MaintenanceService extends BaseService { } async startRestoreFlow(): Promise<{ jwt: string }> { - const adminUser = await this.userRepository.getAdmin(); - if (adminUser) { - throw new BadRequestException('The server already has an admin'); - } + await this.requireSetupAvailable(); return this.startMaintenance( { diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index e1575a496a..1b93fcea78 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -161,7 +161,7 @@ describe(ServerService.name, () => { oauthButtonText: 'Login with OAuth', trashDays: 30, userDeleteDelay: 7, - isInitialized: undefined, + isInitialized: false, isOnboarded: false, externalDomain: '', publicUsers: true, diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index ad212292ba..57342f9509 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -111,9 +111,8 @@ export class ServerService extends BaseService { } async getSystemConfig(): Promise { - const { setup } = this.configRepository.getEnv(); const config = await this.getConfig({ withCache: false }); - const isInitialized = !setup.allow || (await this.userRepository.hasAdmin()); + const isInitialized = !(await this.isSetupAvailable()); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts index b416b3b904..9b75bc85ec 100644 --- a/server/test/medium/responses.ts +++ b/server/test/medium/responses.ts @@ -35,7 +35,4 @@ export const errorDto = { incorrectLogin: { message: 'Incorrect email or password', }, - alreadyHasAdmin: { - message: 'The server already has an admin', - }, }; diff --git a/server/test/medium/specs/services/auth.service.spec.ts b/server/test/medium/specs/services/auth.service.spec.ts index 1fc306f790..8208137e00 100644 --- a/server/test/medium/specs/services/auth.service.spec.ts +++ b/server/test/medium/specs/services/auth.service.spec.ts @@ -65,7 +65,7 @@ describe(AuthService.name, () => { const response = sut.adminSignUp(dto); await expect(response).rejects.toThrow(BadRequestException); - await expect(response).rejects.toThrow('The server already has an admin'); + await expect(response).rejects.toThrow('Admin setup is not available'); }); });