mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	fix(server): require local admin account (#1070)
This commit is contained in:
		
							
								
								
									
										10
									
								
								mobile/openapi/doc/UserApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/doc/UserApi.md
									
									
									
										generated
									
									
									
								
							@@ -335,7 +335,7 @@ No authorization required
 | 
			
		||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
# **getUserCount**
 | 
			
		||||
> UserCountResponseDto getUserCount()
 | 
			
		||||
> UserCountResponseDto getUserCount(admin)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -344,9 +344,10 @@ No authorization required
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
final api_instance = UserApi();
 | 
			
		||||
final admin = true; // bool | 
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    final result = api_instance.getUserCount();
 | 
			
		||||
    final result = api_instance.getUserCount(admin);
 | 
			
		||||
    print(result);
 | 
			
		||||
} catch (e) {
 | 
			
		||||
    print('Exception when calling UserApi->getUserCount: $e\n');
 | 
			
		||||
@@ -354,7 +355,10 @@ try {
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Parameters
 | 
			
		||||
This endpoint does not need any parameter.
 | 
			
		||||
 | 
			
		||||
Name | Type | Description  | Notes
 | 
			
		||||
------------- | ------------- | ------------- | -------------
 | 
			
		||||
 **admin** | **bool**|  | [optional] [default to false]
 | 
			
		||||
 | 
			
		||||
### Return type
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								mobile/openapi/lib/api/user_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/lib/api/user_api.dart
									
									
									
										generated
									
									
									
								
							@@ -358,7 +358,10 @@ class UserApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Performs an HTTP 'GET /user/count' operation and returns the [Response].
 | 
			
		||||
  Future<Response> getUserCountWithHttpInfo() async {
 | 
			
		||||
  /// Parameters:
 | 
			
		||||
  ///
 | 
			
		||||
  /// * [bool] admin:
 | 
			
		||||
  Future<Response> getUserCountWithHttpInfo({ bool? admin, }) async {
 | 
			
		||||
    // ignore: prefer_const_declarations
 | 
			
		||||
    final path = r'/user/count';
 | 
			
		||||
 | 
			
		||||
@@ -369,6 +372,10 @@ class UserApi {
 | 
			
		||||
    final headerParams = <String, String>{};
 | 
			
		||||
    final formParams = <String, String>{};
 | 
			
		||||
 | 
			
		||||
    if (admin != null) {
 | 
			
		||||
      queryParams.addAll(_queryParams('', 'admin', admin));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const contentTypes = <String>[];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -383,8 +390,11 @@ class UserApi {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<UserCountResponseDto?> getUserCount() async {
 | 
			
		||||
    final response = await getUserCountWithHttpInfo();
 | 
			
		||||
  /// Parameters:
 | 
			
		||||
  ///
 | 
			
		||||
  /// * [bool] admin:
 | 
			
		||||
  Future<UserCountResponseDto?> getUserCount({ bool? admin, }) async {
 | 
			
		||||
    final response = await getUserCountWithHttpInfo( admin: admin, );
 | 
			
		||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
			
		||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								mobile/openapi/test/user_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/user_api_test.dart
									
									
									
										generated
									
									
									
								
							@@ -52,7 +52,7 @@ void main() {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //Future<UserCountResponseDto> getUserCount() async
 | 
			
		||||
    //Future<UserCountResponseDto> getUserCount({ bool admin }) async
 | 
			
		||||
    test('test getUserCount', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								server/apps/immich/src/api-v1/user/dto/user-count.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/apps/immich/src/api-v1/user/dto/user-count.dto.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
import { IsBoolean, IsOptional } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class UserCountDto {
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @Transform(({ value }) => value === 'true')
 | 
			
		||||
  /**
 | 
			
		||||
   * When true, return the number of admins accounts
 | 
			
		||||
   */
 | 
			
		||||
  admin?: boolean = false;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								server/apps/immich/src/api-v1/user/user-repository.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								server/apps/immich/src/api-v1/user/user-repository.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import { UserEntity } from '@app/database/entities/user.entity';
 | 
			
		||||
import { BadRequestException } from '@nestjs/common';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
import { UserRepository } from './user-repository';
 | 
			
		||||
 | 
			
		||||
describe('UserRepository', () => {
 | 
			
		||||
  let sui: UserRepository;
 | 
			
		||||
  let userRepositoryMock: jest.Mocked<Repository<UserEntity>>;
 | 
			
		||||
 | 
			
		||||
  beforeAll(() => {
 | 
			
		||||
    userRepositoryMock = {
 | 
			
		||||
      findOne: jest.fn(),
 | 
			
		||||
      save: jest.fn(),
 | 
			
		||||
    } as unknown as jest.Mocked<Repository<UserEntity>>;
 | 
			
		||||
 | 
			
		||||
    sui = new UserRepository(userRepositoryMock);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(sui).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('create', () => {
 | 
			
		||||
    it('should not create a user if there is no local admin account', async () => {
 | 
			
		||||
      userRepositoryMock.findOne.mockResolvedValue(null);
 | 
			
		||||
      await expect(sui.create({ isAdmin: false })).rejects.toBeInstanceOf(BadRequestException);
 | 
			
		||||
      expect(userRepositoryMock.findOne).toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -60,6 +60,11 @@ export class UserRepository implements IUserRepository {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async create(user: Partial<UserEntity>): Promise<UserEntity> {
 | 
			
		||||
    const localAdmin = await this.getAdmin();
 | 
			
		||||
    if (!localAdmin && !user.isAdmin) {
 | 
			
		||||
      throw new BadRequestException('The first registered account must the administrator.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (user.password) {
 | 
			
		||||
      user.salt = await bcrypt.genSalt();
 | 
			
		||||
      user.password = await this.hashPassword(user.password, user.salt);
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import { UserResponseDto } from './response-dto/user-response.dto';
 | 
			
		||||
import { UserCountResponseDto } from './response-dto/user-count-response.dto';
 | 
			
		||||
import { CreateProfileImageDto } from './dto/create-profile-image.dto';
 | 
			
		||||
import { CreateProfileImageResponseDto } from './response-dto/create-profile-image-response.dto';
 | 
			
		||||
import { UserCountDto } from './dto/user-count.dto';
 | 
			
		||||
 | 
			
		||||
@ApiTags('User')
 | 
			
		||||
@Controller('user')
 | 
			
		||||
@@ -64,8 +65,8 @@ export class UserController {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Get('/count')
 | 
			
		||||
  async getUserCount(): Promise<UserCountResponseDto> {
 | 
			
		||||
    return await this.userService.getUserCount();
 | 
			
		||||
  async getUserCount(@Query(new ValidationPipe({ transform: true })) dto: UserCountDto): Promise<UserCountResponseDto> {
 | 
			
		||||
    return await this.userService.getUserCount(dto);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Authenticated({ admin: true })
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import { createReadStream } from 'fs';
 | 
			
		||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
			
		||||
import { CreateUserDto } from './dto/create-user.dto';
 | 
			
		||||
import { UpdateUserDto } from './dto/update-user.dto';
 | 
			
		||||
import { UserCountDto } from './dto/user-count.dto';
 | 
			
		||||
import {
 | 
			
		||||
  CreateProfileImageResponseDto,
 | 
			
		||||
  mapCreateProfileImageResponse,
 | 
			
		||||
@@ -57,8 +58,12 @@ export class UserService {
 | 
			
		||||
    return mapUser(user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getUserCount(): Promise<UserCountResponseDto> {
 | 
			
		||||
    const users = await this.userRepository.getList();
 | 
			
		||||
  async getUserCount(dto: UserCountDto): Promise<UserCountResponseDto> {
 | 
			
		||||
    let users = await this.userRepository.getList();
 | 
			
		||||
 | 
			
		||||
    if (dto.admin) {
 | 
			
		||||
      users = users.filter((user) => user.isAdmin);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return mapUserCountResponse(users.length);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -166,7 +166,17 @@
 | 
			
		||||
    "/user/count": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "operationId": "getUserCount",
 | 
			
		||||
        "parameters": [],
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "name": "admin",
 | 
			
		||||
            "required": false,
 | 
			
		||||
            "in": "query",
 | 
			
		||||
            "schema": {
 | 
			
		||||
              "default": false,
 | 
			
		||||
              "type": "boolean"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "description": "",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@@ -6108,10 +6108,11 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {boolean} [admin] 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getUserCount: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
        getUserCount: async (admin?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            const localVarPath = `/user/count`;
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
@@ -6124,6 +6125,10 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            if (admin !== undefined) {
 | 
			
		||||
                localVarQueryParameter['admin'] = admin;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
@@ -6292,11 +6297,12 @@ export const UserApiFp = function(configuration?: Configuration) {
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {boolean} [admin] 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getUserCount(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserCountResponseDto>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options);
 | 
			
		||||
        async getUserCount(admin?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserCountResponseDto>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(admin, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
@@ -6393,11 +6399,12 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {boolean} [admin] 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getUserCount(options?: any): AxiosPromise<UserCountResponseDto> {
 | 
			
		||||
            return localVarFp.getUserCount(options).then((request) => request(axios, basePath));
 | 
			
		||||
        getUserCount(admin?: boolean, options?: any): AxiosPromise<UserCountResponseDto> {
 | 
			
		||||
            return localVarFp.getUserCount(admin, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
@@ -6505,12 +6512,13 @@ export class UserApi extends BaseAPI {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {boolean} [admin] 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof UserApi
 | 
			
		||||
     */
 | 
			
		||||
    public getUserCount(options?: AxiosRequestConfig) {
 | 
			
		||||
        return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    public getUserCount(admin?: boolean, options?: AxiosRequestConfig) {
 | 
			
		||||
        return UserApiFp(this.configuration).getUserCount(admin, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,5 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
	export let data: PageData;
 | 
			
		||||
 | 
			
		||||
	async function onGettingStartedClicked() {
 | 
			
		||||
		data.isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:head>
 | 
			
		||||
@@ -26,7 +19,7 @@
 | 
			
		||||
		</h1>
 | 
			
		||||
		<button
 | 
			
		||||
			class="border px-4 py-4 rounded-md bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:border-immich-dark-gray hover:bg-immich-primary/75 text-white font-bold w-[200px]"
 | 
			
		||||
			on:click={onGettingStartedClicked}
 | 
			
		||||
			on:click={() => goto('/auth/login')}
 | 
			
		||||
			>Getting Started
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,10 @@
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import { api } from '@api';
 | 
			
		||||
import type { PageLoad } from './$types';
 | 
			
		||||
import { browser } from '$app/environment';
 | 
			
		||||
 | 
			
		||||
export const load: PageLoad = async ({ parent }) => {
 | 
			
		||||
	const { user } = await parent();
 | 
			
		||||
	if (user) {
 | 
			
		||||
		throw redirect(302, '/photos');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (browser) {
 | 
			
		||||
		const { data } = await api.userApi.getUserCount();
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			isAdminUserExist: data.userCount != 0
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								web/src/routes/auth/login/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/routes/auth/login/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageServerLoad } from './$types';
 | 
			
		||||
import { serverApi } from '@api';
 | 
			
		||||
 | 
			
		||||
export const load: PageServerLoad = async () => {
 | 
			
		||||
	const { data } = await serverApi.userApi.getUserCount(true);
 | 
			
		||||
	if (data.userCount === 0) {
 | 
			
		||||
		// Admin not registered
 | 
			
		||||
		throw redirect(302, '/auth/register');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return;
 | 
			
		||||
};
 | 
			
		||||
@@ -3,7 +3,7 @@ import type { PageServerLoad } from './$types';
 | 
			
		||||
import { serverApi } from '@api';
 | 
			
		||||
 | 
			
		||||
export const load: PageServerLoad = async () => {
 | 
			
		||||
	const { data } = await serverApi.userApi.getUserCount();
 | 
			
		||||
	const { data } = await serverApi.userApi.getUserCount(true);
 | 
			
		||||
	if (data.userCount != 0) {
 | 
			
		||||
		// Admin has been registered, redirect to login
 | 
			
		||||
		throw redirect(302, '/auth/login');
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user