Selaa lähdekoodia

fix(server): require local admin account (#1070)

Jason Rasmussen 2 vuotta sitten
vanhempi
commit
14889e7d85

+ 7 - 3
mobile/openapi/doc/UserApi.md

@@ -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
 

+ 13 - 3
mobile/openapi/lib/api/user_api.dart

@@ -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));
     }

+ 1 - 1
mobile/openapi/test/user_api_test.dart

@@ -52,7 +52,7 @@ void main() {
       // TODO
     });
 
-    //Future<UserCountResponseDto> getUserCount() async
+    //Future<UserCountResponseDto> getUserCount({ bool admin }) async
     test('test getUserCount', () async {
       // TODO
     });

+ 12 - 0
server/apps/immich/src/api-v1/user/dto/user-count.dto.ts

@@ -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 - 0
server/apps/immich/src/api-v1/user/user-repository.spec.ts

@@ -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();
+    });
+  });
+});

+ 5 - 0
server/apps/immich/src/api-v1/user/user-repository.ts

@@ -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);

+ 3 - 2
server/apps/immich/src/api-v1/user/user.controller.ts

@@ -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 })

+ 7 - 2
server/apps/immich/src/api-v1/user/user.service.ts

@@ -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);
   }

+ 11 - 1
server/immich-openapi-specs.json

@@ -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": "",

+ 15 - 7
web/src/api/open-api/api.ts

@@ -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 - 8
web/src/routes/+page.svelte

@@ -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>

+ 0 - 10
web/src/routes/+page.ts

@@ -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 - 0
web/src/routes/auth/login/+page.server.ts

@@ -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;
+};

+ 1 - 1
web/src/routes/auth/register/+page.server.ts

@@ -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');