mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 13:57:39 +09:00 
			
		
		
		
	feat: pending sync reset flag (#19861)
This commit is contained in:
		| @@ -116,6 +116,7 @@ export const deviceDto = { | |||||||
|     createdAt: expect.any(String), |     createdAt: expect.any(String), | ||||||
|     updatedAt: expect.any(String), |     updatedAt: expect.any(String), | ||||||
|     current: true, |     current: true, | ||||||
|  |     isPendingSyncReset: false, | ||||||
|     deviceOS: '', |     deviceOS: '', | ||||||
|     deviceType: '', |     deviceType: '', | ||||||
|   }, |   }, | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -208,6 +208,7 @@ Class | Method | HTTP request | Description | |||||||
| *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |  | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |  | ||||||
| *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |  | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |  | ||||||
| *SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |  | *SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |  | ||||||
|  | *SessionsApi* | [**updateSession**](doc//SessionsApi.md#updatesession) | **PUT** /sessions/{id} |  | ||||||
| *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |  | *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |  | ||||||
| *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |  | *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |  | ||||||
| *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |  | *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |  | ||||||
| @@ -449,6 +450,7 @@ Class | Method | HTTP request | Description | |||||||
|  - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) |  - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) | ||||||
|  - [SessionResponseDto](doc//SessionResponseDto.md) |  - [SessionResponseDto](doc//SessionResponseDto.md) | ||||||
|  - [SessionUnlockDto](doc//SessionUnlockDto.md) |  - [SessionUnlockDto](doc//SessionUnlockDto.md) | ||||||
|  |  - [SessionUpdateDto](doc//SessionUpdateDto.md) | ||||||
|  - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) |  - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) | ||||||
|  - [SharedLinkEditDto](doc//SharedLinkEditDto.md) |  - [SharedLinkEditDto](doc//SharedLinkEditDto.md) | ||||||
|  - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) |  - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -232,6 +232,7 @@ part 'model/session_create_dto.dart'; | |||||||
| part 'model/session_create_response_dto.dart'; | part 'model/session_create_response_dto.dart'; | ||||||
| part 'model/session_response_dto.dart'; | part 'model/session_response_dto.dart'; | ||||||
| part 'model/session_unlock_dto.dart'; | part 'model/session_unlock_dto.dart'; | ||||||
|  | part 'model/session_update_dto.dart'; | ||||||
| part 'model/shared_link_create_dto.dart'; | part 'model/shared_link_create_dto.dart'; | ||||||
| part 'model/shared_link_edit_dto.dart'; | part 'model/shared_link_edit_dto.dart'; | ||||||
| part 'model/shared_link_response_dto.dart'; | part 'model/shared_link_response_dto.dart'; | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								mobile/openapi/lib/api/sessions_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								mobile/openapi/lib/api/sessions_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -219,4 +219,56 @@ class SessionsApi { | |||||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); |       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /// Performs an HTTP 'PUT /sessions/{id}' operation and returns the [Response]. | ||||||
|  |   /// Parameters: | ||||||
|  |   /// | ||||||
|  |   /// * [String] id (required): | ||||||
|  |   /// | ||||||
|  |   /// * [SessionUpdateDto] sessionUpdateDto (required): | ||||||
|  |   Future<Response> updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async { | ||||||
|  |     // ignore: prefer_const_declarations | ||||||
|  |     final apiPath = r'/sessions/{id}' | ||||||
|  |       .replaceAll('{id}', id); | ||||||
|  | 
 | ||||||
|  |     // ignore: prefer_final_locals | ||||||
|  |     Object? postBody = sessionUpdateDto; | ||||||
|  | 
 | ||||||
|  |     final queryParams = <QueryParam>[]; | ||||||
|  |     final headerParams = <String, String>{}; | ||||||
|  |     final formParams = <String, String>{}; | ||||||
|  | 
 | ||||||
|  |     const contentTypes = <String>['application/json']; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     return apiClient.invokeAPI( | ||||||
|  |       apiPath, | ||||||
|  |       'PUT', | ||||||
|  |       queryParams, | ||||||
|  |       postBody, | ||||||
|  |       headerParams, | ||||||
|  |       formParams, | ||||||
|  |       contentTypes.isEmpty ? null : contentTypes.first, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Parameters: | ||||||
|  |   /// | ||||||
|  |   /// * [String] id (required): | ||||||
|  |   /// | ||||||
|  |   /// * [SessionUpdateDto] sessionUpdateDto (required): | ||||||
|  |   Future<SessionResponseDto?> updateSession(String id, SessionUpdateDto sessionUpdateDto,) async { | ||||||
|  |     final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,); | ||||||
|  |     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), 'SessionResponseDto',) as SessionResponseDto; | ||||||
|  |      | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -520,6 +520,8 @@ class ApiClient { | |||||||
|           return SessionResponseDto.fromJson(value); |           return SessionResponseDto.fromJson(value); | ||||||
|         case 'SessionUnlockDto': |         case 'SessionUnlockDto': | ||||||
|           return SessionUnlockDto.fromJson(value); |           return SessionUnlockDto.fromJson(value); | ||||||
|  |         case 'SessionUpdateDto': | ||||||
|  |           return SessionUpdateDto.fromJson(value); | ||||||
|         case 'SharedLinkCreateDto': |         case 'SharedLinkCreateDto': | ||||||
|           return SharedLinkCreateDto.fromJson(value); |           return SharedLinkCreateDto.fromJson(value); | ||||||
|         case 'SharedLinkEditDto': |         case 'SharedLinkEditDto': | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ class SessionCreateResponseDto { | |||||||
|     required this.deviceType, |     required this.deviceType, | ||||||
|     this.expiresAt, |     this.expiresAt, | ||||||
|     required this.id, |     required this.id, | ||||||
|  |     required this.isPendingSyncReset, | ||||||
|     required this.token, |     required this.token, | ||||||
|     required this.updatedAt, |     required this.updatedAt, | ||||||
|   }); |   }); | ||||||
| @@ -41,6 +42,8 @@ class SessionCreateResponseDto { | |||||||
| 
 | 
 | ||||||
|   String id; |   String id; | ||||||
| 
 | 
 | ||||||
|  |   bool isPendingSyncReset; | ||||||
|  | 
 | ||||||
|   String token; |   String token; | ||||||
| 
 | 
 | ||||||
|   String updatedAt; |   String updatedAt; | ||||||
| @@ -53,6 +56,7 @@ class SessionCreateResponseDto { | |||||||
|     other.deviceType == deviceType && |     other.deviceType == deviceType && | ||||||
|     other.expiresAt == expiresAt && |     other.expiresAt == expiresAt && | ||||||
|     other.id == id && |     other.id == id && | ||||||
|  |     other.isPendingSyncReset == isPendingSyncReset && | ||||||
|     other.token == token && |     other.token == token && | ||||||
|     other.updatedAt == updatedAt; |     other.updatedAt == updatedAt; | ||||||
| 
 | 
 | ||||||
| @@ -65,11 +69,12 @@ class SessionCreateResponseDto { | |||||||
|     (deviceType.hashCode) + |     (deviceType.hashCode) + | ||||||
|     (expiresAt == null ? 0 : expiresAt!.hashCode) + |     (expiresAt == null ? 0 : expiresAt!.hashCode) + | ||||||
|     (id.hashCode) + |     (id.hashCode) + | ||||||
|  |     (isPendingSyncReset.hashCode) + | ||||||
|     (token.hashCode) + |     (token.hashCode) + | ||||||
|     (updatedAt.hashCode); |     (updatedAt.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]'; |   String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -83,6 +88,7 @@ class SessionCreateResponseDto { | |||||||
|     //  json[r'expiresAt'] = null; |     //  json[r'expiresAt'] = null; | ||||||
|     } |     } | ||||||
|       json[r'id'] = this.id; |       json[r'id'] = this.id; | ||||||
|  |       json[r'isPendingSyncReset'] = this.isPendingSyncReset; | ||||||
|       json[r'token'] = this.token; |       json[r'token'] = this.token; | ||||||
|       json[r'updatedAt'] = this.updatedAt; |       json[r'updatedAt'] = this.updatedAt; | ||||||
|     return json; |     return json; | ||||||
| @@ -103,6 +109,7 @@ class SessionCreateResponseDto { | |||||||
|         deviceType: mapValueOfType<String>(json, r'deviceType')!, |         deviceType: mapValueOfType<String>(json, r'deviceType')!, | ||||||
|         expiresAt: mapValueOfType<String>(json, r'expiresAt'), |         expiresAt: mapValueOfType<String>(json, r'expiresAt'), | ||||||
|         id: mapValueOfType<String>(json, r'id')!, |         id: mapValueOfType<String>(json, r'id')!, | ||||||
|  |         isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!, | ||||||
|         token: mapValueOfType<String>(json, r'token')!, |         token: mapValueOfType<String>(json, r'token')!, | ||||||
|         updatedAt: mapValueOfType<String>(json, r'updatedAt')!, |         updatedAt: mapValueOfType<String>(json, r'updatedAt')!, | ||||||
|       ); |       ); | ||||||
| @@ -157,6 +164,7 @@ class SessionCreateResponseDto { | |||||||
|     'deviceOS', |     'deviceOS', | ||||||
|     'deviceType', |     'deviceType', | ||||||
|     'id', |     'id', | ||||||
|  |     'isPendingSyncReset', | ||||||
|     'token', |     'token', | ||||||
|     'updatedAt', |     'updatedAt', | ||||||
|   }; |   }; | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								mobile/openapi/lib/model/session_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/lib/model/session_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -19,6 +19,7 @@ class SessionResponseDto { | |||||||
|     required this.deviceType, |     required this.deviceType, | ||||||
|     this.expiresAt, |     this.expiresAt, | ||||||
|     required this.id, |     required this.id, | ||||||
|  |     required this.isPendingSyncReset, | ||||||
|     required this.updatedAt, |     required this.updatedAt, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @@ -40,6 +41,8 @@ class SessionResponseDto { | |||||||
| 
 | 
 | ||||||
|   String id; |   String id; | ||||||
| 
 | 
 | ||||||
|  |   bool isPendingSyncReset; | ||||||
|  | 
 | ||||||
|   String updatedAt; |   String updatedAt; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
| @@ -50,6 +53,7 @@ class SessionResponseDto { | |||||||
|     other.deviceType == deviceType && |     other.deviceType == deviceType && | ||||||
|     other.expiresAt == expiresAt && |     other.expiresAt == expiresAt && | ||||||
|     other.id == id && |     other.id == id && | ||||||
|  |     other.isPendingSyncReset == isPendingSyncReset && | ||||||
|     other.updatedAt == updatedAt; |     other.updatedAt == updatedAt; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
| @@ -61,10 +65,11 @@ class SessionResponseDto { | |||||||
|     (deviceType.hashCode) + |     (deviceType.hashCode) + | ||||||
|     (expiresAt == null ? 0 : expiresAt!.hashCode) + |     (expiresAt == null ? 0 : expiresAt!.hashCode) + | ||||||
|     (id.hashCode) + |     (id.hashCode) + | ||||||
|  |     (isPendingSyncReset.hashCode) + | ||||||
|     (updatedAt.hashCode); |     (updatedAt.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]'; |   String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -78,6 +83,7 @@ class SessionResponseDto { | |||||||
|     //  json[r'expiresAt'] = null; |     //  json[r'expiresAt'] = null; | ||||||
|     } |     } | ||||||
|       json[r'id'] = this.id; |       json[r'id'] = this.id; | ||||||
|  |       json[r'isPendingSyncReset'] = this.isPendingSyncReset; | ||||||
|       json[r'updatedAt'] = this.updatedAt; |       json[r'updatedAt'] = this.updatedAt; | ||||||
|     return json; |     return json; | ||||||
|   } |   } | ||||||
| @@ -97,6 +103,7 @@ class SessionResponseDto { | |||||||
|         deviceType: mapValueOfType<String>(json, r'deviceType')!, |         deviceType: mapValueOfType<String>(json, r'deviceType')!, | ||||||
|         expiresAt: mapValueOfType<String>(json, r'expiresAt'), |         expiresAt: mapValueOfType<String>(json, r'expiresAt'), | ||||||
|         id: mapValueOfType<String>(json, r'id')!, |         id: mapValueOfType<String>(json, r'id')!, | ||||||
|  |         isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!, | ||||||
|         updatedAt: mapValueOfType<String>(json, r'updatedAt')!, |         updatedAt: mapValueOfType<String>(json, r'updatedAt')!, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -150,6 +157,7 @@ class SessionResponseDto { | |||||||
|     'deviceOS', |     'deviceOS', | ||||||
|     'deviceType', |     'deviceType', | ||||||
|     'id', |     'id', | ||||||
|  |     'isPendingSyncReset', | ||||||
|     'updatedAt', |     'updatedAt', | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								mobile/openapi/lib/model/session_update_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								mobile/openapi/lib/model/session_update_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | // | ||||||
|  | // 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 SessionUpdateDto { | ||||||
|  |   /// Returns a new [SessionUpdateDto] instance. | ||||||
|  |   SessionUpdateDto({ | ||||||
|  |     this.isPendingSyncReset, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   /// | ||||||
|  |   /// 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? isPendingSyncReset; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) => identical(this, other) || other is SessionUpdateDto && | ||||||
|  |     other.isPendingSyncReset == isPendingSyncReset; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int get hashCode => | ||||||
|  |     // ignore: unnecessary_parenthesis | ||||||
|  |     (isPendingSyncReset == null ? 0 : isPendingSyncReset!.hashCode); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   String toString() => 'SessionUpdateDto[isPendingSyncReset=$isPendingSyncReset]'; | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |     final json = <String, dynamic>{}; | ||||||
|  |     if (this.isPendingSyncReset != null) { | ||||||
|  |       json[r'isPendingSyncReset'] = this.isPendingSyncReset; | ||||||
|  |     } else { | ||||||
|  |     //  json[r'isPendingSyncReset'] = null; | ||||||
|  |     } | ||||||
|  |     return json; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Returns a new [SessionUpdateDto] instance and imports its values from | ||||||
|  |   /// [value] if it's a [Map], null otherwise. | ||||||
|  |   // ignore: prefer_constructors_over_static_methods | ||||||
|  |   static SessionUpdateDto? fromJson(dynamic value) { | ||||||
|  |     upgradeDto(value, "SessionUpdateDto"); | ||||||
|  |     if (value is Map) { | ||||||
|  |       final json = value.cast<String, dynamic>(); | ||||||
|  | 
 | ||||||
|  |       return SessionUpdateDto( | ||||||
|  |         isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset'), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static List<SessionUpdateDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final result = <SessionUpdateDto>[]; | ||||||
|  |     if (json is List && json.isNotEmpty) { | ||||||
|  |       for (final row in json) { | ||||||
|  |         final value = SessionUpdateDto.fromJson(row); | ||||||
|  |         if (value != null) { | ||||||
|  |           result.add(value); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return result.toList(growable: growable); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static Map<String, SessionUpdateDto> mapFromJson(dynamic json) { | ||||||
|  |     final map = <String, SessionUpdateDto>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         final value = SessionUpdateDto.fromJson(entry.value); | ||||||
|  |         if (value != null) { | ||||||
|  |           map[entry.key] = value; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // maps a json object with a list of SessionUpdateDto-objects as value to a dart map | ||||||
|  |   static Map<String, List<SessionUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final map = <String, List<SessionUpdateDto>>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       // ignore: parameter_assignments | ||||||
|  |       json = json.cast<String, dynamic>(); | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         map[entry.key] = SessionUpdateDto.listFromJson(entry.value, growable: growable,); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// The list of required keys that must be present in a JSON. | ||||||
|  |   static const requiredKeys = <String>{ | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/model/sync_entity_type.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/model/sync_entity_type.dart
									
									
									
										generated
									
									
									
								
							| @@ -59,6 +59,7 @@ class SyncEntityType { | |||||||
|   static const personV1 = SyncEntityType._(r'PersonV1'); |   static const personV1 = SyncEntityType._(r'PersonV1'); | ||||||
|   static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); |   static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); | ||||||
|   static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); |   static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); | ||||||
|  |   static const syncResetV1 = SyncEntityType._(r'SyncResetV1'); | ||||||
| 
 | 
 | ||||||
|   /// List of all possible values in this [enum][SyncEntityType]. |   /// List of all possible values in this [enum][SyncEntityType]. | ||||||
|   static const values = <SyncEntityType>[ |   static const values = <SyncEntityType>[ | ||||||
| @@ -98,6 +99,7 @@ class SyncEntityType { | |||||||
|     personV1, |     personV1, | ||||||
|     personDeleteV1, |     personDeleteV1, | ||||||
|     syncAckV1, |     syncAckV1, | ||||||
|  |     syncResetV1, | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); |   static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); | ||||||
| @@ -172,6 +174,7 @@ class SyncEntityTypeTypeTransformer { | |||||||
|         case r'PersonV1': return SyncEntityType.personV1; |         case r'PersonV1': return SyncEntityType.personV1; | ||||||
|         case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; |         case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; | ||||||
|         case r'SyncAckV1': return SyncEntityType.syncAckV1; |         case r'SyncAckV1': return SyncEntityType.syncAckV1; | ||||||
|  |         case r'SyncResetV1': return SyncEntityType.syncResetV1; | ||||||
|         default: |         default: | ||||||
|           if (!allowNull) { |           if (!allowNull) { | ||||||
|             throw ArgumentError('Unknown enum value to decode: $data'); |             throw ArgumentError('Unknown enum value to decode: $data'); | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								mobile/openapi/lib/model/sync_stream_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/lib/model/sync_stream_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -13,25 +13,41 @@ part of openapi.api; | |||||||
| class SyncStreamDto { | class SyncStreamDto { | ||||||
|   /// Returns a new [SyncStreamDto] instance. |   /// Returns a new [SyncStreamDto] instance. | ||||||
|   SyncStreamDto({ |   SyncStreamDto({ | ||||||
|  |     this.reset, | ||||||
|     this.types = const [], |     this.types = const [], | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   /// | ||||||
|  |   /// 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? reset; | ||||||
|  | 
 | ||||||
|   List<SyncRequestType> types; |   List<SyncRequestType> types; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto && |   bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto && | ||||||
|  |     other.reset == reset && | ||||||
|     _deepEquality.equals(other.types, types); |     _deepEquality.equals(other.types, types); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   int get hashCode => |   int get hashCode => | ||||||
|     // ignore: unnecessary_parenthesis |     // ignore: unnecessary_parenthesis | ||||||
|  |     (reset == null ? 0 : reset!.hashCode) + | ||||||
|     (types.hashCode); |     (types.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SyncStreamDto[types=$types]'; |   String toString() => 'SyncStreamDto[reset=$reset, types=$types]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
|  |     if (this.reset != null) { | ||||||
|  |       json[r'reset'] = this.reset; | ||||||
|  |     } else { | ||||||
|  |     //  json[r'reset'] = null; | ||||||
|  |     } | ||||||
|       json[r'types'] = this.types; |       json[r'types'] = this.types; | ||||||
|     return json; |     return json; | ||||||
|   } |   } | ||||||
| @@ -45,6 +61,7 @@ class SyncStreamDto { | |||||||
|       final json = value.cast<String, dynamic>(); |       final json = value.cast<String, dynamic>(); | ||||||
| 
 | 
 | ||||||
|       return SyncStreamDto( |       return SyncStreamDto( | ||||||
|  |         reset: mapValueOfType<bool>(json, r'reset'), | ||||||
|         types: SyncRequestType.listFromJson(json[r'types']), |         types: SyncRequestType.listFromJson(json[r'types']), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -6024,6 +6024,56 @@ | |||||||
|         "tags": [ |         "tags": [ | ||||||
|           "Sessions" |           "Sessions" | ||||||
|         ] |         ] | ||||||
|  |       }, | ||||||
|  |       "put": { | ||||||
|  |         "operationId": "updateSession", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "name": "id", | ||||||
|  |             "required": true, | ||||||
|  |             "in": "path", | ||||||
|  |             "schema": { | ||||||
|  |               "format": "uuid", | ||||||
|  |               "type": "string" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "requestBody": { | ||||||
|  |           "content": { | ||||||
|  |             "application/json": { | ||||||
|  |               "schema": { | ||||||
|  |                 "$ref": "#/components/schemas/SessionUpdateDto" | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           "required": true | ||||||
|  |         }, | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "content": { | ||||||
|  |               "application/json": { | ||||||
|  |                 "schema": { | ||||||
|  |                   "$ref": "#/components/schemas/SessionResponseDto" | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             "description": "" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "security": [ | ||||||
|  |           { | ||||||
|  |             "bearer": [] | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "cookie": [] | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "api_key": [] | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "Sessions" | ||||||
|  |         ] | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "/sessions/{id}/lock": { |     "/sessions/{id}/lock": { | ||||||
| @@ -12790,6 +12840,9 @@ | |||||||
|           "id": { |           "id": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "isPendingSyncReset": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "token": { |           "token": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
| @@ -12803,6 +12856,7 @@ | |||||||
|           "deviceOS", |           "deviceOS", | ||||||
|           "deviceType", |           "deviceType", | ||||||
|           "id", |           "id", | ||||||
|  |           "isPendingSyncReset", | ||||||
|           "token", |           "token", | ||||||
|           "updatedAt" |           "updatedAt" | ||||||
|         ], |         ], | ||||||
| @@ -12828,6 +12882,9 @@ | |||||||
|           "id": { |           "id": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "isPendingSyncReset": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "updatedAt": { |           "updatedAt": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           } |           } | ||||||
| @@ -12838,6 +12895,7 @@ | |||||||
|           "deviceOS", |           "deviceOS", | ||||||
|           "deviceType", |           "deviceType", | ||||||
|           "id", |           "id", | ||||||
|  |           "isPendingSyncReset", | ||||||
|           "updatedAt" |           "updatedAt" | ||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
| @@ -12854,6 +12912,14 @@ | |||||||
|         }, |         }, | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
|  |       "SessionUpdateDto": { | ||||||
|  |         "properties": { | ||||||
|  |           "isPendingSyncReset": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "SharedLinkCreateDto": { |       "SharedLinkCreateDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "albumId": { |           "albumId": { | ||||||
| @@ -13836,7 +13902,8 @@ | |||||||
|           "StackDeleteV1", |           "StackDeleteV1", | ||||||
|           "PersonV1", |           "PersonV1", | ||||||
|           "PersonDeleteV1", |           "PersonDeleteV1", | ||||||
|           "SyncAckV1" |           "SyncAckV1", | ||||||
|  |           "SyncResetV1" | ||||||
|         ], |         ], | ||||||
|         "type": "string" |         "type": "string" | ||||||
|       }, |       }, | ||||||
| @@ -14074,6 +14141,10 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "string" |         "type": "string" | ||||||
|       }, |       }, | ||||||
|  |       "SyncResetV1": { | ||||||
|  |         "properties": {}, | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "SyncStackDeleteV1": { |       "SyncStackDeleteV1": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "stackId": { |           "stackId": { | ||||||
| @@ -14116,6 +14187,9 @@ | |||||||
|       }, |       }, | ||||||
|       "SyncStreamDto": { |       "SyncStreamDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|  |           "reset": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "types": { |           "types": { | ||||||
|             "items": { |             "items": { | ||||||
|               "$ref": "#/components/schemas/SyncRequestType" |               "$ref": "#/components/schemas/SyncRequestType" | ||||||
|   | |||||||
| @@ -1164,6 +1164,7 @@ export type SessionResponseDto = { | |||||||
|     deviceType: string; |     deviceType: string; | ||||||
|     expiresAt?: string; |     expiresAt?: string; | ||||||
|     id: string; |     id: string; | ||||||
|  |     isPendingSyncReset: boolean; | ||||||
|     updatedAt: string; |     updatedAt: string; | ||||||
| }; | }; | ||||||
| export type SessionCreateDto = { | export type SessionCreateDto = { | ||||||
| @@ -1179,9 +1180,13 @@ export type SessionCreateResponseDto = { | |||||||
|     deviceType: string; |     deviceType: string; | ||||||
|     expiresAt?: string; |     expiresAt?: string; | ||||||
|     id: string; |     id: string; | ||||||
|  |     isPendingSyncReset: boolean; | ||||||
|     token: string; |     token: string; | ||||||
|     updatedAt: string; |     updatedAt: string; | ||||||
| }; | }; | ||||||
|  | export type SessionUpdateDto = { | ||||||
|  |     isPendingSyncReset?: boolean; | ||||||
|  | }; | ||||||
| export type SharedLinkResponseDto = { | export type SharedLinkResponseDto = { | ||||||
|     album?: AlbumResponseDto; |     album?: AlbumResponseDto; | ||||||
|     allowDownload: boolean; |     allowDownload: boolean; | ||||||
| @@ -1264,6 +1269,7 @@ export type AssetFullSyncDto = { | |||||||
|     userId?: string; |     userId?: string; | ||||||
| }; | }; | ||||||
| export type SyncStreamDto = { | export type SyncStreamDto = { | ||||||
|  |     reset?: boolean; | ||||||
|     types: SyncRequestType[]; |     types: SyncRequestType[]; | ||||||
| }; | }; | ||||||
| export type DatabaseBackupConfig = { | export type DatabaseBackupConfig = { | ||||||
| @@ -3170,6 +3176,19 @@ export function deleteSession({ id }: { | |||||||
|         method: "DELETE" |         method: "DELETE" | ||||||
|     })); |     })); | ||||||
| } | } | ||||||
|  | export function updateSession({ id, sessionUpdateDto }: { | ||||||
|  |     id: string; | ||||||
|  |     sessionUpdateDto: SessionUpdateDto; | ||||||
|  | }, opts?: Oazapfts.RequestOpts) { | ||||||
|  |     return oazapfts.ok(oazapfts.fetchJson<{ | ||||||
|  |         status: 200; | ||||||
|  |         data: SessionResponseDto; | ||||||
|  |     }>(`/sessions/${encodeURIComponent(id)}`, oazapfts.json({ | ||||||
|  |         ...opts, | ||||||
|  |         method: "PUT", | ||||||
|  |         body: sessionUpdateDto | ||||||
|  |     }))); | ||||||
|  | } | ||||||
| export function lockSession({ id }: { | export function lockSession({ id }: { | ||||||
|     id: string; |     id: string; | ||||||
| }, opts?: Oazapfts.RequestOpts) { | }, opts?: Oazapfts.RequestOpts) { | ||||||
| @@ -4097,7 +4116,8 @@ export enum SyncEntityType { | |||||||
|     StackDeleteV1 = "StackDeleteV1", |     StackDeleteV1 = "StackDeleteV1", | ||||||
|     PersonV1 = "PersonV1", |     PersonV1 = "PersonV1", | ||||||
|     PersonDeleteV1 = "PersonDeleteV1", |     PersonDeleteV1 = "PersonDeleteV1", | ||||||
|     SyncAckV1 = "SyncAckV1" |     SyncAckV1 = "SyncAckV1", | ||||||
|  |     SyncResetV1 = "SyncResetV1" | ||||||
| } | } | ||||||
| export enum SyncRequestType { | export enum SyncRequestType { | ||||||
|     AlbumsV1 = "AlbumsV1", |     AlbumsV1 = "AlbumsV1", | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; | import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; | ||||||
| import { ApiTags } from '@nestjs/swagger'; | import { ApiTags } from '@nestjs/swagger'; | ||||||
| import { AuthDto } from 'src/dtos/auth.dto'; | import { AuthDto } from 'src/dtos/auth.dto'; | ||||||
| import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto } from 'src/dtos/session.dto'; | import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto'; | ||||||
| import { Permission } from 'src/enum'; | import { Permission } from 'src/enum'; | ||||||
| import { Auth, Authenticated } from 'src/middleware/auth.guard'; | import { Auth, Authenticated } from 'src/middleware/auth.guard'; | ||||||
| import { SessionService } from 'src/services/session.service'; | import { SessionService } from 'src/services/session.service'; | ||||||
| @@ -31,6 +31,16 @@ export class SessionController { | |||||||
|     return this.service.deleteAll(auth); |     return this.service.deleteAll(auth); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @Put(':id') | ||||||
|  |   @Authenticated({ permission: Permission.SESSION_UPDATE }) | ||||||
|  |   updateSession( | ||||||
|  |     @Auth() auth: AuthDto, | ||||||
|  |     @Param() { id }: UUIDParamDto, | ||||||
|  |     @Body() dto: SessionUpdateDto, | ||||||
|  |   ): Promise<SessionResponseDto> { | ||||||
|  |     return this.service.update(auth, id, dto); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @Delete(':id') |   @Delete(':id') | ||||||
|   @Authenticated({ permission: Permission.SESSION_DELETE }) |   @Authenticated({ permission: Permission.SESSION_DELETE }) | ||||||
|   @HttpCode(HttpStatus.NO_CONTENT) |   @HttpCode(HttpStatus.NO_CONTENT) | ||||||
|   | |||||||
| @@ -201,6 +201,7 @@ export type Album = Selectable<AlbumTable> & { | |||||||
|  |  | ||||||
| export type AuthSession = { | export type AuthSession = { | ||||||
|   id: string; |   id: string; | ||||||
|  |   isPendingSyncReset: boolean; | ||||||
|   hasElevatedPermission: boolean; |   hasElevatedPermission: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -238,6 +239,7 @@ export type Session = { | |||||||
|   deviceOS: string; |   deviceOS: string; | ||||||
|   deviceType: string; |   deviceType: string; | ||||||
|   pinExpiresAt: Date | null; |   pinExpiresAt: Date | null; | ||||||
|  |   isPendingSyncReset: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type Exif = Omit<Selectable<ExifTable>, 'updatedAt' | 'updateId'>; | export type Exif = Omit<Selectable<ExifTable>, 'updatedAt' | 'updateId'>; | ||||||
| @@ -311,7 +313,7 @@ export const columns = { | |||||||
|     'users.quotaSizeInBytes', |     'users.quotaSizeInBytes', | ||||||
|   ], |   ], | ||||||
|   authApiKey: ['api_keys.id', 'api_keys.permissions'], |   authApiKey: ['api_keys.id', 'api_keys.permissions'], | ||||||
|   authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'], |   authSession: ['sessions.id', 'sessions.isPendingSyncReset', 'sessions.updatedAt', 'sessions.pinExpiresAt'], | ||||||
|   authSharedLink: [ |   authSharedLink: [ | ||||||
|     'shared_links.id', |     'shared_links.id', | ||||||
|     'shared_links.userId', |     'shared_links.userId', | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { IsInt, IsPositive, IsString } from 'class-validator'; | import { IsInt, IsPositive, IsString } from 'class-validator'; | ||||||
| import { Session } from 'src/database'; | import { Session } from 'src/database'; | ||||||
| import { Optional } from 'src/validation'; | import { Optional, ValidateBoolean } from 'src/validation'; | ||||||
|  |  | ||||||
| export class SessionCreateDto { | export class SessionCreateDto { | ||||||
|   /** |   /** | ||||||
| @@ -20,6 +20,11 @@ export class SessionCreateDto { | |||||||
|   deviceOS?: string; |   deviceOS?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export class SessionUpdateDto { | ||||||
|  |   @ValidateBoolean({ optional: true }) | ||||||
|  |   isPendingSyncReset?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
| export class SessionResponseDto { | export class SessionResponseDto { | ||||||
|   id!: string; |   id!: string; | ||||||
|   createdAt!: string; |   createdAt!: string; | ||||||
| @@ -28,6 +33,7 @@ export class SessionResponseDto { | |||||||
|   current!: boolean; |   current!: boolean; | ||||||
|   deviceType!: string; |   deviceType!: string; | ||||||
|   deviceOS!: string; |   deviceOS!: string; | ||||||
|  |   isPendingSyncReset!: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class SessionCreateResponseDto extends SessionResponseDto { | export class SessionCreateResponseDto extends SessionResponseDto { | ||||||
| @@ -42,4 +48,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse | |||||||
|   current: currentId === entity.id, |   current: currentId === entity.id, | ||||||
|   deviceOS: entity.deviceOS, |   deviceOS: entity.deviceOS, | ||||||
|   deviceType: entity.deviceType, |   deviceType: entity.deviceType, | ||||||
|  |   isPendingSyncReset: entity.isPendingSyncReset, | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { | |||||||
|   SyncEntityType, |   SyncEntityType, | ||||||
|   SyncRequestType, |   SyncRequestType, | ||||||
| } from 'src/enum'; | } from 'src/enum'; | ||||||
| import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; | import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; | ||||||
|  |  | ||||||
| export class AssetFullSyncDto { | export class AssetFullSyncDto { | ||||||
|   @ValidateUUID({ optional: true }) |   @ValidateUUID({ optional: true }) | ||||||
| @@ -256,6 +256,9 @@ export class SyncPersonDeleteV1 { | |||||||
| @ExtraModel() | @ExtraModel() | ||||||
| export class SyncAckV1 {} | export class SyncAckV1 {} | ||||||
|  |  | ||||||
|  | @ExtraModel() | ||||||
|  | export class SyncResetV1 {} | ||||||
|  |  | ||||||
| export type SyncItem = { | export type SyncItem = { | ||||||
|   [SyncEntityType.UserV1]: SyncUserV1; |   [SyncEntityType.UserV1]: SyncUserV1; | ||||||
|   [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; |   [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; | ||||||
| @@ -293,12 +296,16 @@ export type SyncItem = { | |||||||
|   [SyncEntityType.PersonV1]: SyncPersonV1; |   [SyncEntityType.PersonV1]: SyncPersonV1; | ||||||
|   [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; |   [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; | ||||||
|   [SyncEntityType.SyncAckV1]: SyncAckV1; |   [SyncEntityType.SyncAckV1]: SyncAckV1; | ||||||
|  |   [SyncEntityType.SyncResetV1]: SyncResetV1; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export class SyncStreamDto { | export class SyncStreamDto { | ||||||
|   @IsEnum(SyncRequestType, { each: true }) |   @IsEnum(SyncRequestType, { each: true }) | ||||||
|   @ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true }) |   @ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true }) | ||||||
|   types!: SyncRequestType[]; |   types!: SyncRequestType[]; | ||||||
|  |  | ||||||
|  |   @ValidateBoolean({ optional: true }) | ||||||
|  |   reset?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class SyncAckDto { | export class SyncAckDto { | ||||||
|   | |||||||
| @@ -640,6 +640,7 @@ export enum SyncEntityType { | |||||||
|   PersonDeleteV1 = 'PersonDeleteV1', |   PersonDeleteV1 = 'PersonDeleteV1', | ||||||
|  |  | ||||||
|   SyncAckV1 = 'SyncAckV1', |   SyncAckV1 = 'SyncAckV1', | ||||||
|  |   SyncResetV1 = 'SyncResetV1', | ||||||
| } | } | ||||||
|  |  | ||||||
| export enum NotificationLevel { | export enum NotificationLevel { | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ where | |||||||
| -- SessionRepository.getByToken | -- SessionRepository.getByToken | ||||||
| select | select | ||||||
|   "sessions"."id", |   "sessions"."id", | ||||||
|  |   "sessions"."isPendingSyncReset", | ||||||
|   "sessions"."updatedAt", |   "sessions"."updatedAt", | ||||||
|   "sessions"."pinExpiresAt", |   "sessions"."pinExpiresAt", | ||||||
|   ( |   ( | ||||||
| @@ -71,3 +72,15 @@ set | |||||||
|   "pinExpiresAt" = $1 |   "pinExpiresAt" = $1 | ||||||
| where | where | ||||||
|   "userId" = $2 |   "userId" = $2 | ||||||
|  |  | ||||||
|  | -- SessionRepository.resetSyncProgress | ||||||
|  | begin | ||||||
|  | update "sessions" | ||||||
|  | set | ||||||
|  |   "isPendingSyncReset" = $1 | ||||||
|  | where | ||||||
|  |   "id" = $2 | ||||||
|  | delete from "session_sync_checkpoints" | ||||||
|  | where | ||||||
|  |   "sessionId" = $1 | ||||||
|  | commit | ||||||
|   | |||||||
| @@ -95,4 +95,14 @@ export class SessionRepository { | |||||||
|   async lockAll(userId: string) { |   async lockAll(userId: string) { | ||||||
|     await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute(); |     await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @GenerateSql({ params: [DummyValue.UUID] }) | ||||||
|  |   async resetSyncProgress(sessionId: string) { | ||||||
|  |     await this.db.transaction().execute((tx) => { | ||||||
|  |       return Promise.all([ | ||||||
|  |         tx.updateTable('sessions').set({ isPendingSyncReset: false }).where('id', '=', sessionId).execute(), | ||||||
|  |         tx.deleteFrom('session_sync_checkpoints').where('sessionId', '=', sessionId).execute(), | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | import { Kysely, sql } from 'kysely'; | ||||||
|  |  | ||||||
|  | export async function up(db: Kysely<any>): Promise<void> { | ||||||
|  |   await sql`ALTER TABLE "sessions" ADD "isPendingSyncReset" boolean NOT NULL DEFAULT false;`.execute(db); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(db: Kysely<any>): Promise<void> { | ||||||
|  |   await sql`ALTER TABLE "sessions" DROP COLUMN "isPendingSyncReset";`.execute(db); | ||||||
|  | } | ||||||
| @@ -45,6 +45,9 @@ export class SessionTable { | |||||||
|   @UpdateIdColumn({ indexName: 'IDX_sessions_update_id' }) |   @UpdateIdColumn({ indexName: 'IDX_sessions_update_id' }) | ||||||
|   updateId!: Generated<string>; |   updateId!: Generated<string>; | ||||||
|  |  | ||||||
|  |   @Column({ type: 'boolean', default: false }) | ||||||
|  |   isPendingSyncReset!: Generated<boolean>; | ||||||
|  |  | ||||||
|   @Column({ type: 'timestamp with time zone', nullable: true }) |   @Column({ type: 'timestamp with time zone', nullable: true }) | ||||||
|   pinExpiresAt!: Timestamp | null; |   pinExpiresAt!: Timestamp | null; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -241,6 +241,7 @@ describe(AuthService.name, () => { | |||||||
|       const sessionWithToken = { |       const sessionWithToken = { | ||||||
|         id: session.id, |         id: session.id, | ||||||
|         updatedAt: session.updatedAt, |         updatedAt: session.updatedAt, | ||||||
|  |         isPendingSyncReset: false, | ||||||
|         user: factory.authUser(), |         user: factory.authUser(), | ||||||
|         pinExpiresAt: null, |         pinExpiresAt: null, | ||||||
|       }; |       }; | ||||||
| @@ -255,7 +256,11 @@ describe(AuthService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         user: sessionWithToken.user, |         user: sessionWithToken.user, | ||||||
|         session: { id: session.id, hasElevatedPermission: false }, |         session: { | ||||||
|  |           id: session.id, | ||||||
|  |           hasElevatedPermission: false, | ||||||
|  |           isPendingSyncReset: session.isPendingSyncReset, | ||||||
|  |         }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @@ -366,6 +371,7 @@ describe(AuthService.name, () => { | |||||||
|         id: session.id, |         id: session.id, | ||||||
|         updatedAt: session.updatedAt, |         updatedAt: session.updatedAt, | ||||||
|         user: factory.authUser(), |         user: factory.authUser(), | ||||||
|  |         isPendingSyncReset: false, | ||||||
|         pinExpiresAt: null, |         pinExpiresAt: null, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
| @@ -379,7 +385,11 @@ describe(AuthService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         user: sessionWithToken.user, |         user: sessionWithToken.user, | ||||||
|         session: { id: session.id, hasElevatedPermission: false }, |         session: { | ||||||
|  |           id: session.id, | ||||||
|  |           hasElevatedPermission: false, | ||||||
|  |           isPendingSyncReset: session.isPendingSyncReset, | ||||||
|  |         }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -389,6 +399,7 @@ describe(AuthService.name, () => { | |||||||
|         id: session.id, |         id: session.id, | ||||||
|         updatedAt: session.updatedAt, |         updatedAt: session.updatedAt, | ||||||
|         user: factory.authUser(), |         user: factory.authUser(), | ||||||
|  |         isPendingSyncReset: false, | ||||||
|         pinExpiresAt: null, |         pinExpiresAt: null, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
| @@ -409,6 +420,7 @@ describe(AuthService.name, () => { | |||||||
|         id: session.id, |         id: session.id, | ||||||
|         updatedAt: session.updatedAt, |         updatedAt: session.updatedAt, | ||||||
|         user: factory.authUser(), |         user: factory.authUser(), | ||||||
|  |         isPendingSyncReset: false, | ||||||
|         pinExpiresAt: null, |         pinExpiresAt: null, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -466,6 +466,7 @@ export class AuthService extends BaseService { | |||||||
|         user: session.user, |         user: session.user, | ||||||
|         session: { |         session: { | ||||||
|           id: session.id, |           id: session.id, | ||||||
|  |           isPendingSyncReset: session.isPendingSyncReset, | ||||||
|           hasElevatedPermission, |           hasElevatedPermission, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|   | |||||||
| @@ -2,7 +2,13 @@ import { BadRequestException, Injectable } from '@nestjs/common'; | |||||||
| import { DateTime } from 'luxon'; | import { DateTime } from 'luxon'; | ||||||
| import { OnJob } from 'src/decorators'; | import { OnJob } from 'src/decorators'; | ||||||
| import { AuthDto } from 'src/dtos/auth.dto'; | import { AuthDto } from 'src/dtos/auth.dto'; | ||||||
| import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, mapSession } from 'src/dtos/session.dto'; | import { | ||||||
|  |   SessionCreateDto, | ||||||
|  |   SessionCreateResponseDto, | ||||||
|  |   SessionResponseDto, | ||||||
|  |   SessionUpdateDto, | ||||||
|  |   mapSession, | ||||||
|  | } from 'src/dtos/session.dto'; | ||||||
| import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; | import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; | ||||||
| import { BaseService } from 'src/services/base.service'; | import { BaseService } from 'src/services/base.service'; | ||||||
|  |  | ||||||
| @@ -44,6 +50,20 @@ export class SessionService extends BaseService { | |||||||
|     return sessions.map((session) => mapSession(session, auth.session?.id)); |     return sessions.map((session) => mapSession(session, auth.session?.id)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async update(auth: AuthDto, id: string, dto: SessionUpdateDto): Promise<SessionResponseDto> { | ||||||
|  |     await this.requireAccess({ auth, permission: Permission.SESSION_UPDATE, ids: [id] }); | ||||||
|  |  | ||||||
|  |     if (Object.values(dto).filter((prop) => prop !== undefined).length === 0) { | ||||||
|  |       throw new BadRequestException('No fields to update'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const session = await this.sessionRepository.update(id, { | ||||||
|  |       isPendingSyncReset: dto.isPendingSyncReset, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return mapSession(session); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async delete(auth: AuthDto, id: string): Promise<void> { |   async delete(auth: AuthDto, id: string): Promise<void> { | ||||||
|     await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); |     await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); | ||||||
|     await this.sessionRepository.delete(id); |     await this.sessionRepository.delete(id); | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ import { SyncAck } from 'src/types'; | |||||||
| import { getMyPartnerIds } from 'src/utils/asset.util'; | import { getMyPartnerIds } from 'src/utils/asset.util'; | ||||||
| import { hexOrBufferToBase64 } from 'src/utils/bytes'; | import { hexOrBufferToBase64 } from 'src/utils/bytes'; | ||||||
| import { setIsEqual } from 'src/utils/set'; | import { setIsEqual } from 'src/utils/set'; | ||||||
| import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync'; | import { fromAck, mapJsonLine, serialize, SerializeOptions, toAck } from 'src/utils/sync'; | ||||||
|  |  | ||||||
| type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>; | type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>; | ||||||
| type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & { | type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & { | ||||||
| @@ -118,30 +118,42 @@ export class SyncService extends BaseService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async stream(auth: AuthDto, response: Writable, dto: SyncStreamDto) { |   async stream(auth: AuthDto, response: Writable, dto: SyncStreamDto) { | ||||||
|     const sessionId = auth.session?.id; |     const session = auth.session; | ||||||
|     if (!sessionId) { |     if (!session) { | ||||||
|       return throwSessionRequired(); |       return throwSessionRequired(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const checkpoints = await this.syncCheckpointRepository.getAll(sessionId); |     if (dto.reset) { | ||||||
|  |       await this.sessionRepository.resetSyncProgress(session.id); | ||||||
|  |       session.isPendingSyncReset = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (session.isPendingSyncReset) { | ||||||
|  |       response.write(mapJsonLine({ type: SyncEntityType.SyncResetV1, data: {} })); | ||||||
|  |       response.end(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const checkpoints = await this.syncCheckpointRepository.getAll(session.id); | ||||||
|     const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); |     const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); | ||||||
|  |  | ||||||
|     const handlers: Record<SyncRequestType, () => Promise<void>> = { |     const handlers: Record<SyncRequestType, () => Promise<void>> = { | ||||||
|       [SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap), |       [SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap), | ||||||
|       [SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth), |       [SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth), |       [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(response, checkpointMap, auth), |       [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId), |       [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.PartnerAssetExifsV1]: () => |       [SyncRequestType.PartnerAssetExifsV1]: () => | ||||||
|         this.syncPartnerAssetExifsV1(response, checkpointMap, auth, sessionId), |         this.syncPartnerAssetExifsV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(response, checkpointMap, auth), |       [SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId), |       [SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, sessionId), |       [SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, sessionId), |       [SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, sessionId), |       [SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(response, checkpointMap, auth), |       [SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(response, checkpointMap, auth), |       [SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.StacksV1]: () => this.syncStackV1(response, checkpointMap, auth), |       [SyncRequestType.StacksV1]: () => this.syncStackV1(response, checkpointMap, auth), | ||||||
|       [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, sessionId), |       [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, session.id), | ||||||
|       [SyncRequestType.PeopleV1]: () => this.syncPeopleV1(response, checkpointMap, auth), |       [SyncRequestType.PeopleV1]: () => this.syncPeopleV1(response, checkpointMap, auth), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -234,11 +234,11 @@ export class SyncTestContext extends MediumTestContext<SyncService> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async syncStream(auth: AuthDto, types: SyncRequestType[]) { |   async syncStream(auth: AuthDto, types: SyncRequestType[], reset?: boolean) { | ||||||
|     const stream = mediumFactory.syncStream(); |     const stream = mediumFactory.syncStream(); | ||||||
|     // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy |     // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy | ||||||
|     await wait(2); |     await wait(2); | ||||||
|     await this.sut.stream(auth, stream, { types }); |     await this.sut.stream(auth, stream, { types, reset }); | ||||||
|  |  | ||||||
|     return stream.getResponse(); |     return stream.getResponse(); | ||||||
|   } |   } | ||||||
| @@ -481,6 +481,7 @@ const sessionInsert = ({ | |||||||
|   const defaults: Insertable<SessionTable> = { |   const defaults: Insertable<SessionTable> = { | ||||||
|     id, |     id, | ||||||
|     userId, |     userId, | ||||||
|  |     isPendingSyncReset: false, | ||||||
|     token: sha256(id), |     token: sha256(id), | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								server/test/medium/specs/sync/sync-reset.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								server/test/medium/specs/sync/sync-reset.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | import { Kysely } from 'kysely'; | ||||||
|  | import { SyncEntityType, SyncRequestType } from 'src/enum'; | ||||||
|  | import { DB } from 'src/schema'; | ||||||
|  | import { SyncTestContext } from 'test/medium.factory'; | ||||||
|  | import { getKyselyDB } from 'test/utils'; | ||||||
|  |  | ||||||
|  | let defaultDatabase: Kysely<DB>; | ||||||
|  |  | ||||||
|  | const setup = async (db?: Kysely<DB>) => { | ||||||
|  |   const ctx = new SyncTestContext(db || defaultDatabase); | ||||||
|  |   const { auth, user, session } = await ctx.newSyncAuthUser(); | ||||||
|  |   return { auth, user, session, ctx }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | beforeAll(async () => { | ||||||
|  |   defaultDatabase = await getKyselyDB(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | describe(SyncEntityType.SyncResetV1, () => { | ||||||
|  |   it('should work', async () => { | ||||||
|  |     const { auth, ctx } = await setup(); | ||||||
|  |  | ||||||
|  |     const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); | ||||||
|  |     expect(response).toEqual([]); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should detect a pending sync reset', async () => { | ||||||
|  |     const { auth, ctx } = await setup(); | ||||||
|  |  | ||||||
|  |     auth.session!.isPendingSyncReset = true; | ||||||
|  |  | ||||||
|  |     const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); | ||||||
|  |     expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {} }]); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should not send other dtos when a reset is pending', async () => { | ||||||
|  |     const { auth, user, ctx } = await setup(); | ||||||
|  |  | ||||||
|  |     await ctx.newAsset({ ownerId: user.id }); | ||||||
|  |  | ||||||
|  |     await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); | ||||||
|  |  | ||||||
|  |     auth.session!.isPendingSyncReset = true; | ||||||
|  |  | ||||||
|  |     await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ | ||||||
|  |       { type: SyncEntityType.SyncResetV1, data: {} }, | ||||||
|  |     ]); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should allow resetting a pending reset when requesting changes ', async () => { | ||||||
|  |     const { auth, user, ctx } = await setup(); | ||||||
|  |  | ||||||
|  |     await ctx.newAsset({ ownerId: user.id }); | ||||||
|  |  | ||||||
|  |     auth.session!.isPendingSyncReset = true; | ||||||
|  |  | ||||||
|  |     await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([ | ||||||
|  |       expect.objectContaining({ | ||||||
|  |         type: SyncEntityType.AssetV1, | ||||||
|  |       }), | ||||||
|  |     ]); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -58,7 +58,11 @@ const authFactory = ({ | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (session) { |   if (session) { | ||||||
|     auth.session = { id: session.id, hasElevatedPermission: false }; |     auth.session = { | ||||||
|  |       id: session.id, | ||||||
|  |       isPendingSyncReset: false, | ||||||
|  |       hasElevatedPermission: false, | ||||||
|  |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (sharedLink) { |   if (sharedLink) { | ||||||
| @@ -131,6 +135,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({ | |||||||
|   expiresAt: null, |   expiresAt: null, | ||||||
|   userId: newUuid(), |   userId: newUuid(), | ||||||
|   pinExpiresAt: newDate(), |   pinExpiresAt: newDate(), | ||||||
|  |   isPendingSyncReset: false, | ||||||
|   ...session, |   ...session, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user