0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-21 00:52:43 -05:00

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>
This commit is contained in:
Jason Rasmussen 2024-05-27 22:16:53 -04:00 committed by GitHub
parent 1f9158c545
commit 0fc6d69824
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1392 additions and 327 deletions

View file

@ -77,7 +77,6 @@ immich-admin list-users
deletedAt: null,
updatedAt: 2023-09-21T15:42:28.129Z,
oauthId: '',
memoriesEnabled: true
}
]
```

View file

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

View file

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

View file

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

View file

@ -68,7 +68,6 @@ export const signupResponseDto = {
updatedAt: expect.any(String),
deletedAt: null,
oauthId: '',
memoriesEnabled: true,
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
status: 'active',

View file

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

View file

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

View file

@ -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 (_) {}

View file

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

View file

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

View file

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

View file

@ -205,6 +205,47 @@ class UserApi {
return null;
}
/// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response].
Future<Response> getMyPreferencesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/users/me/preferences';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<UserPreferencesResponseDto?> 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<Response> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<UserPreferencesResponseDto?> 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<Response> updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/users/me/preferences';
// ignore: prefer_final_locals
Object? postBody = userPreferencesUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required):
Future<UserPreferencesResponseDto?> 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<Response> 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 = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required):
Future<UserPreferencesResponseDto?> 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;
}
}

View file

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

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return AvatarResponse(
color: UserAvatarColor.fromJson(json[r'color'])!,
);
}
return null;
}
static List<AvatarResponse> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AvatarResponse>[];
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<String, AvatarResponse> mapFromJson(dynamic json) {
final map = <String, AvatarResponse>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<AvatarResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AvatarResponse>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'color',
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return AvatarUpdate(
color: UserAvatarColor.fromJson(json[r'color']),
);
}
return null;
}
static List<AvatarUpdate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AvatarUpdate>[];
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<String, AvatarUpdate> mapFromJson(dynamic json) {
final map = <String, AvatarUpdate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<AvatarUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AvatarUpdate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return MemoryResponse(
enabled: mapValueOfType<bool>(json, r'enabled')!,
);
}
return null;
}
static List<MemoryResponse> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MemoryResponse>[];
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<String, MemoryResponse> mapFromJson(dynamic json) {
final map = <String, MemoryResponse>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<MemoryResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MemoryResponse>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'enabled',
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return MemoryUpdate(
enabled: mapValueOfType<bool>(json, r'enabled'),
);
}
return null;
}
static List<MemoryUpdate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MemoryUpdate>[];
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<String, MemoryUpdate> mapFromJson(dynamic json) {
final map = <String, MemoryUpdate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<MemoryUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MemoryUpdate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String>(json, r'email')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name')!,
notify: mapValueOfType<bool>(json, r'notify'),
password: mapValueOfType<String>(json, r'password')!,

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -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<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return UserAdminUpdateDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'),
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'),
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return UserPreferencesResponseDto(
avatar: AvatarResponse.fromJson(json[r'avatar'])!,
memories: MemoryResponse.fromJson(json[r'memories'])!,
);
}
return null;
}
static List<UserPreferencesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserPreferencesResponseDto>[];
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<String, UserPreferencesResponseDto> mapFromJson(dynamic json) {
final map = <String, UserPreferencesResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<UserPreferencesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserPreferencesResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'avatar',
'memories',
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return UserPreferencesUpdateDto(
avatar: AvatarUpdate.fromJson(json[r'avatar']),
memories: MemoryUpdate.fromJson(json[r'memories']),
);
}
return null;
}
static List<UserPreferencesUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserPreferencesUpdateDto>[];
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<String, UserPreferencesUpdateDto> mapFromJson(dynamic json) {
final map = <String, UserPreferencesUpdateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<UserPreferencesUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserPreferencesUpdateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
};
}

View file

@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return UserUpdateMeDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'),
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'),
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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