mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: notifications (#17701)
* feat: notifications * UI works * chore: pr feedback * initial fetch and clear notification upon logging out * fix: merge --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
23717ce981
commit
1b5fc9c665
@ -857,6 +857,7 @@
|
||||
"failed_to_remove_product_key": "Failed to remove product key",
|
||||
"failed_to_stack_assets": "Failed to stack assets",
|
||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||
"failed_to_update_notification_status": "Failed to update notification status",
|
||||
"import_path_already_exists": "This import path already exists.",
|
||||
"incorrect_email_or_password": "Incorrect email or password",
|
||||
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
||||
@ -1199,6 +1200,9 @@
|
||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||
"mark_as_read": "Mark as read",
|
||||
"mark_all_as_read": "Mark all as read",
|
||||
"marked_all_as_read": "Marked all as read",
|
||||
"matches": "Matches",
|
||||
"media_type": "Media type",
|
||||
"memories": "Memories",
|
||||
@ -1260,6 +1264,7 @@
|
||||
"no_places": "No places",
|
||||
"no_results": "No results",
|
||||
"no_results_description": "Try a synonym or more general keyword",
|
||||
"no_notifications": "No notifications",
|
||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||
"not_in_any_album": "Not in any album",
|
||||
"not_selected": "Not selected",
|
||||
|
18
mobile/openapi/README.md
generated
18
mobile/openapi/README.md
generated
@ -145,8 +145,15 @@ Class | Method | HTTP request | Description
|
||||
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
||||
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
||||
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
||||
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} |
|
||||
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email |
|
||||
*NotificationsApi* | [**deleteNotification**](doc//NotificationsApi.md#deletenotification) | **DELETE** /notifications/{id} |
|
||||
*NotificationsApi* | [**deleteNotifications**](doc//NotificationsApi.md#deletenotifications) | **DELETE** /notifications |
|
||||
*NotificationsApi* | [**getNotification**](doc//NotificationsApi.md#getnotification) | **GET** /notifications/{id} |
|
||||
*NotificationsApi* | [**getNotifications**](doc//NotificationsApi.md#getnotifications) | **GET** /notifications |
|
||||
*NotificationsApi* | [**updateNotification**](doc//NotificationsApi.md#updatenotification) | **PUT** /notifications/{id} |
|
||||
*NotificationsApi* | [**updateNotifications**](doc//NotificationsApi.md#updatenotifications) | **PUT** /notifications |
|
||||
*NotificationsAdminApi* | [**createNotification**](doc//NotificationsAdminApi.md#createnotification) | **POST** /admin/notifications |
|
||||
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /admin/notifications/templates/{name} |
|
||||
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /admin/notifications/test-email |
|
||||
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
||||
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
||||
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
||||
@ -360,6 +367,13 @@ Class | Method | HTTP request | Description
|
||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||
- [MergePersonDto](doc//MergePersonDto.md)
|
||||
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
||||
- [NotificationCreateDto](doc//NotificationCreateDto.md)
|
||||
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
|
||||
- [NotificationDto](doc//NotificationDto.md)
|
||||
- [NotificationLevel](doc//NotificationLevel.md)
|
||||
- [NotificationType](doc//NotificationType.md)
|
||||
- [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md)
|
||||
- [NotificationUpdateDto](doc//NotificationUpdateDto.md)
|
||||
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
|
||||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||
|
8
mobile/openapi/lib/api.dart
generated
8
mobile/openapi/lib/api.dart
generated
@ -44,6 +44,7 @@ part 'api/jobs_api.dart';
|
||||
part 'api/libraries_api.dart';
|
||||
part 'api/map_api.dart';
|
||||
part 'api/memories_api.dart';
|
||||
part 'api/notifications_api.dart';
|
||||
part 'api/notifications_admin_api.dart';
|
||||
part 'api/o_auth_api.dart';
|
||||
part 'api/partners_api.dart';
|
||||
@ -167,6 +168,13 @@ part 'model/memory_type.dart';
|
||||
part 'model/memory_update_dto.dart';
|
||||
part 'model/merge_person_dto.dart';
|
||||
part 'model/metadata_search_dto.dart';
|
||||
part 'model/notification_create_dto.dart';
|
||||
part 'model/notification_delete_all_dto.dart';
|
||||
part 'model/notification_dto.dart';
|
||||
part 'model/notification_level.dart';
|
||||
part 'model/notification_type.dart';
|
||||
part 'model/notification_update_all_dto.dart';
|
||||
part 'model/notification_update_dto.dart';
|
||||
part 'model/o_auth_authorize_response_dto.dart';
|
||||
part 'model/o_auth_callback_dto.dart';
|
||||
part 'model/o_auth_config_dto.dart';
|
||||
|
55
mobile/openapi/lib/api/notifications_admin_api.dart
generated
55
mobile/openapi/lib/api/notifications_admin_api.dart
generated
@ -16,7 +16,54 @@ class NotificationsAdminApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'POST /notifications/admin/templates/{name}' operation and returns the [Response].
|
||||
/// Performs an HTTP 'POST /admin/notifications' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [NotificationCreateDto] notificationCreateDto (required):
|
||||
Future<Response> createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/notifications';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = notificationCreateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [NotificationCreateDto] notificationCreateDto (required):
|
||||
Future<NotificationDto?> createNotification(NotificationCreateDto notificationCreateDto,) async {
|
||||
final response = await createNotificationWithHttpInfo(notificationCreateDto,);
|
||||
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), 'NotificationDto',) as NotificationDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /admin/notifications/templates/{name}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
@ -24,7 +71,7 @@ class NotificationsAdminApi {
|
||||
/// * [TemplateDto] templateDto (required):
|
||||
Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications/admin/templates/{name}'
|
||||
final apiPath = r'/admin/notifications/templates/{name}'
|
||||
.replaceAll('{name}', name);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
@ -68,13 +115,13 @@ class NotificationsAdminApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /notifications/admin/test-email' operation and returns the [Response].
|
||||
/// Performs an HTTP 'POST /admin/notifications/test-email' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
||||
Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications/admin/test-email';
|
||||
final apiPath = r'/admin/notifications/test-email';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = systemConfigSmtpDto;
|
||||
|
311
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
311
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
@ -0,0 +1,311 @@
|
||||
//
|
||||
// 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 NotificationsApi {
|
||||
NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'DELETE /notifications/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deleteNotificationWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications/{id}'
|
||||
.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(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> deleteNotification(String id,) async {
|
||||
final response = await deleteNotificationWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /notifications' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
|
||||
Future<Response> deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = notificationDeleteAllDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
|
||||
Future<void> deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async {
|
||||
final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /notifications/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getNotificationWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications/{id}'
|
||||
.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(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<NotificationDto?> getNotification(String id,) async {
|
||||
final response = await getNotificationWithHttpInfo(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), 'NotificationDto',) as NotificationDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /notifications' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
///
|
||||
/// * [NotificationLevel] level:
|
||||
///
|
||||
/// * [NotificationType] type:
|
||||
///
|
||||
/// * [bool] unread:
|
||||
Future<Response> getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (level != null) {
|
||||
queryParams.addAll(_queryParams('', 'level', level));
|
||||
}
|
||||
if (type != null) {
|
||||
queryParams.addAll(_queryParams('', 'type', type));
|
||||
}
|
||||
if (unread != null) {
|
||||
queryParams.addAll(_queryParams('', 'unread', unread));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
///
|
||||
/// * [NotificationLevel] level:
|
||||
///
|
||||
/// * [NotificationType] type:
|
||||
///
|
||||
/// * [bool] unread:
|
||||
Future<List<NotificationDto>?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||
final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, );
|
||||
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) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<NotificationDto>') as List)
|
||||
.cast<NotificationDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /notifications/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [NotificationUpdateDto] notificationUpdateDto (required):
|
||||
Future<Response> updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = notificationUpdateDto;
|
||||
|
||||
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):
|
||||
///
|
||||
/// * [NotificationUpdateDto] notificationUpdateDto (required):
|
||||
Future<NotificationDto?> updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async {
|
||||
final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,);
|
||||
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), 'NotificationDto',) as NotificationDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /notifications' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
|
||||
Future<Response> updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = notificationUpdateAllDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
|
||||
Future<void> updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async {
|
||||
final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
}
|
14
mobile/openapi/lib/api_client.dart
generated
14
mobile/openapi/lib/api_client.dart
generated
@ -390,6 +390,20 @@ class ApiClient {
|
||||
return MergePersonDto.fromJson(value);
|
||||
case 'MetadataSearchDto':
|
||||
return MetadataSearchDto.fromJson(value);
|
||||
case 'NotificationCreateDto':
|
||||
return NotificationCreateDto.fromJson(value);
|
||||
case 'NotificationDeleteAllDto':
|
||||
return NotificationDeleteAllDto.fromJson(value);
|
||||
case 'NotificationDto':
|
||||
return NotificationDto.fromJson(value);
|
||||
case 'NotificationLevel':
|
||||
return NotificationLevelTypeTransformer().decode(value);
|
||||
case 'NotificationType':
|
||||
return NotificationTypeTypeTransformer().decode(value);
|
||||
case 'NotificationUpdateAllDto':
|
||||
return NotificationUpdateAllDto.fromJson(value);
|
||||
case 'NotificationUpdateDto':
|
||||
return NotificationUpdateDto.fromJson(value);
|
||||
case 'OAuthAuthorizeResponseDto':
|
||||
return OAuthAuthorizeResponseDto.fromJson(value);
|
||||
case 'OAuthCallbackDto':
|
||||
|
6
mobile/openapi/lib/api_helper.dart
generated
6
mobile/openapi/lib/api_helper.dart
generated
@ -100,6 +100,12 @@ String parameterToString(dynamic value) {
|
||||
if (value is MemoryType) {
|
||||
return MemoryTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is NotificationLevel) {
|
||||
return NotificationLevelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is NotificationType) {
|
||||
return NotificationTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PartnerDirection) {
|
||||
return PartnerDirectionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
180
mobile/openapi/lib/model/notification_create_dto.dart
generated
Normal file
180
mobile/openapi/lib/model/notification_create_dto.dart
generated
Normal file
@ -0,0 +1,180 @@
|
||||
//
|
||||
// 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 NotificationCreateDto {
|
||||
/// Returns a new [NotificationCreateDto] instance.
|
||||
NotificationCreateDto({
|
||||
this.data,
|
||||
this.description,
|
||||
this.level,
|
||||
this.readAt,
|
||||
required this.title,
|
||||
this.type,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
Object? data;
|
||||
|
||||
String? description;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
NotificationLevel? level;
|
||||
|
||||
DateTime? readAt;
|
||||
|
||||
String title;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
NotificationType? type;
|
||||
|
||||
String userId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto &&
|
||||
other.data == data &&
|
||||
other.description == description &&
|
||||
other.level == level &&
|
||||
other.readAt == readAt &&
|
||||
other.title == title &&
|
||||
other.type == type &&
|
||||
other.userId == userId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(data == null ? 0 : data!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(level == null ? 0 : level!.hashCode) +
|
||||
(readAt == null ? 0 : readAt!.hashCode) +
|
||||
(title.hashCode) +
|
||||
(type == null ? 0 : type!.hashCode) +
|
||||
(userId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'NotificationCreateDto[data=$data, description=$description, level=$level, readAt=$readAt, title=$title, type=$type, userId=$userId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.data != null) {
|
||||
json[r'data'] = this.data;
|
||||
} else {
|
||||
// json[r'data'] = null;
|
||||
}
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
if (this.level != null) {
|
||||
json[r'level'] = this.level;
|
||||
} else {
|
||||
// json[r'level'] = null;
|
||||
}
|
||||
if (this.readAt != null) {
|
||||
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'readAt'] = null;
|
||||
}
|
||||
json[r'title'] = this.title;
|
||||
if (this.type != null) {
|
||||
json[r'type'] = this.type;
|
||||
} else {
|
||||
// json[r'type'] = null;
|
||||
}
|
||||
json[r'userId'] = this.userId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [NotificationCreateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static NotificationCreateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "NotificationCreateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return NotificationCreateDto(
|
||||
data: mapValueOfType<Object>(json, r'data'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
level: NotificationLevel.fromJson(json[r'level']),
|
||||
readAt: mapDateTime(json, r'readAt', r''),
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
type: NotificationType.fromJson(json[r'type']),
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<NotificationCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationCreateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationCreateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, NotificationCreateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, NotificationCreateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = NotificationCreateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of NotificationCreateDto-objects as value to a dart map
|
||||
static Map<String, List<NotificationCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<NotificationCreateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = NotificationCreateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'title',
|
||||
'userId',
|
||||
};
|
||||
}
|
||||
|
101
mobile/openapi/lib/model/notification_delete_all_dto.dart
generated
Normal file
101
mobile/openapi/lib/model/notification_delete_all_dto.dart
generated
Normal file
@ -0,0 +1,101 @@
|
||||
//
|
||||
// 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 NotificationDeleteAllDto {
|
||||
/// Returns a new [NotificationDeleteAllDto] instance.
|
||||
NotificationDeleteAllDto({
|
||||
this.ids = const [],
|
||||
});
|
||||
|
||||
List<String> ids;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is NotificationDeleteAllDto &&
|
||||
_deepEquality.equals(other.ids, ids);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(ids.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'NotificationDeleteAllDto[ids=$ids]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'ids'] = this.ids;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [NotificationDeleteAllDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static NotificationDeleteAllDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "NotificationDeleteAllDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return NotificationDeleteAllDto(
|
||||
ids: json[r'ids'] is Iterable
|
||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<NotificationDeleteAllDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationDeleteAllDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationDeleteAllDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, NotificationDeleteAllDto> mapFromJson(dynamic json) {
|
||||
final map = <String, NotificationDeleteAllDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = NotificationDeleteAllDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of NotificationDeleteAllDto-objects as value to a dart map
|
||||
static Map<String, List<NotificationDeleteAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<NotificationDeleteAllDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = NotificationDeleteAllDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'ids',
|
||||
};
|
||||
}
|
||||
|
182
mobile/openapi/lib/model/notification_dto.dart
generated
Normal file
182
mobile/openapi/lib/model/notification_dto.dart
generated
Normal file
@ -0,0 +1,182 @@
|
||||
//
|
||||
// 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 NotificationDto {
|
||||
/// Returns a new [NotificationDto] instance.
|
||||
NotificationDto({
|
||||
required this.createdAt,
|
||||
this.data,
|
||||
this.description,
|
||||
required this.id,
|
||||
required this.level,
|
||||
this.readAt,
|
||||
required this.title,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
Object? data;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
String? description;
|
||||
|
||||
String id;
|
||||
|
||||
NotificationLevel level;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
DateTime? readAt;
|
||||
|
||||
String title;
|
||||
|
||||
NotificationType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is NotificationDto &&
|
||||
other.createdAt == createdAt &&
|
||||
other.data == data &&
|
||||
other.description == description &&
|
||||
other.id == id &&
|
||||
other.level == level &&
|
||||
other.readAt == readAt &&
|
||||
other.title == title &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(createdAt.hashCode) +
|
||||
(data == null ? 0 : data!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(level.hashCode) +
|
||||
(readAt == null ? 0 : readAt!.hashCode) +
|
||||
(title.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'NotificationDto[createdAt=$createdAt, data=$data, description=$description, id=$id, level=$level, readAt=$readAt, title=$title, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
if (this.data != null) {
|
||||
json[r'data'] = this.data;
|
||||
} else {
|
||||
// json[r'data'] = null;
|
||||
}
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'level'] = this.level;
|
||||
if (this.readAt != null) {
|
||||
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'readAt'] = null;
|
||||
}
|
||||
json[r'title'] = this.title;
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [NotificationDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static NotificationDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "NotificationDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return NotificationDto(
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
data: mapValueOfType<Object>(json, r'data'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
level: NotificationLevel.fromJson(json[r'level'])!,
|
||||
readAt: mapDateTime(json, r'readAt', r''),
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
type: NotificationType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<NotificationDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, NotificationDto> mapFromJson(dynamic json) {
|
||||
final map = <String, NotificationDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = NotificationDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of NotificationDto-objects as value to a dart map
|
||||
static Map<String, List<NotificationDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<NotificationDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = NotificationDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'createdAt',
|
||||
'id',
|
||||
'level',
|
||||
'title',
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
91
mobile/openapi/lib/model/notification_level.dart
generated
Normal file
91
mobile/openapi/lib/model/notification_level.dart
generated
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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 NotificationLevel {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const NotificationLevel._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const success = NotificationLevel._(r'success');
|
||||
static const error = NotificationLevel._(r'error');
|
||||
static const warning = NotificationLevel._(r'warning');
|
||||
static const info = NotificationLevel._(r'info');
|
||||
|
||||
/// List of all possible values in this [enum][NotificationLevel].
|
||||
static const values = <NotificationLevel>[
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
];
|
||||
|
||||
static NotificationLevel? fromJson(dynamic value) => NotificationLevelTypeTransformer().decode(value);
|
||||
|
||||
static List<NotificationLevel> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationLevel>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationLevel.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [NotificationLevel] to String,
|
||||
/// and [decode] dynamic data back to [NotificationLevel].
|
||||
class NotificationLevelTypeTransformer {
|
||||
factory NotificationLevelTypeTransformer() => _instance ??= const NotificationLevelTypeTransformer._();
|
||||
|
||||
const NotificationLevelTypeTransformer._();
|
||||
|
||||
String encode(NotificationLevel data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a NotificationLevel.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
NotificationLevel? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'success': return NotificationLevel.success;
|
||||
case r'error': return NotificationLevel.error;
|
||||
case r'warning': return NotificationLevel.warning;
|
||||
case r'info': return NotificationLevel.info;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [NotificationLevelTypeTransformer] instance.
|
||||
static NotificationLevelTypeTransformer? _instance;
|
||||
}
|
||||
|
91
mobile/openapi/lib/model/notification_type.dart
generated
Normal file
91
mobile/openapi/lib/model/notification_type.dart
generated
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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 NotificationType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const NotificationType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const jobFailed = NotificationType._(r'JobFailed');
|
||||
static const backupFailed = NotificationType._(r'BackupFailed');
|
||||
static const systemMessage = NotificationType._(r'SystemMessage');
|
||||
static const custom = NotificationType._(r'Custom');
|
||||
|
||||
/// List of all possible values in this [enum][NotificationType].
|
||||
static const values = <NotificationType>[
|
||||
jobFailed,
|
||||
backupFailed,
|
||||
systemMessage,
|
||||
custom,
|
||||
];
|
||||
|
||||
static NotificationType? fromJson(dynamic value) => NotificationTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<NotificationType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [NotificationType] to String,
|
||||
/// and [decode] dynamic data back to [NotificationType].
|
||||
class NotificationTypeTypeTransformer {
|
||||
factory NotificationTypeTypeTransformer() => _instance ??= const NotificationTypeTypeTransformer._();
|
||||
|
||||
const NotificationTypeTypeTransformer._();
|
||||
|
||||
String encode(NotificationType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a NotificationType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
NotificationType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'JobFailed': return NotificationType.jobFailed;
|
||||
case r'BackupFailed': return NotificationType.backupFailed;
|
||||
case r'SystemMessage': return NotificationType.systemMessage;
|
||||
case r'Custom': return NotificationType.custom;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [NotificationTypeTypeTransformer] instance.
|
||||
static NotificationTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
112
mobile/openapi/lib/model/notification_update_all_dto.dart
generated
Normal file
112
mobile/openapi/lib/model/notification_update_all_dto.dart
generated
Normal file
@ -0,0 +1,112 @@
|
||||
//
|
||||
// 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 NotificationUpdateAllDto {
|
||||
/// Returns a new [NotificationUpdateAllDto] instance.
|
||||
NotificationUpdateAllDto({
|
||||
this.ids = const [],
|
||||
this.readAt,
|
||||
});
|
||||
|
||||
List<String> ids;
|
||||
|
||||
DateTime? readAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateAllDto &&
|
||||
_deepEquality.equals(other.ids, ids) &&
|
||||
other.readAt == readAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(ids.hashCode) +
|
||||
(readAt == null ? 0 : readAt!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'NotificationUpdateAllDto[ids=$ids, readAt=$readAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'ids'] = this.ids;
|
||||
if (this.readAt != null) {
|
||||
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'readAt'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [NotificationUpdateAllDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static NotificationUpdateAllDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "NotificationUpdateAllDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return NotificationUpdateAllDto(
|
||||
ids: json[r'ids'] is Iterable
|
||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
readAt: mapDateTime(json, r'readAt', r''),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<NotificationUpdateAllDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationUpdateAllDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationUpdateAllDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, NotificationUpdateAllDto> mapFromJson(dynamic json) {
|
||||
final map = <String, NotificationUpdateAllDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = NotificationUpdateAllDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of NotificationUpdateAllDto-objects as value to a dart map
|
||||
static Map<String, List<NotificationUpdateAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<NotificationUpdateAllDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = NotificationUpdateAllDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'ids',
|
||||
};
|
||||
}
|
||||
|
102
mobile/openapi/lib/model/notification_update_dto.dart
generated
Normal file
102
mobile/openapi/lib/model/notification_update_dto.dart
generated
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// 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 NotificationUpdateDto {
|
||||
/// Returns a new [NotificationUpdateDto] instance.
|
||||
NotificationUpdateDto({
|
||||
this.readAt,
|
||||
});
|
||||
|
||||
DateTime? readAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateDto &&
|
||||
other.readAt == readAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(readAt == null ? 0 : readAt!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'NotificationUpdateDto[readAt=$readAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.readAt != null) {
|
||||
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'readAt'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [NotificationUpdateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static NotificationUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "NotificationUpdateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return NotificationUpdateDto(
|
||||
readAt: mapDateTime(json, r'readAt', r''),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<NotificationUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <NotificationUpdateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = NotificationUpdateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, NotificationUpdateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, NotificationUpdateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = NotificationUpdateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of NotificationUpdateDto-objects as value to a dart map
|
||||
static Map<String, List<NotificationUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<NotificationUpdateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = NotificationUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
12
mobile/openapi/lib/model/permission.dart
generated
12
mobile/openapi/lib/model/permission.dart
generated
@ -66,6 +66,10 @@ class Permission {
|
||||
static const memoryPeriodRead = Permission._(r'memory.read');
|
||||
static const memoryPeriodUpdate = Permission._(r'memory.update');
|
||||
static const memoryPeriodDelete = Permission._(r'memory.delete');
|
||||
static const notificationPeriodCreate = Permission._(r'notification.create');
|
||||
static const notificationPeriodRead = Permission._(r'notification.read');
|
||||
static const notificationPeriodUpdate = Permission._(r'notification.update');
|
||||
static const notificationPeriodDelete = Permission._(r'notification.delete');
|
||||
static const partnerPeriodCreate = Permission._(r'partner.create');
|
||||
static const partnerPeriodRead = Permission._(r'partner.read');
|
||||
static const partnerPeriodUpdate = Permission._(r'partner.update');
|
||||
@ -147,6 +151,10 @@ class Permission {
|
||||
memoryPeriodRead,
|
||||
memoryPeriodUpdate,
|
||||
memoryPeriodDelete,
|
||||
notificationPeriodCreate,
|
||||
notificationPeriodRead,
|
||||
notificationPeriodUpdate,
|
||||
notificationPeriodDelete,
|
||||
partnerPeriodCreate,
|
||||
partnerPeriodRead,
|
||||
partnerPeriodUpdate,
|
||||
@ -263,6 +271,10 @@ class PermissionTypeTransformer {
|
||||
case r'memory.read': return Permission.memoryPeriodRead;
|
||||
case r'memory.update': return Permission.memoryPeriodUpdate;
|
||||
case r'memory.delete': return Permission.memoryPeriodDelete;
|
||||
case r'notification.create': return Permission.notificationPeriodCreate;
|
||||
case r'notification.read': return Permission.notificationPeriodRead;
|
||||
case r'notification.update': return Permission.notificationPeriodUpdate;
|
||||
case r'notification.delete': return Permission.notificationPeriodDelete;
|
||||
case r'partner.create': return Permission.partnerPeriodCreate;
|
||||
case r'partner.read': return Permission.partnerPeriodRead;
|
||||
case r'partner.update': return Permission.partnerPeriodUpdate;
|
||||
|
@ -206,6 +206,141 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/notifications": {
|
||||
"post": {
|
||||
"operationId": "createNotification",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationCreateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications (Admin)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/notifications/templates/{name}": {
|
||||
"post": {
|
||||
"operationId": "getNotificationTemplateAdmin",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TemplateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TemplateResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications (Admin)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/notifications/test-email": {
|
||||
"post": {
|
||||
"operationId": "sendTestEmailAdmin",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TestEmailResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications (Admin)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/users": {
|
||||
"get": {
|
||||
"operationId": "searchUsersAdmin",
|
||||
@ -3485,15 +3620,224 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/notifications/admin/templates/{name}": {
|
||||
"post": {
|
||||
"operationId": "getNotificationTemplateAdmin",
|
||||
"/notifications": {
|
||||
"delete": {
|
||||
"operationId": "deleteNotifications",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationDeleteAllDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getNotifications",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "level",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationLevel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationType"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unread",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NotificationDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateNotifications",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationUpdateAllDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/notifications/{id}": {
|
||||
"delete": {
|
||||
"operationId": "deleteNotification",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getNotification",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateNotification",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@ -3502,7 +3846,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TemplateDto"
|
||||
"$ref": "#/components/schemas/NotificationUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3513,7 +3857,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TemplateResponseDto"
|
||||
"$ref": "#/components/schemas/NotificationDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3532,49 +3876,7 @@
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications (Admin)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/notifications/admin/test-email": {
|
||||
"post": {
|
||||
"operationId": "sendTestEmailAdmin",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TestEmailResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Notifications (Admin)"
|
||||
"Notifications"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -10326,6 +10628,157 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationCreateDto": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object"
|
||||
},
|
||||
"description": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"level": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NotificationLevel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"readAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NotificationType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"userId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationDeleteAllDto": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"level": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NotificationLevel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"readAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NotificationType"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"id",
|
||||
"level",
|
||||
"title",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationLevel": {
|
||||
"enum": [
|
||||
"success",
|
||||
"error",
|
||||
"warning",
|
||||
"info"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NotificationType": {
|
||||
"enum": [
|
||||
"JobFailed",
|
||||
"BackupFailed",
|
||||
"SystemMessage",
|
||||
"Custom"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NotificationUpdateAllDto": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"readAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationUpdateDto": {
|
||||
"properties": {
|
||||
"readAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"OAuthAuthorizeResponseDto": {
|
||||
"properties": {
|
||||
"url": {
|
||||
@ -10600,6 +11053,10 @@
|
||||
"memory.read",
|
||||
"memory.update",
|
||||
"memory.delete",
|
||||
"notification.create",
|
||||
"notification.read",
|
||||
"notification.update",
|
||||
"notification.delete",
|
||||
"partner.create",
|
||||
"partner.read",
|
||||
"partner.update",
|
||||
|
@ -39,6 +39,48 @@ export type ActivityCreateDto = {
|
||||
export type ActivityStatisticsResponseDto = {
|
||||
comments: number;
|
||||
};
|
||||
export type NotificationCreateDto = {
|
||||
data?: object;
|
||||
description?: string | null;
|
||||
level?: NotificationLevel;
|
||||
readAt?: string | null;
|
||||
title: string;
|
||||
"type"?: NotificationType;
|
||||
userId: string;
|
||||
};
|
||||
export type NotificationDto = {
|
||||
createdAt: string;
|
||||
data?: object;
|
||||
description?: string;
|
||||
id: string;
|
||||
level: NotificationLevel;
|
||||
readAt?: string;
|
||||
title: string;
|
||||
"type": NotificationType;
|
||||
};
|
||||
export type TemplateDto = {
|
||||
template: string;
|
||||
};
|
||||
export type TemplateResponseDto = {
|
||||
html: string;
|
||||
name: string;
|
||||
};
|
||||
export type SystemConfigSmtpTransportDto = {
|
||||
host: string;
|
||||
ignoreCert: boolean;
|
||||
password: string;
|
||||
port: number;
|
||||
username: string;
|
||||
};
|
||||
export type SystemConfigSmtpDto = {
|
||||
enabled: boolean;
|
||||
"from": string;
|
||||
replyTo: string;
|
||||
transport: SystemConfigSmtpTransportDto;
|
||||
};
|
||||
export type TestEmailResponseDto = {
|
||||
messageId: string;
|
||||
};
|
||||
export type UserLicense = {
|
||||
activatedAt: string;
|
||||
activationKey: string;
|
||||
@ -661,28 +703,15 @@ export type MemoryUpdateDto = {
|
||||
memoryAt?: string;
|
||||
seenAt?: string;
|
||||
};
|
||||
export type TemplateDto = {
|
||||
template: string;
|
||||
export type NotificationDeleteAllDto = {
|
||||
ids: string[];
|
||||
};
|
||||
export type TemplateResponseDto = {
|
||||
html: string;
|
||||
name: string;
|
||||
export type NotificationUpdateAllDto = {
|
||||
ids: string[];
|
||||
readAt?: string | null;
|
||||
};
|
||||
export type SystemConfigSmtpTransportDto = {
|
||||
host: string;
|
||||
ignoreCert: boolean;
|
||||
password: string;
|
||||
port: number;
|
||||
username: string;
|
||||
};
|
||||
export type SystemConfigSmtpDto = {
|
||||
enabled: boolean;
|
||||
"from": string;
|
||||
replyTo: string;
|
||||
transport: SystemConfigSmtpTransportDto;
|
||||
};
|
||||
export type TestEmailResponseDto = {
|
||||
messageId: string;
|
||||
export type NotificationUpdateDto = {
|
||||
readAt?: string | null;
|
||||
};
|
||||
export type OAuthConfigDto = {
|
||||
codeChallenge?: string;
|
||||
@ -1453,6 +1482,43 @@ export function deleteActivity({ id }: {
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function createNotification({ notificationCreateDto }: {
|
||||
notificationCreateDto: NotificationCreateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: NotificationDto;
|
||||
}>("/admin/notifications", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: notificationCreateDto
|
||||
})));
|
||||
}
|
||||
export function getNotificationTemplateAdmin({ name, templateDto }: {
|
||||
name: string;
|
||||
templateDto: TemplateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TemplateResponseDto;
|
||||
}>(`/admin/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: templateDto
|
||||
})));
|
||||
}
|
||||
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
||||
systemConfigSmtpDto: SystemConfigSmtpDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TestEmailResponseDto;
|
||||
}>("/admin/notifications/test-email", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: systemConfigSmtpDto
|
||||
})));
|
||||
}
|
||||
export function searchUsersAdmin({ withDeleted }: {
|
||||
withDeleted?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@ -2321,29 +2387,71 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
|
||||
body: bulkIdsDto
|
||||
})));
|
||||
}
|
||||
export function getNotificationTemplateAdmin({ name, templateDto }: {
|
||||
name: string;
|
||||
templateDto: TemplateDto;
|
||||
export function deleteNotifications({ notificationDeleteAllDto }: {
|
||||
notificationDeleteAllDto: NotificationDeleteAllDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TemplateResponseDto;
|
||||
}>(`/notifications/admin/templates/${encodeURIComponent(name)}`, oazapfts.json({
|
||||
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: templateDto
|
||||
method: "DELETE",
|
||||
body: notificationDeleteAllDto
|
||||
})));
|
||||
}
|
||||
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
||||
systemConfigSmtpDto: SystemConfigSmtpDto;
|
||||
export function getNotifications({ id, level, $type, unread }: {
|
||||
id?: string;
|
||||
level?: NotificationLevel;
|
||||
$type?: NotificationType;
|
||||
unread?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TestEmailResponseDto;
|
||||
}>("/notifications/admin/test-email", oazapfts.json({
|
||||
data: NotificationDto[];
|
||||
}>(`/notifications${QS.query(QS.explode({
|
||||
id,
|
||||
level,
|
||||
"type": $type,
|
||||
unread
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function updateNotifications({ notificationUpdateAllDto }: {
|
||||
notificationUpdateAllDto: NotificationUpdateAllDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: systemConfigSmtpDto
|
||||
method: "PUT",
|
||||
body: notificationUpdateAllDto
|
||||
})));
|
||||
}
|
||||
export function deleteNotification({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/notifications/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function getNotification({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: NotificationDto;
|
||||
}>(`/notifications/${encodeURIComponent(id)}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function updateNotification({ id, notificationUpdateDto }: {
|
||||
id: string;
|
||||
notificationUpdateDto: NotificationUpdateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: NotificationDto;
|
||||
}>(`/notifications/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: notificationUpdateDto
|
||||
})));
|
||||
}
|
||||
export function startOAuth({ oAuthConfigDto }: {
|
||||
@ -3452,6 +3560,18 @@ export enum UserAvatarColor {
|
||||
Gray = "gray",
|
||||
Amber = "amber"
|
||||
}
|
||||
export enum NotificationLevel {
|
||||
Success = "success",
|
||||
Error = "error",
|
||||
Warning = "warning",
|
||||
Info = "info"
|
||||
}
|
||||
export enum NotificationType {
|
||||
JobFailed = "JobFailed",
|
||||
BackupFailed = "BackupFailed",
|
||||
SystemMessage = "SystemMessage",
|
||||
Custom = "Custom"
|
||||
}
|
||||
export enum UserStatus {
|
||||
Active = "active",
|
||||
Removing = "removing",
|
||||
@ -3526,6 +3646,10 @@ export enum Permission {
|
||||
MemoryRead = "memory.read",
|
||||
MemoryUpdate = "memory.update",
|
||||
MemoryDelete = "memory.delete",
|
||||
NotificationCreate = "notification.create",
|
||||
NotificationRead = "notification.read",
|
||||
NotificationUpdate = "notification.update",
|
||||
NotificationDelete = "notification.delete",
|
||||
PartnerCreate = "partner.create",
|
||||
PartnerRead = "partner.read",
|
||||
PartnerUpdate = "partner.update",
|
||||
|
@ -14,6 +14,7 @@ import { LibraryController } from 'src/controllers/library.controller';
|
||||
import { MapController } from 'src/controllers/map.controller';
|
||||
import { MemoryController } from 'src/controllers/memory.controller';
|
||||
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
||||
import { NotificationController } from 'src/controllers/notification.controller';
|
||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||
import { PartnerController } from 'src/controllers/partner.controller';
|
||||
import { PersonController } from 'src/controllers/person.controller';
|
||||
@ -47,6 +48,7 @@ export const controllers = [
|
||||
LibraryController,
|
||||
MapController,
|
||||
MemoryController,
|
||||
NotificationController,
|
||||
NotificationAdminController,
|
||||
OAuthController,
|
||||
PartnerController,
|
||||
|
@ -1,16 +1,28 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
|
||||
import {
|
||||
NotificationCreateDto,
|
||||
NotificationDto,
|
||||
TemplateDto,
|
||||
TemplateResponseDto,
|
||||
TestEmailResponseDto,
|
||||
} from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||
|
||||
@ApiTags('Notifications (Admin)')
|
||||
@Controller('notifications/admin')
|
||||
@Controller('admin/notifications')
|
||||
export class NotificationAdminController {
|
||||
constructor(private service: NotificationService) {}
|
||||
constructor(private service: NotificationAdminService) {}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise<NotificationDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Post('test-email')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
60
server/src/controllers/notification.controller.ts
Normal file
60
server/src/controllers/notification.controller.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
NotificationDeleteAllDto,
|
||||
NotificationDto,
|
||||
NotificationSearchDto,
|
||||
NotificationUpdateAllDto,
|
||||
NotificationUpdateDto,
|
||||
} from 'src/dtos/notification.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Notifications')
|
||||
@Controller('notifications')
|
||||
export class NotificationController {
|
||||
constructor(private service: NotificationService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.NOTIFICATION_READ })
|
||||
getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Put()
|
||||
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
|
||||
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
|
||||
return this.service.updateAll(auth, dto);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
|
||||
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.NOTIFICATION_READ })
|
||||
getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
|
||||
updateNotification(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: NotificationUpdateDto,
|
||||
): Promise<NotificationDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
|
||||
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
}
|
@ -333,6 +333,7 @@ export const columns = {
|
||||
],
|
||||
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
|
||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
|
||||
syncAsset: [
|
||||
'id',
|
||||
'ownerId',
|
||||
|
18
server/src/db.d.ts
vendored
18
server/src/db.d.ts
vendored
@ -11,6 +11,8 @@ import {
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
MemoryType,
|
||||
NotificationLevel,
|
||||
NotificationType,
|
||||
Permission,
|
||||
SharedLinkType,
|
||||
SourceType,
|
||||
@ -263,6 +265,21 @@ export interface Memories {
|
||||
updateId: Generated<string>;
|
||||
}
|
||||
|
||||
export interface Notifications {
|
||||
id: Generated<string>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
updateId: Generated<string>;
|
||||
userId: string;
|
||||
level: Generated<NotificationLevel>;
|
||||
type: NotificationType;
|
||||
title: string;
|
||||
description: string | null;
|
||||
data: any | null;
|
||||
readAt: Timestamp | null;
|
||||
}
|
||||
|
||||
export interface MemoriesAssetsAssets {
|
||||
assetsId: string;
|
||||
memoriesId: string;
|
||||
@ -463,6 +480,7 @@ export interface DB {
|
||||
memories: Memories;
|
||||
memories_assets_assets: MemoriesAssetsAssets;
|
||||
migrations: Migrations;
|
||||
notifications: Notifications;
|
||||
move_history: MoveHistory;
|
||||
naturalearth_countries: NaturalearthCountries;
|
||||
partners_audit: PartnersAudit;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsString } from 'class-validator';
|
||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TestEmailResponseDto {
|
||||
messageId!: string;
|
||||
@ -11,3 +14,106 @@ export class TemplateDto {
|
||||
@IsString()
|
||||
template!: string;
|
||||
}
|
||||
|
||||
export class NotificationDto {
|
||||
id!: string;
|
||||
@ValidateDate()
|
||||
createdAt!: Date;
|
||||
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||
level!: NotificationLevel;
|
||||
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||
type!: NotificationType;
|
||||
title!: string;
|
||||
description?: string;
|
||||
data?: any;
|
||||
readAt?: Date;
|
||||
}
|
||||
|
||||
export class NotificationSearchDto {
|
||||
@Optional()
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
|
||||
@IsEnum(NotificationLevel)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||
level?: NotificationLevel;
|
||||
|
||||
@IsEnum(NotificationType)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||
type?: NotificationType;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
unread?: boolean;
|
||||
}
|
||||
|
||||
export class NotificationCreateDto {
|
||||
@Optional()
|
||||
@IsEnum(NotificationLevel)
|
||||
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||
level?: NotificationLevel;
|
||||
|
||||
@IsEnum(NotificationType)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||
type?: NotificationType;
|
||||
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@Optional({ nullable: true })
|
||||
description?: string | null;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
data?: any;
|
||||
|
||||
@ValidateDate({ optional: true, nullable: true })
|
||||
readAt?: Date | null;
|
||||
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
export class NotificationUpdateDto {
|
||||
@ValidateDate({ optional: true, nullable: true })
|
||||
readAt?: Date | null;
|
||||
}
|
||||
|
||||
export class NotificationUpdateAllDto {
|
||||
@ValidateUUID({ each: true, optional: true })
|
||||
ids!: string[];
|
||||
|
||||
@ValidateDate({ optional: true, nullable: true })
|
||||
readAt?: Date | null;
|
||||
}
|
||||
|
||||
export class NotificationDeleteAllDto {
|
||||
@ValidateUUID({ each: true })
|
||||
ids!: string[];
|
||||
}
|
||||
|
||||
export type MapNotification = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updateId?: string;
|
||||
level: NotificationLevel;
|
||||
type: NotificationType;
|
||||
data: any | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
readAt: Date | null;
|
||||
};
|
||||
export const mapNotification = (notification: MapNotification): NotificationDto => {
|
||||
return {
|
||||
id: notification.id,
|
||||
createdAt: notification.createdAt,
|
||||
level: notification.level,
|
||||
type: notification.type,
|
||||
title: notification.title,
|
||||
description: notification.description ?? undefined,
|
||||
data: notification.data ?? undefined,
|
||||
readAt: notification.readAt ?? undefined,
|
||||
};
|
||||
};
|
||||
|
@ -126,6 +126,11 @@ export enum Permission {
|
||||
MEMORY_UPDATE = 'memory.update',
|
||||
MEMORY_DELETE = 'memory.delete',
|
||||
|
||||
NOTIFICATION_CREATE = 'notification.create',
|
||||
NOTIFICATION_READ = 'notification.read',
|
||||
NOTIFICATION_UPDATE = 'notification.update',
|
||||
NOTIFICATION_DELETE = 'notification.delete',
|
||||
|
||||
PARTNER_CREATE = 'partner.create',
|
||||
PARTNER_READ = 'partner.read',
|
||||
PARTNER_UPDATE = 'partner.update',
|
||||
@ -515,6 +520,7 @@ export enum JobName {
|
||||
NOTIFY_SIGNUP = 'notify-signup',
|
||||
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
||||
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
||||
NOTIFICATIONS_CLEANUP = 'notifications-cleanup',
|
||||
SEND_EMAIL = 'notification-send-email',
|
||||
|
||||
// Version check
|
||||
@ -580,3 +586,17 @@ export enum SyncEntityType {
|
||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
Warning = 'warning',
|
||||
Info = 'info',
|
||||
}
|
||||
|
||||
export enum NotificationType {
|
||||
JobFailed = 'JobFailed',
|
||||
BackupFailed = 'BackupFailed',
|
||||
SystemMessage = 'SystemMessage',
|
||||
Custom = 'Custom',
|
||||
}
|
||||
|
@ -157,6 +157,15 @@ where
|
||||
and "memories"."ownerId" = $2
|
||||
and "memories"."deletedAt" is null
|
||||
|
||||
-- AccessRepository.notification.checkOwnerAccess
|
||||
select
|
||||
"notifications"."id"
|
||||
from
|
||||
"notifications"
|
||||
where
|
||||
"notifications"."id" in ($1)
|
||||
and "notifications"."userId" = $2
|
||||
|
||||
-- AccessRepository.person.checkOwnerAccess
|
||||
select
|
||||
"person"."id"
|
||||
|
58
server/src/queries/notification.repository.sql
Normal file
58
server/src/queries/notification.repository.sql
Normal file
@ -0,0 +1,58 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- NotificationRepository.cleanup
|
||||
delete from "notifications"
|
||||
where
|
||||
(
|
||||
(
|
||||
"deletedAt" is not null
|
||||
and "deletedAt" < $1
|
||||
)
|
||||
or (
|
||||
"readAt" > $2
|
||||
and "createdAt" < $3
|
||||
)
|
||||
or (
|
||||
"readAt" = $4
|
||||
and "createdAt" < $5
|
||||
)
|
||||
)
|
||||
|
||||
-- NotificationRepository.search
|
||||
select
|
||||
"id",
|
||||
"createdAt",
|
||||
"level",
|
||||
"type",
|
||||
"title",
|
||||
"description",
|
||||
"data",
|
||||
"readAt"
|
||||
from
|
||||
"notifications"
|
||||
where
|
||||
"userId" = $1
|
||||
and "deletedAt" is null
|
||||
order by
|
||||
"createdAt" desc
|
||||
|
||||
-- NotificationRepository.search (unread)
|
||||
select
|
||||
"id",
|
||||
"createdAt",
|
||||
"level",
|
||||
"type",
|
||||
"title",
|
||||
"description",
|
||||
"data",
|
||||
"readAt"
|
||||
from
|
||||
"notifications"
|
||||
where
|
||||
(
|
||||
"userId" = $1
|
||||
and "readAt" is null
|
||||
)
|
||||
and "deletedAt" is null
|
||||
order by
|
||||
"createdAt" desc
|
@ -279,6 +279,26 @@ class AuthDeviceAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, notificationIds: Set<string>) {
|
||||
if (notificationIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('notifications')
|
||||
.select('notifications.id')
|
||||
.where('notifications.id', 'in', [...notificationIds])
|
||||
.where('notifications.userId', '=', userId)
|
||||
.execute()
|
||||
.then((stacks) => new Set(stacks.map((stack) => stack.id)));
|
||||
}
|
||||
}
|
||||
|
||||
class StackAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@ -426,6 +446,7 @@ export class AccessRepository {
|
||||
asset: AssetAccess;
|
||||
authDevice: AuthDeviceAccess;
|
||||
memory: MemoryAccess;
|
||||
notification: NotificationAccess;
|
||||
person: PersonAccess;
|
||||
partner: PartnerAccess;
|
||||
stack: StackAccess;
|
||||
@ -438,6 +459,7 @@ export class AccessRepository {
|
||||
this.asset = new AssetAccess(db);
|
||||
this.authDevice = new AuthDeviceAccess(db);
|
||||
this.memory = new MemoryAccess(db);
|
||||
this.notification = new NotificationAccess(db);
|
||||
this.person = new PersonAccess(db);
|
||||
this.partner = new PartnerAccess(db);
|
||||
this.stack = new StackAccess(db);
|
||||
|
@ -14,6 +14,7 @@ import { SystemConfig } from 'src/config';
|
||||
import { EventConfig } from 'src/decorators';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
@ -64,6 +65,7 @@ type EventMap = {
|
||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||
|
||||
'job.start': [QueueName, JobItem];
|
||||
'job.failed': [{ job: JobItem; error: Error | any }];
|
||||
|
||||
// session events
|
||||
'session.delete': [{ sessionId: string }];
|
||||
@ -104,6 +106,7 @@ export interface ClientEventMap {
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
on_new_release: [ReleaseNotification];
|
||||
on_notification: [NotificationDto];
|
||||
on_session_delete: [string];
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
@ -55,6 +56,7 @@ export const repositories = [
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
DownloadRepository,
|
||||
EmailRepository,
|
||||
EventRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
@ -65,7 +67,7 @@ export const repositories = [
|
||||
MemoryRepository,
|
||||
MetadataRepository,
|
||||
MoveRepository,
|
||||
EmailRepository,
|
||||
NotificationRepository,
|
||||
OAuthRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
|
103
server/src/repositories/notification.repository.ts
Normal file
103
server/src/repositories/notification.repository.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, Notifications } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { NotificationSearchDto } from 'src/dtos/notification.dto';
|
||||
|
||||
export class NotificationRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
cleanup() {
|
||||
return this.db
|
||||
.deleteFrom('notifications')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
// remove soft-deleted notifications
|
||||
eb.and([eb('deletedAt', 'is not', null), eb('deletedAt', '<', DateTime.now().minus({ days: 3 }).toJSDate())]),
|
||||
|
||||
// remove old, read notifications
|
||||
eb.and([
|
||||
// keep recently read messages around for a few days
|
||||
eb('readAt', '>', DateTime.now().minus({ days: 2 }).toJSDate()),
|
||||
eb('createdAt', '<', DateTime.now().minus({ days: 15 }).toJSDate()),
|
||||
]),
|
||||
|
||||
eb.and([
|
||||
// remove super old, unread notifications
|
||||
eb('readAt', '=', null),
|
||||
eb('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}] }, { name: 'unread', params: [DummyValue.UUID, { unread: true }] })
|
||||
search(userId: string, dto: NotificationSearchDto) {
|
||||
return this.db
|
||||
.selectFrom('notifications')
|
||||
.select(columns.notification)
|
||||
.where((qb) =>
|
||||
qb.and({
|
||||
userId,
|
||||
id: dto.id,
|
||||
level: dto.level,
|
||||
type: dto.type,
|
||||
readAt: dto.unread ? null : undefined,
|
||||
}),
|
||||
)
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(notification: Insertable<Notifications>) {
|
||||
return this.db
|
||||
.insertInto('notifications')
|
||||
.values(notification)
|
||||
.returning(columns.notification)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
return this.db
|
||||
.selectFrom('notifications')
|
||||
.select(columns.notification)
|
||||
.where('id', '=', id)
|
||||
.where('deletedAt', 'is not', null)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
update(id: string, notification: Updateable<Notifications>) {
|
||||
return this.db
|
||||
.updateTable('notifications')
|
||||
.set(notification)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('id', '=', id)
|
||||
.returning(columns.notification)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async updateAll(ids: string[], notification: Updateable<Notifications>) {
|
||||
await this.db.updateTable('notifications').set(notification).where('id', 'in', ids).execute();
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||
.where('id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAll(ids: string[]) {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||
.where('id', 'in', ids)
|
||||
.execute();
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
||||
import { MoveTable } from 'src/schema/tables/move.table';
|
||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||
import { NotificationTable } from 'src/schema/tables/notification.table';
|
||||
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
@ -76,6 +77,7 @@ export class ImmichDatabase {
|
||||
MemoryTable,
|
||||
MoveTable,
|
||||
NaturalEarthCountriesTable,
|
||||
NotificationTable,
|
||||
PartnerAuditTable,
|
||||
PartnerTable,
|
||||
PersonTable,
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), "userId" uuid, "level" character varying NOT NULL DEFAULT 'info', "type" character varying NOT NULL DEFAULT 'info', "data" jsonb, "title" character varying NOT NULL, "description" text, "readAt" timestamp with time zone);`.execute(db);
|
||||
await sql`ALTER TABLE "notifications" ADD CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`ALTER TABLE "notifications" ADD CONSTRAINT "FK_692a909ee0fa9383e7859f9b406" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_notifications_update_id" ON "notifications" ("updateId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_692a909ee0fa9383e7859f9b40" ON "notifications" ("userId")`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "notifications_updated_at"
|
||||
BEFORE UPDATE ON "notifications"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "notifications_updated_at" ON "notifications";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_notifications_update_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_692a909ee0fa9383e7859f9b40";`.execute(db);
|
||||
await sql`ALTER TABLE "notifications" DROP CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a";`.execute(db);
|
||||
await sql`ALTER TABLE "notifications" DROP CONSTRAINT "FK_692a909ee0fa9383e7859f9b406";`.execute(db);
|
||||
await sql`DROP TABLE "notifications";`.execute(db);
|
||||
}
|
52
server/src/schema/tables/notification.table.ts
Normal file
52
server/src/schema/tables/notification.table.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
ForeignKeyColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('notifications')
|
||||
@UpdatedAtTrigger('notifications_updated_at')
|
||||
export class NotificationTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
|
||||
@UpdateIdColumn({ indexName: 'IDX_notifications_update_id' })
|
||||
updateId?: string;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
userId!: string;
|
||||
|
||||
@Column({ default: NotificationLevel.Info })
|
||||
level!: NotificationLevel;
|
||||
|
||||
@Column({ default: NotificationLevel.Info })
|
||||
type!: NotificationType;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
data!: any | null;
|
||||
|
||||
@Column()
|
||||
title!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||
readAt?: Date | null;
|
||||
}
|
@ -142,52 +142,55 @@ describe(BackupService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
||||
});
|
||||
|
||||
it('should run a database backup successfully', async () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rename file on success', async () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(mocks.storage.rename).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if pg_dumpall fails', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||
});
|
||||
|
||||
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if gzip fails', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1');
|
||||
});
|
||||
|
||||
it('should fail if write stream fails', async () => {
|
||||
mocks.storage.createWriteStream.mockImplementation(() => {
|
||||
throw new Error('error');
|
||||
});
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||
});
|
||||
|
||||
it('should fail if rename fails', async () => {
|
||||
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||
});
|
||||
|
||||
it('should ignore unlink failing and still return failed job status', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||
expect(mocks.storage.unlink).toHaveBeenCalled();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
|
||||
it.each`
|
||||
postgresVersion | expectedVersion
|
||||
${'14.10'} | ${14}
|
||||
|
@ -174,7 +174,7 @@ export class BackupService extends BaseService {
|
||||
await this.storageRepository
|
||||
.unlink(backupFilePath)
|
||||
.catch((error) => this.logger.error('Failed to delete failed backup file', error));
|
||||
return JobStatus.FAILED;
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.log(`Database Backup Success`);
|
||||
|
@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
@ -80,6 +81,7 @@ export class BaseService {
|
||||
protected memoryRepository: MemoryRepository,
|
||||
protected metadataRepository: MetadataRepository,
|
||||
protected moveRepository: MoveRepository,
|
||||
protected notificationRepository: NotificationRepository,
|
||||
protected oauthRepository: OAuthRepository,
|
||||
protected partnerRepository: PartnerRepository,
|
||||
protected personRepository: PersonRepository,
|
||||
|
@ -17,6 +17,7 @@ import { MapService } from 'src/services/map.service';
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
@ -60,6 +61,7 @@ export const services = [
|
||||
MemoryService,
|
||||
MetadataService,
|
||||
NotificationService,
|
||||
NotificationAdminService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
SearchService,
|
||||
|
@ -215,11 +215,7 @@ export class JobService extends BaseService {
|
||||
await this.onDone(job);
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(
|
||||
`Unable to run job handler (${queueName}/${job.name}): ${error}`,
|
||||
error?.stack,
|
||||
JSON.stringify(job.data),
|
||||
);
|
||||
await this.eventRepository.emit('job.failed', { job, error });
|
||||
} finally {
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
||||
}
|
||||
|
111
server/src/services/notification-admin.service.spec.ts
Normal file
111
server/src/services/notification-admin.service.spec.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const smtpTransport = Object.freeze<SystemConfig>({
|
||||
...defaults,
|
||||
notifications: {
|
||||
smtp: {
|
||||
...defaults.notifications.smtp,
|
||||
enabled: true,
|
||||
transport: {
|
||||
ignoreCert: false,
|
||||
host: 'localhost',
|
||||
port: 587,
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe(NotificationService.name, () => {
|
||||
let sut: NotificationService;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(NotificationService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('sendTestEmail', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should throw error if smtp validation fails', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockRejectedValue('');
|
||||
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow(
|
||||
'Failed to verify SMTP configuration',
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to default domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to external domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email with replyTo', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(
|
||||
sut.sendTestEmail('', { ...smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
||||
).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: smtpTransport.notifications.smtp.transport,
|
||||
replyTo: 'demo@immich.app',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
120
server/src/services/notification-admin.service.ts
Normal file
120
server/src/services/notification-admin.service.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { mapNotification, NotificationCreateDto } from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationAdminService extends BaseService {
|
||||
async create(auth: AuthDto, dto: NotificationCreateDto) {
|
||||
const item = await this.notificationRepository.create({
|
||||
userId: dto.userId,
|
||||
type: dto.type ?? NotificationType.Custom,
|
||||
level: dto.level ?? NotificationLevel.Info,
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
data: dto.data,
|
||||
});
|
||||
|
||||
return mapNotification(item);
|
||||
}
|
||||
|
||||
async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {
|
||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.emailRepository.verifySmtp(dto.transport);
|
||||
} catch (error) {
|
||||
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
|
||||
}
|
||||
|
||||
const { server } = await this.getConfig({ withCache: false });
|
||||
const { html, text } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
displayName: user.name,
|
||||
},
|
||||
customTemplate: tempTemplate!,
|
||||
});
|
||||
const { messageId } = await this.emailRepository.sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Test email from Immich',
|
||||
html,
|
||||
text,
|
||||
from: dto.from,
|
||||
replyTo: dto.replyTo || dto.from,
|
||||
smtp: dto.transport,
|
||||
});
|
||||
|
||||
return { messageId };
|
||||
}
|
||||
|
||||
async getTemplate(name: EmailTemplate, customTemplate: string) {
|
||||
const { server, templates } = await this.getConfig({ withCache: false });
|
||||
|
||||
let templateResponse = '';
|
||||
|
||||
switch (name) {
|
||||
case EmailTemplate.WELCOME: {
|
||||
const { html: _welcomeHtml } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.WELCOME,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
displayName: 'John Doe',
|
||||
username: 'john@doe.com',
|
||||
password: 'thisIsAPassword123',
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.welcomeTemplate,
|
||||
});
|
||||
|
||||
templateResponse = _welcomeHtml;
|
||||
break;
|
||||
}
|
||||
case EmailTemplate.ALBUM_UPDATE: {
|
||||
const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_UPDATE,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
albumId: '1',
|
||||
albumName: 'Favorite Photos',
|
||||
recipientName: 'Jane Doe',
|
||||
cid: undefined,
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||
});
|
||||
templateResponse = _updateAlbumHtml;
|
||||
break;
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_INVITE: {
|
||||
const { html } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_INVITE,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
albumId: '1',
|
||||
albumName: "John Doe's Favorites",
|
||||
senderName: 'John Doe',
|
||||
recipientName: 'Jane Doe',
|
||||
cid: undefined,
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||
});
|
||||
templateResponse = html;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
templateResponse = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { name, html: templateResponse };
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import { defaults, SystemConfig } from 'src/config';
|
||||
import { AlbumUser } from 'src/database';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { INotifyAlbumUpdateJob } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
@ -241,82 +240,6 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTestEmail', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should throw error if smtp validation fails', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockRejectedValue('');
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow(
|
||||
'Failed to verify SMTP configuration',
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to default domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to external domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email with replyTo', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(
|
||||
sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
||||
).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
replyTo: 'demo@immich.app',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserSignup', () => {
|
||||
it('should skip if user could not be found', async () => {
|
||||
await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
|
@ -1,7 +1,24 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
mapNotification,
|
||||
NotificationDeleteAllDto,
|
||||
NotificationDto,
|
||||
NotificationSearchDto,
|
||||
NotificationUpdateAllDto,
|
||||
NotificationUpdateDto,
|
||||
} from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import {
|
||||
AssetFileType,
|
||||
JobName,
|
||||
JobStatus,
|
||||
NotificationLevel,
|
||||
NotificationType,
|
||||
Permission,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@ -15,6 +32,80 @@ import { getPreferences } from 'src/utils/preferences';
|
||||
export class NotificationService extends BaseService {
|
||||
private static albumUpdateEmailDelayMs = 300_000;
|
||||
|
||||
async search(auth: AuthDto, dto: NotificationSearchDto): Promise<NotificationDto[]> {
|
||||
const items = await this.notificationRepository.search(auth.user.id, dto);
|
||||
return items.map((item) => mapNotification(item));
|
||||
}
|
||||
|
||||
async updateAll(auth: AuthDto, dto: NotificationUpdateAllDto) {
|
||||
await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_UPDATE });
|
||||
await this.notificationRepository.updateAll(dto.ids, {
|
||||
readAt: dto.readAt,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, dto: NotificationDeleteAllDto) {
|
||||
await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_DELETE });
|
||||
await this.notificationRepository.deleteAll(dto.ids);
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string) {
|
||||
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_READ });
|
||||
const item = await this.notificationRepository.get(id);
|
||||
if (!item) {
|
||||
throw new BadRequestException('Notification not found');
|
||||
}
|
||||
return mapNotification(item);
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: NotificationUpdateDto) {
|
||||
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_UPDATE });
|
||||
const item = await this.notificationRepository.update(id, {
|
||||
readAt: dto.readAt,
|
||||
});
|
||||
return mapNotification(item);
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string) {
|
||||
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_DELETE });
|
||||
await this.notificationRepository.delete(id);
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.NOTIFICATIONS_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||
async onNotificationsCleanup() {
|
||||
await this.notificationRepository.cleanup();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'job.failed' })
|
||||
async onJobFailed({ job, error }: ArgOf<'job.failed'>) {
|
||||
const admin = await this.userRepository.getAdmin();
|
||||
if (!admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data));
|
||||
|
||||
switch (job.name) {
|
||||
case JobName.BACKUP_DATABASE: {
|
||||
const errorMessage = error instanceof Error ? error.message : error;
|
||||
const item = await this.notificationRepository.create({
|
||||
userId: admin.id,
|
||||
type: NotificationType.JobFailed,
|
||||
level: NotificationLevel.Error,
|
||||
title: 'Job Failed',
|
||||
description: `Job ${[job.name]} failed with error: ${errorMessage}`,
|
||||
});
|
||||
|
||||
this.eventRepository.clientSend('on_notification', admin.id, mapNotification(item));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update' })
|
||||
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
||||
this.eventRepository.clientBroadcast('on_config_update');
|
||||
|
@ -297,6 +297,10 @@ export type JobItem =
|
||||
// Metadata Extraction
|
||||
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
||||
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
||||
|
||||
// Notifications
|
||||
| { name: JobName.NOTIFICATIONS_CLEANUP; data?: IBaseJob }
|
||||
|
||||
// Sidecar Scanning
|
||||
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
|
||||
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
|
||||
|
@ -221,6 +221,12 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.NOTIFICATION_READ:
|
||||
case Permission.NOTIFICATION_UPDATE:
|
||||
case Permission.NOTIFICATION_DELETE: {
|
||||
return access.notification.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.TAG_ASSET:
|
||||
case Permission.TAG_READ:
|
||||
case Permission.TAG_UPDATE:
|
||||
|
@ -13,9 +13,11 @@ import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
@ -42,10 +44,12 @@ type RepositoriesTypes = {
|
||||
config: ConfigRepository;
|
||||
crypto: CryptoRepository;
|
||||
database: DatabaseRepository;
|
||||
email: EmailRepository;
|
||||
job: JobRepository;
|
||||
user: UserRepository;
|
||||
logger: LoggingRepository;
|
||||
memory: MemoryRepository;
|
||||
notification: NotificationRepository;
|
||||
partner: PartnerRepository;
|
||||
person: PersonRepository;
|
||||
search: SearchRepository;
|
||||
@ -142,6 +146,11 @@ export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kys
|
||||
return new DatabaseRepository(db, new LoggingRepository(undefined, configRepo), configRepo);
|
||||
}
|
||||
|
||||
case 'email': {
|
||||
const logger = new LoggingRepository(undefined, new ConfigRepository());
|
||||
return new EmailRepository(logger);
|
||||
}
|
||||
|
||||
case 'logger': {
|
||||
const configMock = { getEnv: () => ({ noColor: false }) };
|
||||
return new LoggingRepository(undefined, configMock as ConfigRepository);
|
||||
@ -151,6 +160,10 @@ export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kys
|
||||
return new MemoryRepository(db);
|
||||
}
|
||||
|
||||
case 'notification': {
|
||||
return new NotificationRepository(db);
|
||||
}
|
||||
|
||||
case 'partner': {
|
||||
return new PartnerRepository(db);
|
||||
}
|
||||
@ -221,6 +234,10 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
||||
});
|
||||
}
|
||||
|
||||
case 'email': {
|
||||
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
|
||||
}
|
||||
|
||||
case 'job': {
|
||||
return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] });
|
||||
}
|
||||
@ -234,6 +251,10 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
||||
return automock(MemoryRepository);
|
||||
}
|
||||
|
||||
case 'notification': {
|
||||
return automock(NotificationRepository);
|
||||
}
|
||||
|
||||
case 'partner': {
|
||||
return automock(PartnerRepository);
|
||||
}
|
||||
@ -284,7 +305,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
||||
repositories.crypto || getRepositoryMock('crypto'),
|
||||
repositories.database || getRepositoryMock('database'),
|
||||
repositories.downloadRepository,
|
||||
repositories.email,
|
||||
repositories.email || getRepositoryMock('email'),
|
||||
repositories.event,
|
||||
repositories.job || getRepositoryMock('job'),
|
||||
repositories.library,
|
||||
@ -294,6 +315,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
||||
repositories.memory || getRepositoryMock('memory'),
|
||||
repositories.metadata,
|
||||
repositories.move,
|
||||
repositories.notification || getRepositoryMock('notification'),
|
||||
repositories.oauth,
|
||||
repositories.partner || getRepositoryMock('partner'),
|
||||
repositories.person || getRepositoryMock('person'),
|
||||
|
@ -0,0 +1,86 @@
|
||||
import { NotificationController } from 'src/controllers/notification.controller';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import request from 'supertest';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
|
||||
import { factory } from 'test/small.factory';
|
||||
|
||||
describe(NotificationController.name, () => {
|
||||
let realApp: TestControllerApp;
|
||||
let mockApp: TestControllerApp;
|
||||
|
||||
beforeEach(async () => {
|
||||
realApp = await createControllerTestApp({ authType: 'real' });
|
||||
mockApp = await createControllerTestApp({ authType: 'mock' });
|
||||
});
|
||||
|
||||
describe('GET /notifications', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(realApp.getHttpServer()).get('/notifications');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should call the service with an auth dto', async () => {
|
||||
const auth = factory.auth({ user: factory.user() });
|
||||
mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth);
|
||||
const service = mockApp.getMockedService(NotificationService);
|
||||
|
||||
const { status } = await request(mockApp.getHttpServer())
|
||||
.get('/notifications')
|
||||
.set('Authorization', `Bearer token`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(service.search).toHaveBeenCalledWith(auth, {});
|
||||
});
|
||||
|
||||
it(`should reject an invalid notification level`, async () => {
|
||||
const auth = factory.auth({ user: factory.user() });
|
||||
mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth);
|
||||
const service = mockApp.getMockedService(NotificationService);
|
||||
|
||||
const { status, body } = await request(mockApp.getHttpServer())
|
||||
.get(`/notifications`)
|
||||
.query({ level: 'invalid' })
|
||||
.set('Authorization', `Bearer token`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')]));
|
||||
expect(service.search).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /notifications', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(realApp.getHttpServer())
|
||||
.put(`/notifications`)
|
||||
.send({ ids: [], readAt: new Date().toISOString() });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /notifications/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(realApp.getHttpServer()).get(`/notifications/${factory.uuid()}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /notifications/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(realApp.getHttpServer())
|
||||
.put(`/notifications/${factory.uuid()}`)
|
||||
.send({ readAt: factory.date() });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await realApp.close();
|
||||
await mockApp.close();
|
||||
});
|
||||
});
|
@ -37,6 +37,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
notification: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
person: {
|
||||
checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
|
@ -314,4 +314,5 @@ export const factory = {
|
||||
sidecarWrite: assetSidecarWriteFactory,
|
||||
},
|
||||
uuid: newUuid,
|
||||
date: newDate,
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
@ -135,6 +136,7 @@ export type ServiceOverrides = {
|
||||
memory: MemoryRepository;
|
||||
metadata: MetadataRepository;
|
||||
move: MoveRepository;
|
||||
notification: NotificationRepository;
|
||||
oauth: OAuthRepository;
|
||||
partner: PartnerRepository;
|
||||
person: PersonRepository;
|
||||
@ -202,6 +204,7 @@ export const newTestService = <T extends BaseService>(
|
||||
memory: automock(MemoryRepository),
|
||||
metadata: newMetadataRepositoryMock(),
|
||||
move: automock(MoveRepository, { strict: false }),
|
||||
notification: automock(NotificationRepository),
|
||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
person: newPersonRepositoryMock(),
|
||||
@ -250,6 +253,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.memory || (mocks.memory as As<MemoryRepository>),
|
||||
overrides.metadata || (mocks.metadata as As<MetadataRepository>),
|
||||
overrides.move || (mocks.move as As<MoveRepository>),
|
||||
overrides.notification || (mocks.notification as As<NotificationRepository>),
|
||||
overrides.oauth || (mocks.oauth as As<OAuthRepository>),
|
||||
overrides.partner || (mocks.partner as As<PartnerRepository>),
|
||||
overrides.person || (mocks.person as As<PersonRepository>),
|
||||
|
@ -8,6 +8,7 @@
|
||||
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
||||
import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import NotificationPanel from '$lib/components/shared-components/navigation-bar/notification-panel.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/stores/auth-manager.svelte';
|
||||
@ -18,13 +19,14 @@
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { mdiBellBadge, mdiBellOutline, mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ThemeButton from '../theme-button.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import AccountInfoPanel from './account-info-panel.svelte';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
|
||||
interface Props {
|
||||
showUploadButton?: boolean;
|
||||
@ -36,7 +38,9 @@
|
||||
let shouldShowAccountInfo = $state(false);
|
||||
let shouldShowAccountInfoPanel = $state(false);
|
||||
let shouldShowHelpPanel = $state(false);
|
||||
let shouldShowNotificationPanel = $state(false);
|
||||
let innerWidth: number = $state(0);
|
||||
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
|
||||
|
||||
let info: ServerAboutResponseDto | undefined = $state();
|
||||
|
||||
@ -146,6 +150,27 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:clickOutside={{
|
||||
onOutclick: () => (shouldShowNotificationPanel = false),
|
||||
onEscape: () => (shouldShowNotificationPanel = false),
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
shape="round"
|
||||
color={hasUnreadNotifications ? 'primary' : 'secondary'}
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon={hasUnreadNotifications ? mdiBellBadge : mdiBellOutline}
|
||||
onclick={() => (shouldShowNotificationPanel = !shouldShowNotificationPanel)}
|
||||
aria-label={$t('notifications')}
|
||||
/>
|
||||
|
||||
{#if shouldShowNotificationPanel}
|
||||
<NotificationPanel />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:clickOutside={{
|
||||
onOutclick: () => (shouldShowAccountInfoPanel = false),
|
||||
|
@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { NotificationLevel, NotificationType, type NotificationDto } from '@immich/sdk';
|
||||
import { IconButton, Stack, Text } from '@immich/ui';
|
||||
import { mdiBackupRestore, mdiInformationOutline, mdiMessageBadgeOutline, mdiSync } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface Props {
|
||||
notification: NotificationDto;
|
||||
onclick: (id: string) => void;
|
||||
}
|
||||
|
||||
let { notification, onclick }: Props = $props();
|
||||
|
||||
const getAlertColor = (level: NotificationLevel) => {
|
||||
switch (level) {
|
||||
case NotificationLevel.Error: {
|
||||
return 'danger';
|
||||
}
|
||||
case NotificationLevel.Warning: {
|
||||
return 'warning';
|
||||
}
|
||||
case NotificationLevel.Info: {
|
||||
return 'primary';
|
||||
}
|
||||
case NotificationLevel.Success: {
|
||||
return 'success';
|
||||
}
|
||||
default: {
|
||||
return 'primary';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getIconBgColor = (level: NotificationLevel) => {
|
||||
switch (level) {
|
||||
case NotificationLevel.Error: {
|
||||
return 'bg-red-500 dark:bg-red-300 dark:hover:bg-red-200';
|
||||
}
|
||||
case NotificationLevel.Warning: {
|
||||
return 'bg-amber-500 dark:bg-amber-200 dark:hover:bg-amber-200';
|
||||
}
|
||||
case NotificationLevel.Info: {
|
||||
return 'bg-blue-500 dark:bg-blue-200 dark:hover:bg-blue-200';
|
||||
}
|
||||
case NotificationLevel.Success: {
|
||||
return 'bg-green-500 dark:bg-green-200 dark:hover:bg-green-200';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getIconType = (type: NotificationType) => {
|
||||
switch (type) {
|
||||
case NotificationType.BackupFailed: {
|
||||
return mdiBackupRestore;
|
||||
}
|
||||
case NotificationType.JobFailed: {
|
||||
return mdiSync;
|
||||
}
|
||||
case NotificationType.SystemMessage: {
|
||||
return mdiMessageBadgeOutline;
|
||||
}
|
||||
case NotificationType.Custom: {
|
||||
return mdiInformationOutline;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatRelativeTime = (dateString: string): string => {
|
||||
try {
|
||||
const date = DateTime.fromISO(dateString);
|
||||
if (!date.isValid) {
|
||||
return dateString; // Return original string if parsing fails
|
||||
}
|
||||
// Use Luxon's toRelative with the current locale
|
||||
return date.setLocale('en').toRelative() || dateString;
|
||||
} catch (error) {
|
||||
console.error('Error formatting relative time:', error);
|
||||
return dateString; // Fallback to original string on error
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="min-h-[80px] p-2 py-3 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 border-b border-gray-200 dark:border-immich-dark-gray w-full"
|
||||
type="button"
|
||||
onclick={() => onclick(notification.id)}
|
||||
title={notification.createdAt}
|
||||
>
|
||||
<div class="grid grid-cols-[56px_1fr_32px] items-center gap-2">
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<IconButton
|
||||
icon={getIconType(notification.type)}
|
||||
color={getAlertColor(notification.level)}
|
||||
aria-label={notification.title}
|
||||
shape="round"
|
||||
class={getIconBgColor(notification.level)}
|
||||
size="small"
|
||||
></IconButton>
|
||||
</div>
|
||||
|
||||
<Stack class="text-left" gap={1}>
|
||||
<Text size="tiny" class="uppercase text-black dark:text-white font-semibold">{notification.title}</Text>
|
||||
{#if notification.description}
|
||||
<Text class="overflow-hidden text-gray-600 dark:text-gray-300">{notification.description}</Text>
|
||||
{/if}
|
||||
|
||||
<Text size="tiny" color="muted">{formatRelativeTime(notification.createdAt)}</Text>
|
||||
</Stack>
|
||||
|
||||
{#if !notification.readAt}
|
||||
<div class="w-2 h-2 rounded-full bg-primary text-right justify-self-center"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import NotificationItem from '$lib/components/shared-components/navigation-bar/notification-item.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType as WebNotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { Button, Scrollable, Stack, Text } from '@immich/ui';
|
||||
import { mdiBellOutline, mdiCheckAll } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { flip } from 'svelte/animate';
|
||||
|
||||
const noUnreadNotifications = $derived(notificationManager.notifications.length === 0);
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
await notificationManager.markAsRead(id);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_update_notification_status'));
|
||||
}
|
||||
};
|
||||
|
||||
const markAllAsRead = async () => {
|
||||
try {
|
||||
await notificationManager.markAllAsRead();
|
||||
notificationController.show({ message: $t('marked_all_as_read'), type: WebNotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_update_notification_status'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
id="notification-panel"
|
||||
class="absolute right-[25px] top-[70px] z-[100] w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray text-light"
|
||||
use:focusTrap
|
||||
>
|
||||
<Stack class="max-h-[500px]">
|
||||
<div class="flex justify-between items-center mt-4 mx-4">
|
||||
<Text size="medium" color="secondary" class="font-semibold">{$t('notifications')}</Text>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={noUnreadNotifications}
|
||||
leadingIcon={mdiCheckAll}
|
||||
size="small"
|
||||
color="primary"
|
||||
onclick={() => markAllAsRead()}>{$t('mark_all_as_read')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{#if noUnreadNotifications}
|
||||
<Stack
|
||||
class="py-12 flex flex-col place-items-center place-content-center text-gray-700 dark:text-gray-300"
|
||||
gap={1}
|
||||
>
|
||||
<Icon path={mdiBellOutline} size={20}></Icon>
|
||||
<Text>{$t('no_notifications')}</Text>
|
||||
</Stack>
|
||||
{:else}
|
||||
<Scrollable class="pb-6">
|
||||
<Stack gap={0}>
|
||||
{#each notificationManager.notifications as notification (notification.id)}
|
||||
<div animate:flip={{ duration: 400 }}>
|
||||
<NotificationItem {notification} onclick={(id) => markAsRead(id)} />
|
||||
</div>
|
||||
{/each}
|
||||
</Stack>
|
||||
</Scrollable>
|
||||
{/if}
|
||||
</Stack>
|
||||
</div>
|
38
web/src/lib/stores/notification-manager.svelte.ts
Normal file
38
web/src/lib/stores/notification-manager.svelte.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { eventManager } from '$lib/stores/event-manager.svelte';
|
||||
import { getNotifications, updateNotification, updateNotifications, type NotificationDto } from '@immich/sdk';
|
||||
|
||||
class NotificationStore {
|
||||
notifications = $state<NotificationDto[]>([]);
|
||||
|
||||
constructor() {
|
||||
// TODO replace this with an `auth.login` event
|
||||
this.refresh().catch(() => {});
|
||||
|
||||
eventManager.on('auth.logout', () => this.clear());
|
||||
}
|
||||
|
||||
get hasUnread() {
|
||||
return this.notifications.length > 0;
|
||||
}
|
||||
|
||||
refresh = async () => {
|
||||
this.notifications = await getNotifications({ unread: true });
|
||||
};
|
||||
|
||||
markAsRead = async (id: string) => {
|
||||
this.notifications = this.notifications.filter((notification) => notification.id !== id);
|
||||
await updateNotification({ id, notificationUpdateDto: { readAt: new Date().toISOString() } });
|
||||
};
|
||||
|
||||
markAllAsRead = async () => {
|
||||
const ids = this.notifications.map(({ id }) => id);
|
||||
this.notifications = [];
|
||||
await updateNotifications({ notificationUpdateAllDto: { ids, readAt: new Date().toISOString() } });
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.notifications = [];
|
||||
};
|
||||
}
|
||||
|
||||
export const notificationManager = new NotificationStore();
|
@ -1,6 +1,7 @@
|
||||
import { authManager } from '$lib/stores/auth-manager.svelte';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
import { createEventEmitter } from '$lib/utils/eventemitter';
|
||||
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||
import { type AssetResponseDto, type NotificationDto, type ServerVersionResponseDto } from '@immich/sdk';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { user } from './user.store';
|
||||
@ -26,6 +27,7 @@ export interface Events {
|
||||
on_config_update: () => void;
|
||||
on_new_release: (newRelase: ReleaseEvent) => void;
|
||||
on_session_delete: (sessionId: string) => void;
|
||||
on_notification: (notification: NotificationDto) => void;
|
||||
}
|
||||
|
||||
const websocket: Socket<Events> = io({
|
||||
@ -50,6 +52,7 @@ websocket
|
||||
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
|
||||
.on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
|
||||
.on('on_session_delete', () => authManager.logout())
|
||||
.on('on_notification', () => notificationManager.refresh())
|
||||
.on('connect_error', (e) => console.log('Websocket Connect Error', e));
|
||||
|
||||
export const openWebsocketConnection = () => {
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@ -24,7 +25,10 @@
|
||||
let loading = $state(false);
|
||||
let oauthLoading = $state(true);
|
||||
|
||||
const onSuccess = async () => await goto(AppRoute.PHOTOS, { invalidateAll: true });
|
||||
const onSuccess = async () => {
|
||||
await notificationManager.refresh();
|
||||
await goto(AppRoute.PHOTOS, { invalidateAll: true });
|
||||
};
|
||||
const onFirstLogin = async () => await goto(AppRoute.AUTH_CHANGE_PASSWORD);
|
||||
const onOnboarding = async () => await goto(AppRoute.AUTH_ONBOARDING);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user