From 0fc6d6982466b73ecc284ee3ec2c99120bee8acc Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jrasm91@gmail.com>
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 <alex.tran1502@gmail.com>
---
 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                      | Bin 27685 -> 28457 bytes
 mobile/openapi/lib/api.dart                   | Bin 9821 -> 10053 bytes
 mobile/openapi/lib/api/user_api.dart          | Bin 20999 -> 27808 bytes
 mobile/openapi/lib/api_client.dart            | Bin 26388 -> 26904 bytes
 mobile/openapi/lib/model/avatar_response.dart | Bin 0 -> 2752 bytes
 mobile/openapi/lib/model/avatar_update.dart   | Bin 0 -> 3144 bytes
 mobile/openapi/lib/model/memory_response.dart | Bin 0 -> 2761 bytes
 mobile/openapi/lib/model/memory_update.dart   | Bin 0 -> 3157 bytes
 .../lib/model/user_admin_create_dto.dart      | Bin 6377 -> 5602 bytes
 .../lib/model/user_admin_response_dto.dart    | Bin 8043 -> 7268 bytes
 .../lib/model/user_admin_update_dto.dart      | Bin 7750 -> 6241 bytes
 .../model/user_preferences_response_dto.dart  | Bin 0 -> 3234 bytes
 .../model/user_preferences_update_dto.dart    | Bin 0 -> 4054 bytes
 .../openapi/lib/model/user_update_me_dto.dart | Bin 6002 -> 4493 bytes
 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, 532 insertions(+), 221 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<AuthenticationState> {
       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<AuthenticationState> {
         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<User?> {
   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 273585c368c90230e296d94cb9cc25bad0079344..cdc75d4f28f2eaf421ce27cfe018d4f6990c30bb 100644
GIT binary patch
delta 467
zcmZ2_gK_0O#tmYQ+yO<YX{kl2dC958lNFuBd2=fZkVGar3iIWr>LUv!+6!+MbWC6r
zN>42bElw>$HbBNnfg4$^jgt}&hUP*iWkxh>MJBIt=FmaXsi{z-prsWM8lt78pkE5K
zrC1-+8IvD4vrV4lyl^s`6F-p6g<`0TlcZ>AK}uptDo8t$(UW;yP#v{d-{m)lw_{mi
zNn%k@YH>k+UU6!yMoNCNzCNm;UT%semx8WBG(<QQY&wcch|p$5-<Ry7Pzm4E-29?S
zn7NZbhh$Gq3>Dt&8G2Nh0~U0XKSqd7R>)-Gf(az2PM+v4%8wqr8#6>i5K4+6wz-t#
KZ)VI?VFm#8HLI}z

delta 51
zcmV-30L=fX-T|f90k9T9vwlHa0kc;_BLS1>M4z(@M)wG_R!`sulX+GPlWkW?v))&*
J6SER*7Xvm36z~85

diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index d7223a1ecf38cabb8f2f634c24c5aa2c40a5427c..94303a768fefcdea817e7be24fe119f8985f31ca 100644
GIT binary patch
delta 117
zcmccXbJTAG8!umCSz<|IQG8KqaY24w@nk_!Q68vJX+cV22}oeF9xwZ5US4mu$wHzc
zlM95|CkF`oz}TCo30v`V6cnYVr52^;O)d~qnWV(Z4dW-L7EfNsCJz+ad_k#~82|yO
BCXfIC

delta 30
mcmX@=ch_eF8}DW>-XONg`-NR5iz!QNwh@ux-@HS)ml*)91`0a>

diff --git a/mobile/openapi/lib/api/user_api.dart b/mobile/openapi/lib/api/user_api.dart
index 3c1a3ff4e7bebb952c45462a3102e6f6fa936503..246ea422c55fd66938ed394488f05c4641e0b0ca 100644
GIT binary patch
delta 1204
zcmZo)!noij<Ay3J9{qx%)U?#1)V$=>;>i~owI-`s2=b?=miSf%z$GR-T14^b=cXd5
zoxH&A$mBT7PkN!nsYM8t#X+gX1^Ic!sV*h?_I3(r<`ip8{-~kBi)?i9W;t0aCT=7X
zCQmdML*W-_drzL?Vgz#7=B-i!;*;mv3JB^~XQtF5xd-HS&&m6g6eh3X<eF@!!;WG^
zqEQJql0P@!F*0YI{9gA4ilU1eqA2`_dNwHhjpCx4<4tw(JN5!c_~vVd$Ar+s)pK$I
zCyNG>H$w|j5=%hA6su5*8iYWZ$rCu$yi36<Py#DFvn0c#q@=(zFD+jKE#%<_Vl(d}
zr=Kv$nqrJtw3&Rs2PLpJpYd7CNUCdPV$fWJnhvmdg(T-6<k6XI>%pOp6n-c{ffj<B
z8~x1*MU(sH2`;6)lOK5LVMf(HCCSMJ-hw1Wmng)KXu&W!(MU!|0ZouVOhryUZ^VY2
zTqoZ#io_C2*BME1piCZ`E3m~7ni9v9+{`?U;F6-uymW=k6w*8bj58czvQb<LIb#tM
JTe?iOTmaY|&#eFe

delta 51
zcmV-30L=fO*#U>70kC=+vkD+f7qhQD*AkNoAS#ofHy@L52?UdmS~8R0Hwu%mUIw!m
JSlSE%eG30q63hSq

diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index bd3433872affc4c0b037a8ad22de36e3480fc2c3..bf306ac10a0617c451855f287fa25538cddad012 100644
GIT binary patch
delta 197
zcmbPoj&a5%#tkl-ypCmwC5c5rsl^5PdBu|rwM5w<B9psZHMk+n(1Mi25|9)Mf1<16
zW*5z3zR5;fB9qUV^G%-QsxbM#Ioss*=9{5nY?JFP*kIzDV=N9Ras(8mrll68=1tDG
lQ{oFPPA!57B&SYZD6hkhDpWj~QC1d1EKyo>vqaP%9soVGNf7`5

delta 32
qcmV+*0N?+Z(gBpt0kA|Vv*0On4wLye9h1N~j<cFMx*@Y5T=)$3$PTsu

diff --git a/mobile/openapi/lib/model/avatar_response.dart b/mobile/openapi/lib/model/avatar_response.dart
new file mode 100644
index 0000000000000000000000000000000000000000..edd242df4e3be1a6861db070ef1cd17963868507
GIT binary patch
literal 2752
zcmbVOU2oeq6n*!vxB-S(0aSV0Q<2PGgT@)!wK0&U4~1a}j6_@PWKkoj7)I*<zI!hv
zS#sPaoq@y_c|XrNm(*x97>(fa^WE&#U(=iE?d9F{3a;OMn1*mQgPYkMe4JgqyZ&;5
zW@Py$6~>K!kA8bSphvYTw2|>rn|P^mc@9-kS((N%mup$Luz6PNQX6;FL*yH=wQ*Uw
z*vNk?WzfA4Yy4jdgWpaXi@}XO?w;tvI#IY-rI=7Hl{D;bk0#4iA#I%NVufZd6Gi;`
z*Eq?9v4a7obD)=?OIe6gMfi6y7$mu{7A_xz6XsUh64Oa|8~}{4{afiOQ&<pC$PLW<
zRtr#ES|TGf@9zZ^0ibCZT&8T4=v<o+H+E;37~r#&#=E_x9pHW5d7~esG1xI$-D{mw
z%|<x18IOb6{a2Y_CoYrb48{}i$Rr{UC{5VxeEa5qfqK&4=_!}TA$(aF_&fPk;b0{l
zWWr`|bt*9<JGbf}Q4}(f)(W#FQjw=jT4mWR;I5*^+jrjW?~^mCSb@|2N5Cx&`*6-V
z$oLojS_~PZkD~m;yM&N#QBf+HV3hY(7Z}6izf^_D;VUk|7@k-^?k>1QV2imKa`@o_
zZTEb66T?DaJ6hNgeLo=(5Jd`pMl2|2;X&k;w6HXKT}PVp5^8FR21AnE96MZuP*gec
zPh@YDWvO+LFO6KH(s@Wsm7qdwOxlTYW}Hf59Is5L#Drfskm;XX0Yg?p=qqdR!(sxi
zi!_AgH0uC8WZ5K2gmq6a4m{z07-*OdDhRMN|6%aY4~HTmh`2yvanI4&2@Mi`Fadq-
z_<zk4Vb=0gcm6?Pd3{+;Ajie)CPM=OWeVYu2XAHcM%?G9dRU6w%5c|U;1Mv=R=Fc}
z9fY1Qd|d<~1tbKi1UNlq8AZz8Xg6J#ZremeRB^(CI<Vg0`NF3A{RVf>$@l=vJyuwB
z4GTfDXYpjra_R=8LyB66M4Ec8JDwiY2HYORwU^V>RM5b54$Uu)!gEsN-<{V3tY9t5
zqbxLqdPvZ!JH`h=nrZF=jyZpbHxo1Ve0)#7IOp{>cn<B272~CWcO%__{+R1=hpT4W
zuouyjlD6rE81el)$0CP`*s8<HGi)MAbMN?%+|p6T{$7ie`UjMvYdbX9G(OTX#29IR
z=WLYAaEYddZqhyjue=fZ)aM(X#%N406ch{R^N12iJ%^(+A#>U|s)?6QCdTNh<!GY+
zqnQ`V;U&_lPq5C^KdT(<s!wm9gNUeUVe1GeYzK2cqG78lFN)rLW1@~2MsSzOmiP9x
z1|??kt`tacg_5|JfESX*v?P83??26M=-sI1oo}i3o#=1!ZExktBZE<2!TgeRF~H;F
EKgd;lPyhe`

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b92eb8dcbdb2d0ecc1f50f96efbcc0c083672e2d
GIT binary patch
literal 3144
zcmbVOU2ob*6n*Dc+*GTENW^sasiF&Ml#f-rO;|O_!wN;#U=J{@nPF$fK`7<F@3}KJ
zm<>szjT8*_{XFN~>*;n|-7cNp-42icxVXOfdU|_tPM4p)Ty*GsNY}$#x*DE;y8Qb9
zW^DN}7uNTGOMZRd;-6}&jg{%d*mR-_*`rdIPUfjB<Xmdsseh~a!dSo8LljG~a_OS<
zsg?hf%EDcXIX)M{;_9V!2yWee_snSLh4!h+kx)&Pw87o&O;#u^U0N7Dg_)lToxXpU
zW;0=3tA%u)%!JHBYOzoW{y(%@Ss|RG(+A;&y<6nMOL}Y(b<zA+=}W5}3DR;&qo&P0
zsZdrX6Bsw2wAq`;&y-6tQyAO9@AVNfg`n}1{wkz!lC<%_^cJ?Xu&B%W6}g!yi=1ZS
zLDE=C4I_Q5%c2nDLLShK(wRhfNp2xC;E@Thj3%b13Z5J3I5cX^iZYjIAeR$S7M>nN
zQA%o;PU&d~@}_#yd$XzFs6#Weln>H&g2_~BX@N^#g?Fa3C?PY5JTIN6i6{yhi|jjj
z!*z3PU{;k(ezgcFF{Ec;6GU1Pc^=L76o9&)X)q*!KOO@zMvvh~V0_*=X(JJ54EqTR
z#3kVV(V!U%KbVb;DOdvN$XM$4+r#lUnL!wU)E-fPKv8I!DB7H*Q$P9fBYq=fZW2y!
z-y$r<hmmrGeI~ooo~F_RB{w@`0Q-aBycso~FfB9boUkjN5Qu&zopQmf9eU#Mj4qwf
zJL<(u2T2_=O0C)Qjkker6Th@Wmxs+BMj>whmvFg144_?!#qZIfL-`65tFi&`|LlxL
zgsAd~(xRYmxJ7+>V*R-O5UN%a3Q$(y9K`DD7!>?R_An$ryho(%@WcRXk_DzE<^)tv
zJs6J4Hl(y+bb}q=URTrix{)TKbBY_yk@7B1jiZS*a|B}@kHR2f3rt{e$SYz5yzV=Z
zm!PGka+Nx(E!x9`$qDvSSZTJ*AV8P0G*fIiud)=&aAtC*I^NjP*-6MB;IHV2b=5dB
z%5l6QP!v<&C#;4BOTi?`g!51MYDej?m@E968fgb$`C%Zc_uqj_7@klb>tW015idag
zLv+5Z!)KMC<}DCZHT^}Qwr1u56}WX-hp)TmG#z>j13k57$@>xF!8UTTv##5Z&0Fbs
z{XwKVfk*eTCdQ7#@oaN?%yiIBqTIFd7xRT*)$sr)_C~u3YnHsM*me8720W6{-yv}e
z5(=&WVNkZ@J(&)E9LrCzG$X|g&j@D?>khb;mp9x}yN0Mo^|uhEiF|Y9cW|P)SnS2W
z?mEH&tL7fq3DH~zHPp!WBGSRGADMVgu^7kgeANev6z_B#UHFI+7v~OZxNBE6ZwdcW
z($$?X+_9UN`3pgkVAEjZCp-e6QFQz+SG?(hf7Bo+et}H5HZ~a(!doXGWa1lasr(GL
zcvkqB(?tI`n!ux4X8a<>sen&Jh=hr(X78%8^jS5#<1%1lzP(oquboB2XsTpSn*XDf
zSIXculB)JqW6po781%GDY!m&2DcL~Q5Kd<^irb;=Y}6FRurtv}(-1)yCr7#AyFOeK
zq~-7m6`+<+XJg6l9f!0mc|o6VhSz**su&M1BP^JBGXIl(H?Q)+Bkb@J$By_Hl4bVv

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/memory_response.dart b/mobile/openapi/lib/model/memory_response.dart
new file mode 100644
index 0000000000000000000000000000000000000000..fb34bc1518876a70c3b39584e94c733993e8d147
GIT binary patch
literal 2761
zcmbVOZExE)5dQ98aRG)z0aSV0ry{An7K<~qYhobv1`LKFFcM|4lSz%FY8a{i`|e0l
zksP<lX27;Y9q;9Np5tje9*xKF?vKU%)o-)g+1<Ov><X^mf1Jf|HHX{z0&eD4@2~$l
zK{K*^n=@@EKPNxE9?`8>OQm^QD4iCf;up}!#_&Al72j}a<8UuFwbJ(J4pwd1&ZKo?
zQ_cTrgvRZXZSc2d8viZV2Aykje0!#(u}s=j<QPyCf@`<k91T_p$xT`*xkfWvGMT>q
zG0jS*&1i(-ET{rh%_XZvg5S&0D65z;aKkrB@9wy%F&u}75x^MBzvH&i(f|X=x3C;q
ztw6ZcGM=FM@L5D207AiFOJNcWdtdQ9#z*^e3=MFvR0Zv_TX?I!aILY_@DP*L%qk6&
zNi@Iz%rh*(mRz61WC|XVj8zf&kHeF%U;Pz`CxxA!y1bl=kPo{4W)4DHSaS;{Gv%);
z=NQ#F=%hi4kUZnYFufy2k))CvVVqgi)|Q0*zU+RLpR5*g4OaP!$Q5=rM044?nf%aR
ztj-)-H>`f+x5bd}P*ftDqNfi=N%Z3>D1>Ace8wf1z!U3-{iRD3oW)X~wE@mnXx%5x
zn`x&9XG;rv;_yG{1enahw~7H_4SZo$!wnQ#ZCX!Dw}jFv(BO=ex5O$}G03K>Jgr+{
zoZ@;M)S4G4c-Jes$xtsQB_*ZUI#y&Uc3G)%;>0f;sPz|@frG7u(PtJg#AG6ums1q0
z!#cnKU1ySH%-AP5N1k}!6Vy`&8AO<x|Fi^Lz=qX-5<K%_v8(wTYL#h3JjU=y)A<Q?
z6MZlM{q5av%M)SN@zhrSQDAyQc}=0h#cL#C0D+J7;Cghe)s~tURZ+0Y@OU3E@Cayb
zn#z*G4qPu+J}-ih91?<*0-T;Y870EpDm&ZMb{9M%s@U&Q>sW2^m~p0$&jaqBlgR;=
z3CYP-f=Rcq5Hyc0o=lva`VQ%=q7o98=br1{P7i7TcK~s_%PJ}=3NT$jxN=@xp9JOg
zdiDS-qWmZeAyE$rTJ>A`z)9%nKH%6v-^JSzGx2<UOTO6W?KHfAZg8bIJ8(qODD(&3
zfID2(yTBfvws_KU6KWwwd_B*x=-edEs>jJQY$8Z_YGVB4Z@#0$%<cDDq|o1?6t{Lw
zgGJ*l9YYQy<tNZq*b<khYiOVj8F=N5a8G-_;fajK^j<-}us@F|fz)%@I#V*I2S+s#
z!^=7oee~6GG|>Oi%nRl45^1$3Sg-4!RSs4)q<6?cLezAy^#l}mjrrK3ai=OTia~#4
zpq?1Uuqb)Qd-ryO5;Hh084}!}B<?xjKr$GX#LwWvr}-_tBDK7`Db>9i{Z4zk$vk~<
PV02^Hy(?Xg@BsN2j}w2k

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/memory_update.dart b/mobile/openapi/lib/model/memory_update.dart
new file mode 100644
index 0000000000000000000000000000000000000000..f2529186c0432db7c348cb2f75849bc1e196b924
GIT binary patch
literal 3157
zcmbVO?@t>!5dEINVyI3fNK5G5r;0YARe@7`g`5gK>4ebA*>#B9jlH$KL=nyZes64V
z*oZ*UA+bs9@%(u2&8!a&dItye{_Ev<|Id^2lP~WtPmbyI<L8r%j>mL9zNEA9@yFAD
zcVNbr?@D3)@Q=aouY3Gc-DqQFJ~cL<s!ASGqZ=p7Tvl=}wa?n$YF-=bx1JEyLM&Zg
zH$J!WN24r`Yca>O78ajgS_kLWZ9iTZ?Yz)FS0w_fsggDvcUyy1N=uhlM&H29&xFoj
zzsif5u&&obI8SCurj}aNYJmSYy<SlX=jcq%ja`1NOW`Hm^@t8o{1@pPs~ri_azR&J
zn`=@btwIi9+}-K(X(B&UZh$zGN|qT`R(psQ+{P#UT}k02Y2$(FC9J8o$jtgBxtVFI
zl4jyo(nLxPBfYPis$y?DbfI)15kQiwWdTGA;g!+E1SRLCk&ay>%c5#Zi4saV6;0*o
zR#c6oe&dwB2~J+5ox>OF4E8dbnT5QSHVY;<QcDYD%H-ae#v+Bn!1KIuo~ELzXd;U5
z<PGP|zJXdwnf$U2I5DUnz$d7*B+4?HZAk!mKT==_fb-Y}WQ-mY0LOSvjH!+&fga#~
zXHbKMdD>;}QHZfL9QMbPZ?b?QfTZ1{;fSKb3Q_ghdDiay{29Ly0(P0EmoMR#;%3V=
z96y?Y(w=Ul2S9H2fw3Eof`fIRaeHZ5NauuIa(h7WGwGBIX8lwdhhS*wh+ffQ8*OLM
zx{XTfmVV)FVB5qm{ZQ?0x0b8qd-z-Wa=jY>JQMZXcqpTCiMCb62o!vBM#D>#`c!FA
z(Kjs7kRDjyt=@#})szCPYoK;w6|ENv9<J<eN?`XI-nv~B-K<DfXqy-rz->Fl;3Vr1
z+J-(3mb|=5jrhF&CZS7;<IR!sE;f*(sWo#rV@Y3yp2A3&!eE!@#T9V7&LT@eUkl}u
zP}4{~M6<~$hE$kuwkaS=m$O`Rj61K29FuWoN~S(8Y-sN&#1AwiT4G);nv8brbQZ0s
z+jYW}=&%(`28D3`0dM^%BNlkVudA1S0G2lsU57Lm^*=mY#?wo*`&(|tQJ+<j(LJeq
zJPY{`-uYt{o+&`fTOcSE{aGQmZsZYFSULsz8`Z}^cN~>&tXc4=gn%%ZoXl2j`LTE_
z9Zy1tb;I%1b<ByeV|U!|93Fihv@=ld-1w7u?U$`Tz=>JXPj1bE=N2Em-){krWDGY*
z9I%E$6-YP%!l2xf_n=33JElLt{EQTLPDVIeSU12;r@Y~k`Yl9<Yp{ZsF7n;h-@u8P
z-im))b%X;}-FdL#qB|5?s1ff;q=VfsGVz#V(T?~1)!r_0yx?(<;rmJ)q#LYZ)h=7!
zFq|+drE4o;xMMX>^B0^9f=!2wU+@TkFm@4r@Leu>+J*B`gB<t?GI6vs$(RtHIsqXQ
z-)alxXISE1;X6+k{rzZy6KR<7I~5lMz8=97I<g%&o#D@_*=&~)8}ss3EoAh(UWAXX
zO16agKU#UF44xyYG_N{+{!7K6rcGkI=nt5ZHDn#(Wb0PEZ^~?~rYMGujz*Y{2o7+G
zlxx1*n>ImO4zE!GYWbozk^J6q2+NWe^yy-J&KIb}cz79Mz{G3%Uwn4`N*~?94li@e
Gi2nfU^82y?

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart
index daf8854e019fa2766a2430f1a75cdecfdaf7c2b2..db514a1d571b6477b41db49d958d450d60329c0f 100644
GIT binary patch
delta 54
zcmV-60LlO9G2$z*qXCoV0S=SP0iLtq1LXs=^#@7;vwsO>0kZ`Q4gs^S3`GP4ZeeX@
Mld%yYvtbd42XRysZ~y=R

delta 342
zcmaE){nBv5Vn&gYjLc%a+|=CsqRiA{*Sy4}oYa)ba~U^n7Gg<e<VTa(?8WqoSqV*0
z!PZs*O=z<kTLdGzsUUTEr8zkY_6i0HR_Iz3H|MaIF`|pws-T<vfJ1;$A~Q`v1FORo
zxFj{ubg0KF*eYOE?9U}9k4-Vik=9%aKv28+HK!K~dMHdT<dYXf*QmK!n{PWi0FP99
ARR910

diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart
index 3fc8c2e274bff3eff66c0a86b53e271869c39fc7..8060fa7cfcf0532d34b93034568898847b449ecf 100644
GIT binary patch
delta 42
zcmV+_0M-BNKIAyCGXk?s0;2)5X9W!cv!n-W0<%vF3<9%#48j7l+7HhKv+@;r2~n~S
AkN^Mx

delta 717
zcmZ`%&q^CX7{?W<4Yi`B*Yuar5(8=qUP@{zMZ62?p+$O_?0oBvoy>%p*_7bHLmwgZ
z)Jq?su$Ml7_y*p*2;O`NXEv$OsoldI_V@SuW^UfxzWcS4Jaf6Q+d~>^Bgh^oHjp%3
zOP-hSlkW>>old6)_!Ebcm?bFfsEZF?gOSlAGHwE!>q2tmY)lvsDNj;cs*o~xHP9C-
zCAgemN0f?8q|6DekO@h1bFRq-Yj~<GIDvsv#)>oPFlMqKY!p_g<I1I3LDF@b_Z8Zk
z$n{qmli92&$E3(GM||#YbzyvmRD0`TVI49iB?c`00;faq;LY%-Gu&iE+g=;y$G%op
za57+=&v*|s@Xh}-VwK?a9`;KM`wPyq#SXNVd-!|#{B6zSeP?`DdA0Z`X+}7Dy;xFS
zE=`i?5_#Z-iLi&)=*E&pxw~9X;=jF@QCRu0cAk6^88#kxCmfAgL>s5RbNEiIR!624
rxwJl`SIr-Hoc?%PUyaDqpHJ%LtCfqTe}LcXFV-S*t6g5Ne|vlfrn31(

diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart
index ecd145248f1feb1e2a907402837176e655905344..dd0db767fe6b08cc7f0c4ce10e479f6ab68e89e2 100644
GIT binary patch
delta 83
zcmX?R^Uz>JKjY-3i~^g_GhSfaEX$hDI9ZmJV>36$Cg#acxn(vR@`y4{uH)6&{E9b(
mkta1bF*C<j1x!tD<9{~!yFl?~Rv|-{$<<=!n}3UWvjYI$&K+a`

delta 648
zcmaE8aLi^yKci`4Sz<|Ik#l}devuBB0uYpBWEShC<|bz5An|ikbMuQbQ;S{m5|eUL
zQ#Mawyv-;RTAW(sh|q4YfMnKYUZ#soli#zm@uO?yWy)gQ{ETH2vlNnY1zTHW-IKdG
zw3N^^f|Q^MZC=Kq%xHvWOkQbDj)J{{fr1sXiHdp|iNzVt`6;QI3fhwoa~kQQnFTT)
zNh_M{=0}|Aj2=if*{UFMbQHkhWvc=SHgsp&s-T%RIh6YuH<JFz4m^^`j#ZCUu!Y0}
zvLZu%ITTG3c;qHq^Aw{8Fvu_sGz$?1qDf2!8j5bf<_SFhEH+3sTPdK$v|d_Key&$>
lex61akcci)M^daFtEn^jnt%fzs!fxh3Myb&#3dNc4gim+>4X3P

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 0000000000000000000000000000000000000000..673f5bfaf856489e712dbc89386dbf35688d8ffe
GIT binary patch
literal 3234
zcmbVOZExE)5dQ98aRG{2!BlzMr^1=M21_#Z#qp4)9|pq^7@4-%$)ZM5F^tszeRrfN
z%eAd!HDFsJ?~V67cf7%%HyFU>m;1?^Kd*1E?=J7Jui)nWr|S@|CU85shw<d<{mtKJ
zC`OX+=1iIR*XWnGJ$e<(LMomtq)HYd=R>H9O7nTbbH3t*3H`fRl~S3b8Z2M4jZVtS
zB#QrA35DtfTj6iX6#g5oG#Xd>xOyrJZCGIvF-M1DA-J;Y`lz#96kI2{ES4x{GFBvS
ze@@bjDc$R#I|Fh7vg8FTMTFmrUN6m=*6>Ah^_Sucu6U7heaCf)e)*M=@YDksU@Gpo
zsZ^nXLBZEBJ*_bVVM9)NgsP|I0<sL?lL96aI%1E^FcluzS@D%rf@|8_o@3+ymsZ#Z
z3r^SWj7fOY?08+f_f9@?r7)dn;X%rr5~7%q3gS4JJbdLTrpaVnokKhVm-dw90mT>k
zop0aV6Nt{_y?$*^V$IcOLoaotu(#`Dq<#N<5U(f<EV+S<>Ff_V=NO@N&`^~aT5yC=
zGqoXxA!#zMg|=dWOHt(2&*3Mx?j7!S4qSgWN)$^l(mez=JN?t}PMxp#mHQ8yL&dMW
zw^`qT#me8D(GcbvtPPQl(9Z`g3-sbxvJeH!;VUjd3<uIr+Y5URuo6?%cRu?w6mHS#
zJ0Ty&PRy@3L1h_lP13@aDEEVv0IZmUYZ4lS(eTLfifdRXxvH&B?GhHx0tNb1XVWn&
zXCV|-jvOUIu7$QW)k3?pMVr_zp{f*FtP|pQf=$DSG{OFo$vL6#7CP$jcN>k(dX0jM
z(nElO1vW<FJcj1i0rtSLB2mh;Ie;l}W$UV*x=BYg0fy{84$I+p)gDpmY_LR%7Ubt=
zu6vNO$8!kdy@qh0>;?O?ojL|fHn#S+<^o~SaG}oVCxJ&jT?Zq`QQ*c$>r+cnhwx+@
z&{D}Yd&sexVZn0E!>!M)%dFzM$_){}!_9Q&;w6~KIH5$LVVAKKj)cCI=6Y3{jkk`-
zVb=?4CvuIA+lub?J+=<_xC5_UO7Spt>L`6X)||zb%5CG+{pn~Co6jA$TAD8L9)LX<
zYAqw_fPqin5d4$1V(YcW2<h6eu_K4DV&ze!e9$gcDAn#l9R_`4YJ-BYbYfz`iydaC
z$_V>VJ(~=n86y(B$#Gzz!Nj%uJ$!LhZ9Kvez2Rl*&qYYNv;7~Wt;xu0v~YU{aZKLS
zDgMJZ)S#`t<1U5K0XwQT#fJ&NS89_sU((&%Yhf~6qKczY=QJ{JoC50Cy%;YXRHW&T
z?C$#IxWyb|TUQB=NR@<+a_FVijyBrtde-TWaO{Qjc?lKkKHcgh&oT{9xKDx8I7I|y
zlg<|5!lu}FWgj*ybR^pA8R)b{o&nrvyx~bRgCZq0P7Dl5P$7jLIN$}V(Jd7YgAbo4
nw=~q&UB%wI%>et6berL1^kfs`ClPli^_zRfPO%p~>{0&#SAZ+o

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..887293931c282acb441fe7979fba57b6b0f4b731
GIT binary patch
literal 4054
zcmeHK-*4MC5PtVxaVd(%!BnT)Q{m1@izXe~HA&H?4}%d1j6}z5Wzr?77)F}^eczFy
zEjLz^4cOZrqR7<!;(g!Uk^B3-{e61%`TXqZAIGQ1A77mxAJLn)?~gM&I-}FGb2>RY
zdi&<D9f+~y%TicB`fc#*^B(_HSK3&aPmIkcs**#hb?s!C%Sz6q_F4L^W^-fxRt-@t
z#M0&S+UHh&tCfYi7Bf8O!s5S|*1@=S+tmxBofq2Ys)R!|QPKu=x7Astv~+o8^cBSX
zROtNqv%HuJ>v}!7^JFGu=2DBf8sP6muUAyUIr{9R{b1!pTB!@^KF>?x<&igZ(<9nP
zB0frATkS}YmJ1qp$zPBP;TCcL#V)BnTP5;S<p$zfcww{k!c5MLRnqa&?H(cw0mLVL
zsHAX`wDG{=9Qri3*pKx~a#K@RB~8V(q)RC^g!EHgSCzP|<PLpO*ce<&a&uWAkA?8c
zXku6!<E4>~O=Ay5RhJT*E#*Yim8WY_)sp(PQ~D|xdDMIvK3YYvm(kQL<h8U}5V?|C
zTBNaT%sW$Cgish*p4HCNL{t@BisCCF(TJOEBfm|^<d^f1MNa=~umA+GB+4?1Z3zHz
zcWf{?fIdDWWt=^R2Zr%{6#$5cA5w(l|Even|A$#WVU{mb))VJL-d`F}$O0{Zb7@S)
z1q&V-OQTW$?DC5&P{ts#-J{U~#kNq0s?XSF>C5-;@r{6YR~gciCvZVguqj0NXgIL>
zHATqZ)BBxhmG*QcJ-F<qubJkfgWzD*NV$J%i3a0@U2^|JOPflkToCI=^05mp0y?B;
zG;E^T86=~7^h=aZVf-MUmRlNk0{!G|C}a~~`XQ6MU3nVExJHlT|7?|j(c|RoVmJ6W
z5%b@p$&AV+y1Xh5P<!v3(eM$w-b86p(HAVyi0(+=+`b6AQxgh+CRlc2PLdG{2F7+b
zhO~PDTdj5)ZQPQq&@7`{V6a7p^V|q;x=kCSF2_k=?K-Xe+<B4;dOy0iVakL!*~)Fx
z{Qvp%A)!l(L)nq?E;evS6KiHL%q?{sdO8Ny1OmG}*~WnHB8%J$1-4MGX_U=DV2EbV
zLoyFj+13TB(&b#QIfk@XMUHtsH6`;oE_4>(VTdI#qhU&nF(J$Zh*Jj@jKI=5VPdri
z6hsDvaQ+UJ`f+%2g*SQaB6vSw&w`Oo!;PUZ;G~5>nY1m=?Zv6hdI{AbowOR;9nB=R
zd$H0*kZj;KJexQZEdu^c9^Wf$v1<bdRN?cR6lY2($D7ejXrfovEchgVLc!d1GP_L~
zjrp|F@t{XZb~+hf#8?>`HqE`2!$PMFlpQE{YW(qR?w83n&_dttH!jSAM>MOhzb8B+
z)}v0otaFKtG)d#ct$A{%Qu%Y-)&R$b$flNNOnFNE1^|tgH@v-mO5KpsTd>kbr``BE
z^G7o=--=PnMobf=+MTh}T)R~!I1tBA(Fk&*h*nq8pgXL~Jsr_=oRV=F;6r~LRvXA-
z)h-jZ1O9~rY}ysW5O?-)jDx{JkZ&P&57tQBI4XXXOP&ZpKXR6X_zB=pZ8cm5iW_3c
zmWd~hh4ND@ah>rcq6?L$Q5@P$&*h^h&e44LfYo(hJZX(eW?6UH9k3#cY~{{F>l-%O
zth&eLzv0#cY4Z>YHvPEOJMLu|_O}Us7eWJuaFxmy(Xv%}uM0g}u`iNmqf5YLi!}SV
zkI5B3(p8s0MTet_0Q`LCxs*IY9o%xc2zvMF?3C{!O_vXI6-_JNlmBGh)gkHN268wa
Hq3`?+X#Fu<

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart
index 1b54d4a383320d8e4f8eda1f0c36939046ee8604..2d665fc7847b8bd3ca24e313ab9c5b34b335d9e4 100644
GIT binary patch
delta 79
zcmeyQ*Q>mtjgd7qH!(A3^KQm2#?6OW@)<X;WdF!G`4FefWInF?$w#?#CL3~h^MDoE
es(`7<EId_{IeCjWi}QIiO<pcwzFA$+n+*V^1RHVy

delta 703
zcmeBG{-n2|jgcp@EU_f9$T>eJzes0t4Wkr)Zfb6RQD$nfYhGefPHM{LZpJP~nb6|Y
zB1eQudj%wIn<ubjF>aP+PG=NN%FoZSS3oxbEZ@WOl2Hmtmx8S=vLTbx*|e0<G=h|%
z2~BQh>or0%C9gCmN5NjfK*0*xKt;Wb#NrI+{FGEp1?|aP>_)n1MuAL6(uyWKS(AO9
z2a-j$Do7k1h1A@{%p6-4Fr|a;NLv+j!!EN|xghCRP=r{msZh<O00vpb`FYVr>PTwT
zV-;)_N-{Ew^^gQ7*K^2mBl870<tF=b6bGPLh|>ZzbqGt)BtRCSr-aEs%Qhe2@Mf|>
za)FfsTGG%<E6UIH0(wUSo1ZjwCNpt6*rM5HrI4Fg5SExzn(ChxQdy8{14>7BU;}g%
Oiqz3mZ#Lr&X9ED|KKVER

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 = "<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<UserPreferencesResponseDto> {
+    return this.service.getPreferences(auth, id);
+  }
+
+  @Put(':id/preferences')
+  @Authenticated()
+  updateUserPreferencesAdmin(
+    @Auth() auth: AuthDto,
+    @Param() { id }: UUIDParamDto,
+    @Body() dto: UserPreferencesUpdateDto,
+  ): Promise<UserPreferencesResponseDto> {
+    return this.service.updatePreferences(auth, id, dto);
+  }
+
   @Post(':id/restore')
   @Authenticated({ admin: true })
   restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
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<UserPreferencesResponseDto> {
+    return this.service.updateMyPreferences(auth, dto);
+  }
+
   @Get(':id')
   @Authenticated()
   getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
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<UserAdminResponseDto> {
-    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<UserPreferencesResponseDto> {
+    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<UserAdminResponseDto> {
-    // 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<UserResponseDto> {
     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 @@
 <script lang="ts">
   import Button from '$lib/components/elements/buttons/button.svelte';
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
+  import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
   import { AppRoute } from '$lib/constants';
-  import { user } from '$lib/stores/user.store';
+  import { preferences, user } from '$lib/stores/user.store';
   import { handleError } from '$lib/utils/handle-error';
-  import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk';
+  import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk';
   import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
   import { createEventDispatcher } from 'svelte';
   import { fade } from 'svelte/transition';
-  import { notificationController, NotificationType } from '../notification/notification';
+  import { NotificationType, notificationController } from '../notification/notification';
   import UserAvatar from '../user-avatar.svelte';
   import AvatarSelector from './avatar-selector.svelte';
-  import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
-  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 
   let isShowSelectAvatar = false;
 
@@ -27,14 +27,7 @@
         await deleteProfileImage();
       }
 
-      $user = await updateMyUser({
-        userUpdateMeDto: {
-          email: $user.email,
-          name: $user.name,
-          avatarColor: color,
-        },
-      });
-
+      $preferences = await updateMyPreferences({ userPreferencesUpdateDto: { avatar: { color } } });
       isShowSelectAvatar = false;
 
       notificationController.show({
diff --git a/web/src/lib/components/user-settings-page/memories-settings.svelte b/web/src/lib/components/user-settings-page/memories-settings.svelte
index dcd7033aa4..4d103ade13 100644
--- a/web/src/lib/components/user-settings-page/memories-settings.svelte
+++ b/web/src/lib/components/user-settings-page/memories-settings.svelte
@@ -3,20 +3,20 @@
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
-  import { updateMyUser, type UserAdminResponseDto } from '@immich/sdk';
+  import { updateMyPreferences } from '@immich/sdk';
   import { fade } from 'svelte/transition';
   import { handleError } from '../../utils/handle-error';
 
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
+  import { preferences } from '$lib/stores/user.store';
   import Button from '../elements/buttons/button.svelte';
 
-  export let user: UserAdminResponseDto;
+  let memoriesEnabled = $preferences?.memories?.enabled ?? false;
 
   const handleSave = async () => {
     try {
-      const data = await updateMyUser({ userUpdateMeDto: { memoriesEnabled: user.memoriesEnabled } });
-
-      Object.assign(user, data);
+      const data = await updateMyPreferences({ userPreferencesUpdateDto: { memories: { enabled: memoriesEnabled } } });
+      $preferences.memories.enabled = data.memories.enabled;
 
       notificationController.show({ message: 'Saved settings', type: NotificationType.Info });
     } catch (error) {
@@ -34,7 +34,7 @@
             id="time-based-memories"
             title="Time-based memories"
             subtitle="Photos from previous years"
-            bind:checked={user.memoriesEnabled}
+            bind:checked={memoriesEnabled}
           />
         </div>
         <div class="flex justify-end">
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte
index d239886ed9..f88ee58872 100644
--- a/web/src/lib/components/user-settings-page/user-settings-list.svelte
+++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte
@@ -42,7 +42,7 @@
   </SettingAccordion>
 
   <SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories">
-    <MemoriesSettings user={$user} />
+    <MemoriesSettings />
   </SettingAccordion>
 
   {#if $featureFlags.loaded && $featureFlags.oauth}
diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts
index 506782f48a..8d422d3704 100644
--- a/web/src/lib/stores/user.store.ts
+++ b/web/src/lib/stores/user.store.ts
@@ -1,7 +1,8 @@
-import type { UserAdminResponseDto } from '@immich/sdk';
+import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
 import { writable } from 'svelte/store';
 
 export const user = writable<UserAdminResponseDto>();
+export const preferences = writable<UserPreferencesResponseDto>();
 
 /**
  * Reset the store to its initial undefined value. Make sure to
@@ -9,4 +10,5 @@ export const user = writable<UserAdminResponseDto>();
  */
 export const resetSavedUser = () => {
   user.set(undefined as unknown as UserAdminResponseDto);
+  preferences.set(undefined as unknown as UserPreferencesResponseDto);
 };
diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts
index 91beb0293f..df5c9bc46a 100644
--- a/web/src/lib/utils/auth.ts
+++ b/web/src/lib/utils/auth.ts
@@ -1,7 +1,7 @@
 import { browser } from '$app/environment';
 import { serverInfo } from '$lib/stores/server-info.store';
-import { user } from '$lib/stores/user.store';
-import { getMyUser, getStorage } from '@immich/sdk';
+import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
+import { getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
 import { redirect } from '@sveltejs/kit';
 import { get } from 'svelte/store';
 import { AppRoute } from '../constants';
@@ -13,12 +13,14 @@ export interface AuthOptions {
 
 export const loadUser = async () => {
   try {
-    let loaded = get(user);
-    if (!loaded && hasAuthCookie()) {
-      loaded = await getMyUser();
-      user.set(loaded);
+    let user = get(user$);
+    let preferences = get(preferences$);
+    if ((!user || !preferences) && hasAuthCookie()) {
+      [user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]);
+      user$.set(user);
+      preferences$.set(preferences);
     }
-    return loaded;
+    return user;
   } catch {
     return null;
   }
@@ -57,7 +59,7 @@ export const authenticate = async (options?: AuthOptions) => {
 };
 
 export const requestServerInfo = async () => {
-  if (get(user)) {
+  if (get(user$)) {
     const data = await getStorage();
     serverInfo.set(data);
   }
diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
index f711b081d6..d510edd25d 100644
--- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
@@ -22,7 +22,7 @@
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { mdiDotsVertical, mdiPlus } from '@mdi/js';
-  import { user } from '$lib/stores/user.store';
+  import { preferences, user } from '$lib/stores/user.store';
 
   let { isViewing: showAssetViewer } = assetViewingStore;
   let handleEscapeKey = false;
@@ -98,7 +98,7 @@
     on:escape={handleEscape}
     withStacked
   >
-    {#if $user.memoriesEnabled}
+    {#if $preferences.memories.enabled}
       <MemoryLane />
     {/if}
     <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" onClick={() => openFileUploadDialog()} slot="empty" />