diff --git a/mobile/openapi/doc/UserApi.md b/mobile/openapi/doc/UserApi.md index e52857a2844e5..752fb7945e0d4 100644 --- a/mobile/openapi/doc/UserApi.md +++ b/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 diff --git a/mobile/openapi/lib/api/user_api.dart b/mobile/openapi/lib/api/user_api.dart index 6af304115ae9b..f187652ac2437 100644 --- a/mobile/openapi/lib/api/user_api.dart +++ b/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 getUserCountWithHttpInfo() async { + /// Parameters: + /// + /// * [bool] admin: + Future getUserCountWithHttpInfo({ bool? admin, }) async { // ignore: prefer_const_declarations final path = r'/user/count'; @@ -369,6 +372,10 @@ class UserApi { final headerParams = {}; final formParams = {}; + if (admin != null) { + queryParams.addAll(_queryParams('', 'admin', admin)); + } + const contentTypes = []; @@ -383,8 +390,11 @@ class UserApi { ); } - Future getUserCount() async { - final response = await getUserCountWithHttpInfo(); + /// Parameters: + /// + /// * [bool] admin: + Future getUserCount({ bool? admin, }) async { + final response = await getUserCountWithHttpInfo( admin: admin, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/test/user_api_test.dart b/mobile/openapi/test/user_api_test.dart index 6e94afed4a6f0..5163a150041fa 100644 --- a/mobile/openapi/test/user_api_test.dart +++ b/mobile/openapi/test/user_api_test.dart @@ -52,7 +52,7 @@ void main() { // TODO }); - //Future getUserCount() async + //Future getUserCount({ bool admin }) async test('test getUserCount', () async { // TODO }); diff --git a/server/apps/immich/src/api-v1/user/dto/user-count.dto.ts b/server/apps/immich/src/api-v1/user/dto/user-count.dto.ts new file mode 100644 index 0000000000000..7853ede3c9574 --- /dev/null +++ b/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; +} diff --git a/server/apps/immich/src/api-v1/user/user-repository.spec.ts b/server/apps/immich/src/api-v1/user/user-repository.spec.ts new file mode 100644 index 0000000000000..e5f78fcf8a07b --- /dev/null +++ b/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>; + + beforeAll(() => { + userRepositoryMock = { + findOne: jest.fn(), + save: jest.fn(), + } as unknown as jest.Mocked>; + + 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(); + }); + }); +}); diff --git a/server/apps/immich/src/api-v1/user/user-repository.ts b/server/apps/immich/src/api-v1/user/user-repository.ts index 574feed292e54..e4dd2e64653bb 100644 --- a/server/apps/immich/src/api-v1/user/user-repository.ts +++ b/server/apps/immich/src/api-v1/user/user-repository.ts @@ -60,6 +60,11 @@ export class UserRepository implements IUserRepository { } public async create(user: Partial): Promise { + 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); diff --git a/server/apps/immich/src/api-v1/user/user.controller.ts b/server/apps/immich/src/api-v1/user/user.controller.ts index 0512a4af1740d..79f62991e8ac3 100644 --- a/server/apps/immich/src/api-v1/user/user.controller.ts +++ b/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 { - return await this.userService.getUserCount(); + async getUserCount(@Query(new ValidationPipe({ transform: true })) dto: UserCountDto): Promise { + return await this.userService.getUserCount(dto); } @Authenticated({ admin: true }) diff --git a/server/apps/immich/src/api-v1/user/user.service.ts b/server/apps/immich/src/api-v1/user/user.service.ts index 6fe2aafefdc8d..6f9da0e9f5da4 100644 --- a/server/apps/immich/src/api-v1/user/user.service.ts +++ b/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 { - const users = await this.userRepository.getList(); + async getUserCount(dto: UserCountDto): Promise { + let users = await this.userRepository.getList(); + + if (dto.admin) { + users = users.filter((user) => user.isAdmin); + } return mapUserCountResponse(users.length); } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 6c9b3e5ac7005..05473e8243b9e 100644 --- a/server/immich-openapi-specs.json +++ b/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": "", diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index fd0a12599f485..23559dda5af1c 100644 --- a/web/src/api/open-api/api.ts +++ b/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 => { + getUserCount: async (admin?: boolean, options: AxiosRequestConfig = {}): Promise => { 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> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options); + async getUserCount(admin?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + 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 { - return localVarFp.getUserCount(options).then((request) => request(axios, basePath)); + getUserCount(admin?: boolean, options?: any): AxiosPromise { + 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)); } /** diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index fe31231229e65..f198716407a29 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,12 +1,5 @@ @@ -26,7 +19,7 @@ diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index 4e1bcb449a83a..fdd41a2e07468 100644 --- a/web/src/routes/+page.ts +++ b/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 - }; - } }; diff --git a/web/src/routes/auth/login/+page.server.ts b/web/src/routes/auth/login/+page.server.ts new file mode 100644 index 0000000000000..16d6aa4f145d1 --- /dev/null +++ b/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; +}; diff --git a/web/src/routes/auth/register/+page.server.ts b/web/src/routes/auth/register/+page.server.ts index 8f78289b7d872..6f5ed461ce8fd 100644 --- a/web/src/routes/auth/register/+page.server.ts +++ b/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');