From 0fc6d6982466b73ecc284ee3ec2c99120bee8acc Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 27 May 2024 22:16:53 -0400 Subject: [PATCH] feat(server): user preferences (#9736) * refactor(server): user endpoints * feat(server): user preferences * mobile: user preference * wording --------- Co-authored-by: Alex --- docs/docs/administration/server-commands.md | 1 - e2e/src/api/specs/user-admin.e2e-spec.ts | 91 +++---- e2e/src/api/specs/user.e2e-spec.ts | 42 ++- e2e/src/fixtures.ts | 6 - e2e/src/responses.ts | 1 - mobile/lib/entities/user.entity.dart | 8 +- .../providers/authentication.provider.dart | 6 +- mobile/lib/providers/user.provider.dart | 3 +- .../lib/routing/tab_navigation_observer.dart | 4 +- mobile/openapi/README.md | 10 + mobile/openapi/lib/api.dart | 6 + mobile/openapi/lib/api/user_api.dart | 188 +++++++++++++ mobile/openapi/lib/api_client.dart | 12 + mobile/openapi/lib/model/avatar_response.dart | 98 +++++++ mobile/openapi/lib/model/avatar_update.dart | 107 ++++++++ mobile/openapi/lib/model/memory_response.dart | 98 +++++++ mobile/openapi/lib/model/memory_update.dart | 107 ++++++++ .../lib/model/user_admin_create_dto.dart | 19 +- .../lib/model/user_admin_response_dto.dart | 19 +- .../lib/model/user_admin_update_dto.dart | 36 +-- .../model/user_preferences_response_dto.dart | 106 ++++++++ .../model/user_preferences_update_dto.dart | 124 +++++++++ .../openapi/lib/model/user_update_me_dto.dart | 36 +-- open-api/immich-openapi-specs.json | 246 ++++++++++++++++-- open-api/typescript-sdk/README.md | 11 +- open-api/typescript-sdk/src/fetch-client.ts | 69 ++++- .../src/controllers/user-admin.controller.ts | 17 ++ server/src/controllers/user.controller.ts | 16 ++ server/src/dtos/user-preferences.dto.ts | 47 ++++ server/src/dtos/user.dto.ts | 24 +- server/src/services/user-admin.service.ts | 54 ++-- server/src/services/user.service.ts | 38 ++- server/src/utils/preferences.ts | 10 + .../navigation-bar/account-info-panel.svelte | 19 +- .../memories-settings.svelte | 12 +- .../user-settings-list.svelte | 2 +- web/src/lib/stores/user.store.ts | 4 +- web/src/lib/utils/auth.ts | 18 +- .../(user)/photos/[[assetId=id]]/+page.svelte | 4 +- 39 files changed, 1392 insertions(+), 327 deletions(-) create mode 100644 mobile/openapi/lib/model/avatar_response.dart create mode 100644 mobile/openapi/lib/model/avatar_update.dart create mode 100644 mobile/openapi/lib/model/memory_response.dart create mode 100644 mobile/openapi/lib/model/memory_update.dart create mode 100644 mobile/openapi/lib/model/user_preferences_response_dto.dart create mode 100644 mobile/openapi/lib/model/user_preferences_update_dto.dart create mode 100644 server/src/dtos/user-preferences.dto.ts diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md index 2594da44b2..355ee10e39 100644 --- a/docs/docs/administration/server-commands.md +++ b/docs/docs/administration/server-commands.md @@ -77,7 +77,6 @@ immich-admin list-users deletedAt: null, updatedAt: 2023-09-21T15:42:28.129Z, oauthId: '', - memoriesEnabled: true } ] ``` diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/api/specs/user-admin.e2e-spec.ts index ac2b3e693a..a041d98419 100644 --- a/e2e/src/api/specs/user-admin.e2e-spec.ts +++ b/e2e/src/api/specs/user-admin.e2e-spec.ts @@ -1,4 +1,11 @@ -import { LoginResponseDto, deleteUserAdmin, getMyUser, getUserAdmin, login } from '@immich/sdk'; +import { + LoginResponseDto, + deleteUserAdmin, + getMyUser, + getUserAdmin, + getUserPreferencesAdmin, + login, +} from '@immich/sdk'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; @@ -103,15 +110,7 @@ describe('/admin/users', () => { expect(body).toEqual(errorDto.forbidden); }); - for (const key of [ - 'password', - 'email', - 'name', - 'quotaSizeInBytes', - 'shouldChangePassword', - 'memoriesEnabled', - 'notify', - ]) { + for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) { it(`should not allow null ${key}`, async () => { const { status, body } = await request(app) .post(`/admin/users`) @@ -139,23 +138,6 @@ describe('/admin/users', () => { }); expect(status).toBe(201); }); - - it('should create a user without memories enabled', async () => { - const { status, body } = await request(app) - .post(`/admin/users`) - .send({ - email: 'no-memories@immich.cloud', - password: 'Password123', - name: 'No Memories', - memoriesEnabled: false, - }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toMatchObject({ - email: 'no-memories@immich.cloud', - memoriesEnabled: false, - }); - expect(status).toBe(201); - }); }); describe('PUT /admin/users/:id', () => { @@ -173,7 +155,7 @@ describe('/admin/users', () => { expect(body).toEqual(errorDto.forbidden); }); - for (const key of ['password', 'email', 'name', 'shouldChangePassword', 'memoriesEnabled']) { + for (const key of ['password', 'email', 'name', 'shouldChangePassword']) { it(`should not allow null ${key}`, async () => { const { status, body } = await request(app) .put(`/admin/users/${uuidDto.notFound}`) @@ -221,22 +203,6 @@ describe('/admin/users', () => { expect(before.updatedAt).not.toEqual(body.updatedAt); }); - it('should update memories enabled', async () => { - const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); - const { status, body } = await request(app) - .put(`/admin/users/${admin.userId}`) - .send({ memoriesEnabled: false }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ - ...before, - updatedAt: expect.anything(), - memoriesEnabled: false, - }); - expect(before.updatedAt).not.toEqual(body.updatedAt); - }); - it('should update password', async () => { const { status, body } = await request(app) .put(`/admin/users/${nonAdmin.userId}`) @@ -254,6 +220,43 @@ describe('/admin/users', () => { }); }); + describe('PUT /admin/users/:id/preferences', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should update memories enabled', async () => { + const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); + expect(before).toMatchObject({ memories: { enabled: true } }); + + const { status, body } = await request(app) + .put(`/admin/users/${admin.userId}/preferences`) + .send({ memories: { enabled: false } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ memories: { enabled: false } }); + + const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ memories: { enabled: false } }); + }); + + it('should update the avatar color', async () => { + const { status, body } = await request(app) + .put(`/admin/users/${admin.userId}/preferences`) + .send({ avatar: { color: 'orange' } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ avatar: { color: 'orange' }, memories: { enabled: false } }); + + const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); + expect(after).toEqual({ avatar: { color: 'orange' }, memories: { enabled: false } }); + }); + }); + describe('DELETE /admin/users/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`); diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 0cc08479d3..ccf7d6dd3a 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyUser, login } from '@immich/sdk'; +import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; @@ -69,7 +69,6 @@ describe('/users', () => { expect(body).toMatchObject({ id: admin.userId, email: 'admin@immich.cloud', - memoriesEnabled: true, quotaUsageInBytes: 0, }); }); @@ -82,7 +81,7 @@ describe('/users', () => { expect(body).toEqual(errorDto.unauthorized); }); - for (const key of ['email', 'name', 'memoriesEnabled', 'avatarColor']) { + for (const key of ['email', 'name']) { it(`should not allow null ${key}`, async () => { const dto = { [key]: null }; const { status, body } = await request(app) @@ -110,24 +109,6 @@ describe('/users', () => { }); }); - it('should update memories enabled', async () => { - const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); - const { status, body } = await request(app) - .put(`/users/me`) - .send({ memoriesEnabled: false }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ - ...before, - updatedAt: expect.anything(), - memoriesEnabled: false, - }); - - const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); - expect(after.memoriesEnabled).toBe(false); - }); - /** @deprecated */ it('should allow a user to change their password (deprecated)', async () => { const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) }); @@ -176,6 +157,24 @@ describe('/users', () => { }); }); + describe('PUT /users/me/preferences', () => { + it('should update memories enabled', async () => { + const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(before).toMatchObject({ memories: { enabled: true } }); + + const { status, body } = await request(app) + .put(`/users/me/preferences`) + .send({ memories: { enabled: false } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ memories: { enabled: false } }); + + const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ memories: { enabled: false } }); + }); + }); + describe('GET /users/:id', () => { it('should require authentication', async () => { const { status } = await request(app).get(`/users/${admin.userId}`); @@ -194,7 +193,6 @@ describe('/users', () => { expect(body).not.toMatchObject({ shouldChangePassword: expect.anything(), - memoriesEnabled: expect.anything(), storageLabel: expect.anything(), }); }); diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts index 031985c5fb..9e311c896d 100644 --- a/e2e/src/fixtures.ts +++ b/e2e/src/fixtures.ts @@ -1,5 +1,3 @@ -import { UserAvatarColor } from '@immich/sdk'; - export const uuidDto = { invalid: 'invalid-uuid', // valid uuid v4 @@ -70,8 +68,6 @@ export const userDto = { updatedAt: new Date('2021-01-01'), tags: [], assets: [], - memoriesEnabled: true, - avatarColor: UserAvatarColor.Primary, quotaSizeInBytes: null, quotaUsageInBytes: 0, }, @@ -88,8 +84,6 @@ export const userDto = { updatedAt: new Date('2021-01-01'), tags: [], assets: [], - memoriesEnabled: true, - avatarColor: UserAvatarColor.Primary, quotaSizeInBytes: null, quotaUsageInBytes: 0, }, diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index afe3334a7f..b7dcfca1ee 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -68,7 +68,6 @@ export const signupResponseDto = { updatedAt: expect.any(String), deletedAt: null, oauthId: '', - memoriesEnabled: true, quotaUsageInBytes: 0, quotaSizeInBytes: null, status: 'active', diff --git a/mobile/lib/entities/user.entity.dart b/mobile/lib/entities/user.entity.dart index b6adcf5d87..55a19fe496 100644 --- a/mobile/lib/entities/user.entity.dart +++ b/mobile/lib/entities/user.entity.dart @@ -27,8 +27,10 @@ class User { Id get isarId => fastHash(id); - User.fromUserDto(UserAdminResponseDto dto) - : id = dto.id, + User.fromUserDto( + UserAdminResponseDto dto, + UserPreferencesResponseDto? preferences, + ) : id = dto.id, updatedAt = dto.updatedAt, email = dto.email, name = dto.name, @@ -36,7 +38,7 @@ class User { isPartnerSharedWith = false, profileImagePath = dto.profileImagePath, isAdmin = dto.isAdmin, - memoryEnabled = dto.memoriesEnabled ?? false, + memoryEnabled = preferences?.memories.enabled ?? false, avatarColor = dto.avatarColor.toAvatarColor(), inTimeline = false, quotaUsageInBytes = dto.quotaUsageInBytes ?? 0, diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart index 073ee09db1..b5fb25bf20 100644 --- a/mobile/lib/providers/authentication.provider.dart +++ b/mobile/lib/providers/authentication.provider.dart @@ -177,8 +177,10 @@ class AuthenticationNotifier extends StateNotifier { retResult = false; } else { UserAdminResponseDto? userResponseDto; + UserPreferencesResponseDto? userPreferences; try { userResponseDto = await _apiService.userApi.getMyUser(); + userPreferences = await _apiService.userApi.getMyPreferences(); } on ApiException catch (error, stackTrace) { _log.severe( "Error getting user information from the server [API EXCEPTION]", @@ -201,13 +203,13 @@ class AuthenticationNotifier extends StateNotifier { Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); Store.put( StoreKey.currentUser, - User.fromUserDto(userResponseDto), + User.fromUserDto(userResponseDto, userPreferences), ); Store.put(StoreKey.serverUrl, serverUrl); Store.put(StoreKey.accessToken, accessToken); shouldChangePassword = userResponseDto.shouldChangePassword; - user = User.fromUserDto(userResponseDto); + user = User.fromUserDto(userResponseDto, userPreferences); retResult = true; } else { diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index bf052ebbba..2767615526 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -21,10 +21,11 @@ class CurrentUserProvider extends StateNotifier { refresh() async { try { final user = await _apiService.userApi.getMyUser(); + final userPreferences = await _apiService.userApi.getMyPreferences(); if (user != null) { Store.put( StoreKey.currentUser, - User.fromUserDto(user), + User.fromUserDto(user, userPreferences), ); } } catch (_) {} diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index 8825e2ef02..6c0f36050b 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -58,6 +58,8 @@ class TabNavigationObserver extends AutoRouterObserver { try { final userResponseDto = await ref.read(apiServiceProvider).userApi.getMyUser(); + final userPreferences = + await ref.read(apiServiceProvider).userApi.getMyPreferences(); if (userResponseDto == null) { return; @@ -65,7 +67,7 @@ class TabNavigationObserver extends AutoRouterObserver { Store.put( StoreKey.currentUser, - User.fromUserDto(userResponseDto), + User.fromUserDto(userResponseDto, userPreferences), ); ref.read(serverInfoProvider.notifier).getServerVersion(); } catch (e) { diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 273585c368..cdc75d4f28 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -215,15 +215,19 @@ Class | Method | HTTP request | Description *UserApi* | [**createUserAdmin**](doc//UserApi.md#createuseradmin) | **POST** /admin/users | *UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /users/profile-image | *UserApi* | [**deleteUserAdmin**](doc//UserApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} | +*UserApi* | [**getMyPreferences**](doc//UserApi.md#getmypreferences) | **GET** /users/me/preferences | *UserApi* | [**getMyUser**](doc//UserApi.md#getmyuser) | **GET** /users/me | *UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /users/{id}/profile-image | *UserApi* | [**getUser**](doc//UserApi.md#getuser) | **GET** /users/{id} | *UserApi* | [**getUserAdmin**](doc//UserApi.md#getuseradmin) | **GET** /admin/users/{id} | +*UserApi* | [**getUserPreferencesAdmin**](doc//UserApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences | *UserApi* | [**restoreUserAdmin**](doc//UserApi.md#restoreuseradmin) | **POST** /admin/users/{id}/restore | *UserApi* | [**searchUsers**](doc//UserApi.md#searchusers) | **GET** /users | *UserApi* | [**searchUsersAdmin**](doc//UserApi.md#searchusersadmin) | **GET** /admin/users | +*UserApi* | [**updateMyPreferences**](doc//UserApi.md#updatemypreferences) | **PUT** /users/me/preferences | *UserApi* | [**updateMyUser**](doc//UserApi.md#updatemyuser) | **PUT** /users/me | *UserApi* | [**updateUserAdmin**](doc//UserApi.md#updateuseradmin) | **PUT** /admin/users/{id} | +*UserApi* | [**updateUserPreferencesAdmin**](doc//UserApi.md#updateuserpreferencesadmin) | **PUT** /admin/users/{id}/preferences | ## Documentation For Models @@ -270,6 +274,8 @@ Class | Method | HTTP request | Description - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) - [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md) + - [AvatarResponse](doc//AvatarResponse.md) + - [AvatarUpdate](doc//AvatarUpdate.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdsDto](doc//BulkIdsDto.md) - [CLIPConfig](doc//CLIPConfig.md) @@ -313,8 +319,10 @@ Class | Method | HTTP request | Description - [MapTheme](doc//MapTheme.md) - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) + - [MemoryResponse](doc//MemoryResponse.md) - [MemoryResponseDto](doc//MemoryResponseDto.md) - [MemoryType](doc//MemoryType.md) + - [MemoryUpdate](doc//MemoryUpdate.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) @@ -409,6 +417,8 @@ Class | Method | HTTP request | Description - [UserAdminResponseDto](doc//UserAdminResponseDto.md) - [UserAdminUpdateDto](doc//UserAdminUpdateDto.md) - [UserAvatarColor](doc//UserAvatarColor.md) + - [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md) + - [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md) - [UserResponseDto](doc//UserResponseDto.md) - [UserStatus](doc//UserStatus.md) - [UserUpdateMeDto](doc//UserUpdateMeDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d7223a1ecf..94303a768f 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -99,6 +99,8 @@ part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; part 'model/audit_deletes_response_dto.dart'; +part 'model/avatar_response.dart'; +part 'model/avatar_update.dart'; part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; part 'model/clip_config.dart'; @@ -142,8 +144,10 @@ part 'model/map_marker_response_dto.dart'; part 'model/map_theme.dart'; part 'model/memory_create_dto.dart'; part 'model/memory_lane_response_dto.dart'; +part 'model/memory_response.dart'; part 'model/memory_response_dto.dart'; part 'model/memory_type.dart'; +part 'model/memory_update.dart'; part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; @@ -238,6 +242,8 @@ part 'model/user_admin_delete_dto.dart'; part 'model/user_admin_response_dto.dart'; part 'model/user_admin_update_dto.dart'; part 'model/user_avatar_color.dart'; +part 'model/user_preferences_response_dto.dart'; +part 'model/user_preferences_update_dto.dart'; part 'model/user_response_dto.dart'; part 'model/user_status.dart'; part 'model/user_update_me_dto.dart'; diff --git a/mobile/openapi/lib/api/user_api.dart b/mobile/openapi/lib/api/user_api.dart index 3c1a3ff4e7..246ea422c5 100644 --- a/mobile/openapi/lib/api/user_api.dart +++ b/mobile/openapi/lib/api/user_api.dart @@ -205,6 +205,47 @@ class UserApi { return null; } + /// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response]. + Future getMyPreferencesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/users/me/preferences'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getMyPreferences() async { + final response = await getMyPreferencesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserPreferencesResponseDto',) as UserPreferencesResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /users/me' operation and returns the [Response]. Future getMyUserWithHttpInfo() async { // ignore: prefer_const_declarations @@ -390,6 +431,54 @@ class UserApi { return null; } + /// Performs an HTTP 'GET /admin/users/{id}/preferences' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getUserPreferencesAdminWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/admin/users/{id}/preferences' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getUserPreferencesAdmin(String id,) async { + final response = await getUserPreferencesAdminWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserPreferencesResponseDto',) as UserPreferencesResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /admin/users/{id}/restore' operation and returns the [Response]. /// Parameters: /// @@ -536,6 +625,53 @@ class UserApi { return null; } + /// Performs an HTTP 'PUT /users/me/preferences' operation and returns the [Response]. + /// Parameters: + /// + /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): + Future updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/users/me/preferences'; + + // ignore: prefer_final_locals + Object? postBody = userPreferencesUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): + Future updateMyPreferences(UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + final response = await updateMyPreferencesWithHttpInfo(userPreferencesUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserPreferencesResponseDto',) as UserPreferencesResponseDto; + + } + return null; + } + /// Performs an HTTP 'PUT /users/me' operation and returns the [Response]. /// Parameters: /// @@ -634,4 +770,56 @@ class UserApi { } return null; } + + /// Performs an HTTP 'PUT /admin/users/{id}/preferences' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): + Future updateUserPreferencesAdminWithHttpInfo(String id, UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/admin/users/{id}/preferences' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = userPreferencesUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): + Future updateUserPreferencesAdmin(String id, UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + final response = await updateUserPreferencesAdminWithHttpInfo(id, userPreferencesUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserPreferencesResponseDto',) as UserPreferencesResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index bd3433872a..bf306ac10a 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -266,6 +266,10 @@ class ApiClient { return AudioCodecTypeTransformer().decode(value); case 'AuditDeletesResponseDto': return AuditDeletesResponseDto.fromJson(value); + case 'AvatarResponse': + return AvatarResponse.fromJson(value); + case 'AvatarUpdate': + return AvatarUpdate.fromJson(value); case 'BulkIdResponseDto': return BulkIdResponseDto.fromJson(value); case 'BulkIdsDto': @@ -352,10 +356,14 @@ class ApiClient { return MemoryCreateDto.fromJson(value); case 'MemoryLaneResponseDto': return MemoryLaneResponseDto.fromJson(value); + case 'MemoryResponse': + return MemoryResponse.fromJson(value); case 'MemoryResponseDto': return MemoryResponseDto.fromJson(value); case 'MemoryType': return MemoryTypeTypeTransformer().decode(value); + case 'MemoryUpdate': + return MemoryUpdate.fromJson(value); case 'MemoryUpdateDto': return MemoryUpdateDto.fromJson(value); case 'MergePersonDto': @@ -544,6 +552,10 @@ class ApiClient { return UserAdminUpdateDto.fromJson(value); case 'UserAvatarColor': return UserAvatarColorTypeTransformer().decode(value); + case 'UserPreferencesResponseDto': + return UserPreferencesResponseDto.fromJson(value); + case 'UserPreferencesUpdateDto': + return UserPreferencesUpdateDto.fromJson(value); case 'UserResponseDto': return UserResponseDto.fromJson(value); case 'UserStatus': diff --git a/mobile/openapi/lib/model/avatar_response.dart b/mobile/openapi/lib/model/avatar_response.dart new file mode 100644 index 0000000000..edd242df4e --- /dev/null +++ b/mobile/openapi/lib/model/avatar_response.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AvatarResponse { + /// Returns a new [AvatarResponse] instance. + AvatarResponse({ + required this.color, + }); + + UserAvatarColor color; + + @override + bool operator ==(Object other) => identical(this, other) || other is AvatarResponse && + other.color == color; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (color.hashCode); + + @override + String toString() => 'AvatarResponse[color=$color]'; + + Map toJson() { + final json = {}; + json[r'color'] = this.color; + return json; + } + + /// Returns a new [AvatarResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AvatarResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AvatarResponse( + color: UserAvatarColor.fromJson(json[r'color'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AvatarResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AvatarResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AvatarResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AvatarResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'color', + }; +} + diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart new file mode 100644 index 0000000000..b92eb8dcbd --- /dev/null +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AvatarUpdate { + /// Returns a new [AvatarUpdate] instance. + AvatarUpdate({ + this.color, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + UserAvatarColor? color; + + @override + bool operator ==(Object other) => identical(this, other) || other is AvatarUpdate && + other.color == color; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (color == null ? 0 : color!.hashCode); + + @override + String toString() => 'AvatarUpdate[color=$color]'; + + Map toJson() { + final json = {}; + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } + return json; + } + + /// Returns a new [AvatarUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AvatarUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AvatarUpdate( + color: UserAvatarColor.fromJson(json[r'color']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AvatarUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AvatarUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AvatarUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AvatarUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/memory_response.dart b/mobile/openapi/lib/model/memory_response.dart new file mode 100644 index 0000000000..fb34bc1518 --- /dev/null +++ b/mobile/openapi/lib/model/memory_response.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryResponse { + /// Returns a new [MemoryResponse] instance. + MemoryResponse({ + required this.enabled, + }); + + bool enabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryResponse && + other.enabled == enabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode); + + @override + String toString() => 'MemoryResponse[enabled=$enabled]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + return json; + } + + /// Returns a new [MemoryResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryResponse( + enabled: mapValueOfType(json, r'enabled')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + }; +} + diff --git a/mobile/openapi/lib/model/memory_update.dart b/mobile/openapi/lib/model/memory_update.dart new file mode 100644 index 0000000000..f2529186c0 --- /dev/null +++ b/mobile/openapi/lib/model/memory_update.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryUpdate { + /// Returns a new [MemoryUpdate] instance. + MemoryUpdate({ + this.enabled, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryUpdate && + other.enabled == enabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode); + + @override + String toString() => 'MemoryUpdate[enabled=$enabled]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + return json; + } + + /// Returns a new [MemoryUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryUpdate( + enabled: mapValueOfType(json, r'enabled'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index daf8854e01..db514a1d57 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -14,7 +14,6 @@ class UserAdminCreateDto { /// Returns a new [UserAdminCreateDto] instance. UserAdminCreateDto({ required this.email, - this.memoriesEnabled, required this.name, this.notify, required this.password, @@ -25,14 +24,6 @@ class UserAdminCreateDto { String email; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? memoriesEnabled; - String name; /// @@ -61,7 +52,6 @@ class UserAdminCreateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto && other.email == email && - other.memoriesEnabled == memoriesEnabled && other.name == name && other.notify == notify && other.password == password && @@ -73,7 +63,6 @@ class UserAdminCreateDto { int get hashCode => // ignore: unnecessary_parenthesis (email.hashCode) + - (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (name.hashCode) + (notify == null ? 0 : notify!.hashCode) + (password.hashCode) + @@ -82,16 +71,11 @@ class UserAdminCreateDto { (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UserAdminCreateDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UserAdminCreateDto[email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; json[r'email'] = this.email; - if (this.memoriesEnabled != null) { - json[r'memoriesEnabled'] = this.memoriesEnabled; - } else { - // json[r'memoriesEnabled'] = null; - } json[r'name'] = this.name; if (this.notify != null) { json[r'notify'] = this.notify; @@ -126,7 +110,6 @@ class UserAdminCreateDto { return UserAdminCreateDto( email: mapValueOfType(json, r'email')!, - memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), name: mapValueOfType(json, r'name')!, notify: mapValueOfType(json, r'notify'), password: mapValueOfType(json, r'password')!, diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 3fc8c2e274..8060fa7cfc 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -19,7 +19,6 @@ class UserAdminResponseDto { required this.email, required this.id, required this.isAdmin, - this.memoriesEnabled, required this.name, required this.oauthId, required this.profileImagePath, @@ -43,14 +42,6 @@ class UserAdminResponseDto { bool isAdmin; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? memoriesEnabled; - String name; String oauthId; @@ -77,7 +68,6 @@ class UserAdminResponseDto { other.email == email && other.id == id && other.isAdmin == isAdmin && - other.memoriesEnabled == memoriesEnabled && other.name == name && other.oauthId == oauthId && other.profileImagePath == profileImagePath && @@ -97,7 +87,6 @@ class UserAdminResponseDto { (email.hashCode) + (id.hashCode) + (isAdmin.hashCode) + - (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (name.hashCode) + (oauthId.hashCode) + (profileImagePath.hashCode) + @@ -109,7 +98,7 @@ class UserAdminResponseDto { (updatedAt.hashCode); @override - String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -123,11 +112,6 @@ class UserAdminResponseDto { json[r'email'] = this.email; json[r'id'] = this.id; json[r'isAdmin'] = this.isAdmin; - if (this.memoriesEnabled != null) { - json[r'memoriesEnabled'] = this.memoriesEnabled; - } else { - // json[r'memoriesEnabled'] = null; - } json[r'name'] = this.name; json[r'oauthId'] = this.oauthId; json[r'profileImagePath'] = this.profileImagePath; @@ -166,7 +150,6 @@ class UserAdminResponseDto { email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, - memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index ecd145248f..dd0db767fe 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -13,9 +13,7 @@ part of openapi.api; class UserAdminUpdateDto { /// Returns a new [UserAdminUpdateDto] instance. UserAdminUpdateDto({ - this.avatarColor, this.email, - this.memoriesEnabled, this.name, this.password, this.quotaSizeInBytes, @@ -23,14 +21,6 @@ class UserAdminUpdateDto { this.storageLabel, }); - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - UserAvatarColor? avatarColor; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -39,14 +29,6 @@ class UserAdminUpdateDto { /// String? email; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? memoriesEnabled; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -78,9 +60,7 @@ class UserAdminUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto && - other.avatarColor == avatarColor && other.email == email && - other.memoriesEnabled == memoriesEnabled && other.name == name && other.password == password && other.quotaSizeInBytes == quotaSizeInBytes && @@ -90,9 +70,7 @@ class UserAdminUpdateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (avatarColor == null ? 0 : avatarColor!.hashCode) + (email == null ? 0 : email!.hashCode) + - (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (name == null ? 0 : name!.hashCode) + (password == null ? 0 : password!.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + @@ -100,25 +78,15 @@ class UserAdminUpdateDto { (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UserAdminUpdateDto[email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; - if (this.avatarColor != null) { - json[r'avatarColor'] = this.avatarColor; - } else { - // json[r'avatarColor'] = null; - } if (this.email != null) { json[r'email'] = this.email; } else { // json[r'email'] = null; } - if (this.memoriesEnabled != null) { - json[r'memoriesEnabled'] = this.memoriesEnabled; - } else { - // json[r'memoriesEnabled'] = null; - } if (this.name != null) { json[r'name'] = this.name; } else { @@ -155,9 +123,7 @@ class UserAdminUpdateDto { final json = value.cast(); return UserAdminUpdateDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), - memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart new file mode 100644 index 0000000000..673f5bfaf8 --- /dev/null +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class UserPreferencesResponseDto { + /// Returns a new [UserPreferencesResponseDto] instance. + UserPreferencesResponseDto({ + required this.avatar, + required this.memories, + }); + + AvatarResponse avatar; + + MemoryResponse memories; + + @override + bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && + other.avatar == avatar && + other.memories == memories; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (avatar.hashCode) + + (memories.hashCode); + + @override + String toString() => 'UserPreferencesResponseDto[avatar=$avatar, memories=$memories]'; + + Map toJson() { + final json = {}; + json[r'avatar'] = this.avatar; + json[r'memories'] = this.memories; + return json; + } + + /// Returns a new [UserPreferencesResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static UserPreferencesResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return UserPreferencesResponseDto( + avatar: AvatarResponse.fromJson(json[r'avatar'])!, + memories: MemoryResponse.fromJson(json[r'memories'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserPreferencesResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = UserPreferencesResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of UserPreferencesResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = UserPreferencesResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'avatar', + 'memories', + }; +} + diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart new file mode 100644 index 0000000000..887293931c --- /dev/null +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class UserPreferencesUpdateDto { + /// Returns a new [UserPreferencesUpdateDto] instance. + UserPreferencesUpdateDto({ + this.avatar, + this.memories, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AvatarUpdate? avatar; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + MemoryUpdate? memories; + + @override + bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && + other.avatar == avatar && + other.memories == memories; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (avatar == null ? 0 : avatar!.hashCode) + + (memories == null ? 0 : memories!.hashCode); + + @override + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, memories=$memories]'; + + Map toJson() { + final json = {}; + if (this.avatar != null) { + json[r'avatar'] = this.avatar; + } else { + // json[r'avatar'] = null; + } + if (this.memories != null) { + json[r'memories'] = this.memories; + } else { + // json[r'memories'] = null; + } + return json; + } + + /// Returns a new [UserPreferencesUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static UserPreferencesUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return UserPreferencesUpdateDto( + avatar: AvatarUpdate.fromJson(json[r'avatar']), + memories: MemoryUpdate.fromJson(json[r'memories']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserPreferencesUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = UserPreferencesUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of UserPreferencesUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = UserPreferencesUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 1b54d4a383..2d665fc784 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -13,21 +13,11 @@ part of openapi.api; class UserUpdateMeDto { /// Returns a new [UserUpdateMeDto] instance. UserUpdateMeDto({ - this.avatarColor, this.email, - this.memoriesEnabled, this.name, this.password, }); - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - UserAvatarColor? avatarColor; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -36,14 +26,6 @@ class UserUpdateMeDto { /// String? email; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? memoriesEnabled; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -62,41 +44,27 @@ class UserUpdateMeDto { @override bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto && - other.avatarColor == avatarColor && other.email == email && - other.memoriesEnabled == memoriesEnabled && other.name == name && other.password == password; @override int get hashCode => // ignore: unnecessary_parenthesis - (avatarColor == null ? 0 : avatarColor!.hashCode) + (email == null ? 0 : email!.hashCode) + - (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (name == null ? 0 : name!.hashCode) + (password == null ? 0 : password!.hashCode); @override - String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password]'; + String toString() => 'UserUpdateMeDto[email=$email, name=$name, password=$password]'; Map toJson() { final json = {}; - if (this.avatarColor != null) { - json[r'avatarColor'] = this.avatarColor; - } else { - // json[r'avatarColor'] = null; - } if (this.email != null) { json[r'email'] = this.email; } else { // json[r'email'] = null; } - if (this.memoriesEnabled != null) { - json[r'memoriesEnabled'] = this.memoriesEnabled; - } else { - // json[r'memoriesEnabled'] = null; - } if (this.name != null) { json[r'name'] = this.name; } else { @@ -118,9 +86,7 @@ class UserUpdateMeDto { final json = value.cast(); return UserUpdateMeDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), - memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), ); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 558823e62b..d875994865 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -432,6 +432,98 @@ ] } }, + "/admin/users/{id}/preferences": { + "get": { + "operationId": "getUserPreferencesAdmin", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "User" + ] + }, + "put": { + "operationId": "updateUserPreferencesAdmin", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "User" + ] + } + }, "/admin/users/{id}/restore": { "post": { "operationId": "restoreUserAdmin", @@ -6403,6 +6495,78 @@ ] } }, + "/users/me/preferences": { + "get": { + "operationId": "getMyPreferences", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "User" + ] + }, + "put": { + "operationId": "updateMyPreferences", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "User" + ] + } + }, "/users/profile-image": { "delete": { "operationId": "deleteProfileImage", @@ -7621,6 +7785,25 @@ ], "type": "object" }, + "AvatarResponse": { + "properties": { + "color": { + "$ref": "#/components/schemas/UserAvatarColor" + } + }, + "required": [ + "color" + ], + "type": "object" + }, + "AvatarUpdate": { + "properties": { + "color": { + "$ref": "#/components/schemas/UserAvatarColor" + } + }, + "type": "object" + }, "BulkIdResponseDto": { "properties": { "error": { @@ -8584,6 +8767,17 @@ ], "type": "object" }, + "MemoryResponse": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, "MemoryResponseDto": { "properties": { "assets": { @@ -8650,6 +8844,14 @@ ], "type": "string" }, + "MemoryUpdate": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, "MemoryUpdateDto": { "properties": { "isSaved": { @@ -10878,9 +11080,6 @@ "email": { "type": "string" }, - "memoriesEnabled": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -10942,9 +11141,6 @@ "isAdmin": { "type": "boolean" }, - "memoriesEnabled": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -11000,15 +11196,9 @@ }, "UserAdminUpdateDto": { "properties": { - "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" - }, "email": { "type": "string" }, - "memoriesEnabled": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -11046,6 +11236,32 @@ ], "type": "string" }, + "UserPreferencesResponseDto": { + "properties": { + "avatar": { + "$ref": "#/components/schemas/AvatarResponse" + }, + "memories": { + "$ref": "#/components/schemas/MemoryResponse" + } + }, + "required": [ + "avatar", + "memories" + ], + "type": "object" + }, + "UserPreferencesUpdateDto": { + "properties": { + "avatar": { + "$ref": "#/components/schemas/AvatarUpdate" + }, + "memories": { + "$ref": "#/components/schemas/MemoryUpdate" + } + }, + "type": "object" + }, "UserResponseDto": { "properties": { "avatarColor": { @@ -11083,15 +11299,9 @@ }, "UserUpdateMeDto": { "properties": { - "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" - }, "email": { "type": "string" }, - "memoriesEnabled": { - "type": "boolean" - }, "name": { "type": "string" }, diff --git a/open-api/typescript-sdk/README.md b/open-api/typescript-sdk/README.md index 53a83a4237..046cea7695 100644 --- a/open-api/typescript-sdk/README.md +++ b/open-api/typescript-sdk/README.md @@ -13,22 +13,13 @@ npm i --save @immich/sdk For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli). ```typescript -<<<<<<< HEAD -import { getAllAlbums, getAllAssets, getMyUser, init } from "@immich/sdk"; -======= -import { getAllAlbums, getMyUserInfo, init } from "@immich/sdk"; ->>>>>>> e7c8501930a988dfb6c23ce1c48b0beb076a58c2 +import { getAllAlbums, getMyUser, init } from "@immich/sdk"; const API_KEY = ""; // process.env.IMMICH_API_KEY init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY }); -<<<<<<< HEAD const user = await getMyUser(); -const assets = await getAllAssets({ take: 1000 }); -======= -const user = await getMyUserInfo(); ->>>>>>> e7c8501930a988dfb6c23ce1c48b0beb076a58c2 const albums = await getAllAlbums({}); console.log({ user, albums }); diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2c07072f68..8030c92d44 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -45,7 +45,6 @@ export type UserAdminResponseDto = { email: string; id: string; isAdmin: boolean; - memoriesEnabled?: boolean; name: string; oauthId: string; profileImagePath: string; @@ -58,7 +57,6 @@ export type UserAdminResponseDto = { }; export type UserAdminCreateDto = { email: string; - memoriesEnabled?: boolean; name: string; notify?: boolean; password: string; @@ -70,15 +68,33 @@ export type UserAdminDeleteDto = { force?: boolean; }; export type UserAdminUpdateDto = { - avatarColor?: UserAvatarColor; email?: string; - memoriesEnabled?: boolean; name?: string; password?: string; quotaSizeInBytes?: number | null; shouldChangePassword?: boolean; storageLabel?: string | null; }; +export type AvatarResponse = { + color: UserAvatarColor; +}; +export type MemoryResponse = { + enabled: boolean; +}; +export type UserPreferencesResponseDto = { + avatar: AvatarResponse; + memories: MemoryResponse; +}; +export type AvatarUpdate = { + color?: UserAvatarColor; +}; +export type MemoryUpdate = { + enabled?: boolean; +}; +export type UserPreferencesUpdateDto = { + avatar?: AvatarUpdate; + memories?: MemoryUpdate; +}; export type AlbumUserResponseDto = { role: AlbumUserRole; user: UserResponseDto; @@ -1073,9 +1089,7 @@ export type TimeBucketResponseDto = { timeBucket: string; }; export type UserUpdateMeDto = { - avatarColor?: UserAvatarColor; email?: string; - memoriesEnabled?: boolean; name?: string; password?: string; }; @@ -1200,6 +1214,29 @@ export function updateUserAdmin({ id, userAdminUpdateDto }: { body: userAdminUpdateDto }))); } +export function getUserPreferencesAdmin({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserPreferencesResponseDto; + }>(`/admin/users/${encodeURIComponent(id)}/preferences`, { + ...opts + })); +} +export function updateUserPreferencesAdmin({ id, userPreferencesUpdateDto }: { + id: string; + userPreferencesUpdateDto: UserPreferencesUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserPreferencesResponseDto; + }>(`/admin/users/${encodeURIComponent(id)}/preferences`, oazapfts.json({ + ...opts, + method: "PUT", + body: userPreferencesUpdateDto + }))); +} export function restoreUserAdmin({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -2780,6 +2817,26 @@ export function updateMyUser({ userUpdateMeDto }: { body: userUpdateMeDto }))); } +export function getMyPreferences(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserPreferencesResponseDto; + }>("/users/me/preferences", { + ...opts + })); +} +export function updateMyPreferences({ userPreferencesUpdateDto }: { + userPreferencesUpdateDto: UserPreferencesUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserPreferencesResponseDto; + }>("/users/me/preferences", oazapfts.json({ + ...opts, + method: "PUT", + body: userPreferencesUpdateDto + }))); +} export function deleteProfileImage(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchText("/users/profile-image", { ...opts, diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index 4d0b781e81..83b5156eda 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; +import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { UserAdminCreateDto, UserAdminDeleteDto, @@ -55,6 +56,22 @@ export class UserAdminController { return this.service.delete(auth, id, dto); } + @Get(':id/preferences') + @Authenticated() + getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getPreferences(auth, id); + } + + @Put(':id/preferences') + @Authenticated() + updateUserPreferencesAdmin( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: UserPreferencesUpdateDto, + ): Promise { + return this.service.updatePreferences(auth, id, dto); + } + @Post(':id/restore') @Authenticated({ admin: true }) restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index f66807b92c..66a92e1a3f 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -17,6 +17,7 @@ import { import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { AuthDto } from 'src/dtos/auth.dto'; +import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -52,6 +53,21 @@ export class UserController { return this.service.updateMe(auth, dto); } + @Get('me/preferences') + @Authenticated() + getMyPreferences(@Auth() auth: AuthDto): UserPreferencesResponseDto { + return this.service.getMyPreferences(auth); + } + + @Put('me/preferences') + @Authenticated() + updateMyPreferences( + @Auth() auth: AuthDto, + @Body() dto: UserPreferencesUpdateDto, + ): Promise { + return this.service.updateMyPreferences(auth, dto); + } + @Get(':id') @Authenticated() getUser(@Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts new file mode 100644 index 0000000000..2dd9492d07 --- /dev/null +++ b/server/src/dtos/user-preferences.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEnum, ValidateNested } from 'class-validator'; +import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; +import { Optional, ValidateBoolean } from 'src/validation'; + +class AvatarUpdate { + @Optional() + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + color?: UserAvatarColor; +} + +class MemoryUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; +} + +export class UserPreferencesUpdateDto { + @Optional() + @ValidateNested() + @Type(() => AvatarUpdate) + avatar?: AvatarUpdate; + + @Optional() + @ValidateNested() + @Type(() => MemoryUpdate) + memories?: MemoryUpdate; +} + +class AvatarResponse { + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + color!: UserAvatarColor; +} + +class MemoryResponse { + enabled!: boolean; +} + +export class UserPreferencesResponseDto implements UserPreferences { + memories!: MemoryResponse; + avatar!: AvatarResponse; +} + +export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { + return preferences; +}; diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 8290df6adb..63bac60d06 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; import { UserAvatarColor } from 'src/entities/user-metadata.entity'; import { UserEntity, UserStatus } from 'src/entities/user.entity'; import { getPreferences } from 'src/utils/preferences'; @@ -22,14 +22,6 @@ export class UserUpdateMeDto { @IsString() @IsNotEmpty() name?: string; - - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - - @Optional() - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - avatarColor?: UserAvatarColor; } export class UserResponseDto { @@ -37,7 +29,6 @@ export class UserResponseDto { name!: string; email!: string; profileImagePath!: string; - @IsEnum(UserAvatarColor) @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) avatarColor!: UserAvatarColor; } @@ -75,9 +66,6 @@ export class UserAdminCreateDto { @Transform(toSanitized) storageLabel?: string | null; - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - @Optional({ nullable: true }) @IsNumber() @IsPositive() @@ -116,14 +104,6 @@ export class UserAdminUpdateDto { @ValidateBoolean({ optional: true }) shouldChangePassword?: boolean; - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - - @Optional() - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - avatarColor?: UserAvatarColor; - @Optional({ nullable: true }) @IsNumber() @IsPositive() @@ -144,7 +124,6 @@ export class UserAdminResponseDto extends UserResponseDto { deletedAt!: Date | null; updatedAt!: Date; oauthId!: string; - memoriesEnabled?: boolean; @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes!: number | null; @ApiProperty({ type: 'integer', format: 'int64' }) @@ -163,7 +142,6 @@ export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { deletedAt: entity.deletedAt, updatedAt: entity.updatedAt, oauthId: entity.oauthId, - memoriesEnabled: getPreferences(entity).memories.enabled, quotaSizeInBytes: entity.quotaSizeInBytes, quotaUsageInBytes: entity.quotaUsageInBytes, status: entity.status, diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 1b93f96e71..72330ac9b7 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, ForbiddenException, Inject, Injectable } from '@ne import { SALT_ROUNDS } from 'src/constants'; import { UserCore } from 'src/cores/user.core'; import { AuthDto } from 'src/dtos/auth.dto'; +import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { UserAdminCreateDto, UserAdminDeleteDto, @@ -17,7 +18,7 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; -import { getPreferences, getPreferencesPartial } from 'src/utils/preferences'; +import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() export class UserAdminService { @@ -40,18 +41,8 @@ export class UserAdminService { } async create(dto: UserAdminCreateDto): Promise { - const { memoriesEnabled, notify, ...rest } = dto; - let user = await this.userCore.createUser(rest); - - // TODO remove and replace with entire dto.preferences config - if (memoriesEnabled === false) { - await this.userRepository.upsertMetadata(user.id, { - key: UserMetadataKey.PREFERENCES, - value: { memories: { enabled: false } }, - }); - - user = await this.findOrFail(user.id, {}); - } + const { notify, ...rest } = dto; + const user = await this.userCore.createUser(rest); const tempPassword = user.shouldChangePassword ? rest.password : undefined; if (notify) { @@ -72,25 +63,6 @@ export class UserAdminService { await this.userRepository.syncUsage(id); } - // TODO replace with entire preferences object - if (dto.memoriesEnabled !== undefined || dto.avatarColor) { - const newPreferences = getPreferences(user); - if (dto.memoriesEnabled !== undefined) { - newPreferences.memories.enabled = dto.memoriesEnabled; - delete dto.memoriesEnabled; - } - - if (dto.avatarColor) { - newPreferences.avatar.color = dto.avatarColor; - delete dto.avatarColor; - } - - await this.userRepository.upsertMetadata(id, { - key: UserMetadataKey.PREFERENCES, - value: getPreferencesPartial(user, newPreferences), - }); - } - if (dto.email) { const duplicate = await this.userRepository.getByEmail(dto.email); if (duplicate && duplicate.id !== id) { @@ -144,6 +116,24 @@ export class UserAdminService { return mapUserAdmin(user); } + async getPreferences(auth: AuthDto, id: string): Promise { + const user = await this.findOrFail(id, { withDeleted: false }); + const preferences = getPreferences(user); + return mapPreferences(preferences); + } + + async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) { + const user = await this.findOrFail(id, { withDeleted: false }); + const preferences = mergePreferences(user, dto); + + await this.userRepository.upsertMetadata(user.id, { + key: UserMetadataKey.PREFERENCES, + value: getPreferencesPartial(user, preferences), + }); + + return mapPreferences(preferences); + } + private async findOrFail(id: string, options: UserFindOptions) { const user = await this.userRepository.get(id, options); if (!user) { diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 1f36501051..3920dbeaac 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -4,6 +4,7 @@ import { SALT_ROUNDS } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; +import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; import { UserMetadataKey } from 'src/entities/user-metadata.entity'; @@ -16,7 +17,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; -import { getPreferences, getPreferencesPartial } from 'src/utils/preferences'; +import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() export class UserService { @@ -45,25 +46,6 @@ export class UserService { } async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise { - // TODO replace with entire preferences object - if (dto.memoriesEnabled !== undefined || dto.avatarColor) { - const newPreferences = getPreferences(user); - if (dto.memoriesEnabled !== undefined) { - newPreferences.memories.enabled = dto.memoriesEnabled; - delete dto.memoriesEnabled; - } - - if (dto.avatarColor) { - newPreferences.avatar.color = dto.avatarColor; - delete dto.avatarColor; - } - - await this.userRepository.upsertMetadata(user.id, { - key: UserMetadataKey.PREFERENCES, - value: getPreferencesPartial(user, newPreferences), - }); - } - if (dto.email) { const duplicate = await this.userRepository.getByEmail(dto.email); if (duplicate && duplicate.id !== user.id) { @@ -87,6 +69,22 @@ export class UserService { return mapUserAdmin(updatedUser); } + getMyPreferences({ user }: AuthDto): UserPreferencesResponseDto { + const preferences = getPreferences(user); + return mapPreferences(preferences); + } + + async updateMyPreferences({ user }: AuthDto, dto: UserPreferencesUpdateDto) { + const preferences = mergePreferences(user, dto); + + await this.userRepository.upsertMetadata(user.id, { + key: UserMetadataKey.PREFERENCES, + value: getPreferencesPartial(user, preferences), + }); + + return mapPreferences(preferences); + } + async get(id: string): Promise { const user = await this.findOrFail(id, { withDeleted: false }); return mapUser(user); diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index ae10c24fc9..f3561fa7b6 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { UserMetadataKey, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { getKeysDeep } from 'src/utils/misc'; @@ -37,3 +38,12 @@ export const getPreferencesPartial = (user: { email: string }, newPreferences: U return partial; }; + +export const mergePreferences = (user: UserEntity, dto: UserPreferencesUpdateDto) => { + const preferences = getPreferences(user); + for (const key of getKeysDeep(dto)) { + _.set(preferences, key, _.get(dto, key)); + } + + return preferences; +}; diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte index 8c73ed4d84..5d5a351de2 100644 --- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte @@ -1,18 +1,18 @@