mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
fix: shared check for server setup availability
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -38,9 +38,6 @@ export const errorDto = {
|
||||
incorrectLogin: {
|
||||
message: 'Incorrect email or password',
|
||||
},
|
||||
alreadyHasAdmin: {
|
||||
message: 'The server already has an admin',
|
||||
},
|
||||
};
|
||||
|
||||
export const signupResponseDto = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,15 +197,7 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
|
||||
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,
|
||||
|
||||
@@ -280,6 +280,17 @@ export class BaseService {
|
||||
return checkAccess(this.accessRepository, request);
|
||||
}
|
||||
|
||||
async isSetupAvailable(): Promise<boolean> {
|
||||
const { setup } = this.configRepository.getEnv();
|
||||
return setup.allow && !(await this.userRepository.hasAdmin());
|
||||
}
|
||||
|
||||
async requireSetupAvailable(): Promise<void> {
|
||||
if (!(await this.isSetupAvailable())) {
|
||||
throw new BadRequestException('Admin setup is not available');
|
||||
}
|
||||
}
|
||||
|
||||
async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserAdmin> {
|
||||
const exists = await this.userRepository.getByEmail(dto.email);
|
||||
if (exists) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -161,7 +161,7 @@ describe(ServerService.name, () => {
|
||||
oauthButtonText: 'Login with OAuth',
|
||||
trashDays: 30,
|
||||
userDeleteDelay: 7,
|
||||
isInitialized: undefined,
|
||||
isInitialized: false,
|
||||
isOnboarded: false,
|
||||
externalDomain: '',
|
||||
publicUsers: true,
|
||||
|
||||
@@ -111,9 +111,8 @@ export class ServerService extends BaseService {
|
||||
}
|
||||
|
||||
async getSystemConfig(): Promise<ServerConfigDto> {
|
||||
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 {
|
||||
|
||||
@@ -35,7 +35,4 @@ export const errorDto = {
|
||||
incorrectLogin: {
|
||||
message: 'Incorrect email or password',
|
||||
},
|
||||
alreadyHasAdmin: {
|
||||
message: 'The server already has an admin',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user