feat: groups

This commit is contained in:
Jason Rasmussen 2025-07-30 18:18:38 -04:00
parent 641a3baadd
commit 4a881022c3
No known key found for this signature in database
GPG Key ID: 2EF24B77EAFA4A41
76 changed files with 6515 additions and 124 deletions

View File

@ -25,6 +25,7 @@
"add_photos": "Add photos",
"add_tag": "Add tag",
"add_to": "Add to…",
"confirm_delete_name": "Are you sure you want to delete {name}?",
"add_to_album": "Add to album",
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
@ -33,8 +34,10 @@
"added_to_archive": "Added to archive",
"added_to_favorites": "Added to favorites",
"added_to_favorites_count": "Added {count, number} to favorites",
"empty_group_message": "This group is empty",
"admin": {
"add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".",
"group_details": "Group Details",
"admin_user": "Admin User",
"asset_offline_description": "This external library asset is no longer found on disk and has been moved to trash. If the file was moved within the library, check your timeline for the new corresponding asset. To restore this asset, please ensure that the file path below can be accessed by Immich and scan the library.",
"authentication_settings": "Authentication Settings",
@ -363,6 +366,7 @@
"user_delete_immediately_checkbox": "Queue user and assets for immediate deletion",
"user_details": "User Details",
"user_management": "User Management",
"group_management": "Group Management",
"user_password_has_been_reset": "The user's password has been reset:",
"user_password_reset_description": "Please provide the temporary password to the user and inform them they will need to change the password at their next login.",
"user_restore_description": "<b>{user}</b>'s account will be restored.",
@ -429,6 +433,8 @@
"album_viewer_appbar_share_leave": "Leave album",
"album_viewer_appbar_share_to": "Share To",
"album_viewer_page_share_add_users": "Add users",
"edit_users": "Edit users",
"add_users": "Add users",
"album_with_link_access": "Let anyone with the link see photos and people in this album.",
"albums": "Albums",
"albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}",
@ -469,6 +475,9 @@
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_added_to_album": "Added to album",
"group": "Group",
"group_users": "Group users",
"other_users": "Other users",
"asset_adding_to_album": "Adding to album…",
"asset_description_updated": "Asset description has been updated",
"asset_filename_is_offline": "Asset {filename} is offline",
@ -721,11 +730,14 @@
"create_new_person": "Create new person",
"create_new_person_hint": "Assign selected assets to a new person",
"create_new_user": "Create new user",
"create_new_group": "Create new group",
"groups": "Groups",
"create_shared_album_page_share_add_assets": "ADD ASSETS",
"create_shared_album_page_share_select_photos": "Select Photos",
"create_tag": "Create tag",
"create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
"create_user": "Create user",
"create_group": "Create group",
"created": "Created",
"created_at": "Created",
"crop": "Crop",
@ -781,6 +793,7 @@
"delete_tag": "Delete tag",
"delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?",
"delete_user": "Delete user",
"delete_group": "Delete group",
"deleted_shared_link": "Deleted shared link",
"deletes_missing_assets": "Deletes assets missing from disk",
"description": "Description",
@ -850,6 +863,7 @@
"edit_tag": "Edit tag",
"edit_title": "Edit Title",
"edit_user": "Edit user",
"edit_group": "Edit group",
"edited": "Edited",
"editor": "Editor",
"editor_close_without_save_prompt": "The changes will not be saved",
@ -937,6 +951,7 @@
"unable_to_create_api_key": "Unable to create a new API Key",
"unable_to_create_library": "Unable to create library",
"unable_to_create_user": "Unable to create user",
"unable_to_create_group": "Unable to create group",
"unable_to_delete_album": "Unable to delete album",
"unable_to_delete_asset": "Unable to delete asset",
"unable_to_delete_assets": "Error deleting assets",
@ -995,6 +1010,7 @@
"unable_to_update_settings": "Unable to update settings",
"unable_to_update_timeline_display_status": "Unable to update timeline display status",
"unable_to_update_user": "Unable to update user",
"unable_to_update_group": "Unable to update group",
"unable_to_upload_file": "Unable to upload file"
},
"exif": "Exif",
@ -2031,6 +2047,7 @@
"view_qr_code": "View QR code",
"view_stack": "View Stack",
"view_user": "View User",
"view_group": "View Group",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack",

View File

@ -83,14 +83,18 @@ Class | Method | HTTP request | Description
*ActivitiesApi* | [**getActivities**](doc//ActivitiesApi.md#getactivities) | **GET** /activities |
*ActivitiesApi* | [**getActivityStatistics**](doc//ActivitiesApi.md#getactivitystatistics) | **GET** /activities/statistics |
*AlbumsApi* | [**addAssetsToAlbum**](doc//AlbumsApi.md#addassetstoalbum) | **PUT** /albums/{id}/assets |
*AlbumsApi* | [**addGroupsToAlbum**](doc//AlbumsApi.md#addgroupstoalbum) | **PUT** /albums/{id}/groups |
*AlbumsApi* | [**addUsersToAlbum**](doc//AlbumsApi.md#adduserstoalbum) | **PUT** /albums/{id}/users |
*AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums |
*AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} |
*AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} |
*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics |
*AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums |
*AlbumsApi* | [**getGroupsForAlbum**](doc//AlbumsApi.md#getgroupsforalbum) | **GET** /albums/{id}/groups |
*AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets |
*AlbumsApi* | [**removeGroupsFromAlbum**](doc//AlbumsApi.md#removegroupsfromalbum) | **DELETE** /albums/{id}/groups |
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} |
*AlbumsApi* | [**updateAlbumGroup**](doc//AlbumsApi.md#updatealbumgroup) | **PUT** /albums/{id}/groups/{groupId} |
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} |
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload
@ -129,6 +133,19 @@ Class | Method | HTTP request | Description
*FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} |
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces |
*FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} |
*GroupsApi* | [**getMyGroup**](doc//GroupsApi.md#getmygroup) | **GET** /groups/{id} |
*GroupsApi* | [**getMyGroupUsers**](doc//GroupsApi.md#getmygroupusers) | **GET** /groups/{id}/users |
*GroupsApi* | [**leaveMyGroup**](doc//GroupsApi.md#leavemygroup) | **DELETE** /groups/{id} |
*GroupsApi* | [**searchMyGroups**](doc//GroupsApi.md#searchmygroups) | **GET** /groups |
*GroupsAdminApi* | [**addUsersToGroupAdmin**](doc//GroupsAdminApi.md#adduserstogroupadmin) | **PUT** /admin/groups/{id}/users |
*GroupsAdminApi* | [**createGroupAdmin**](doc//GroupsAdminApi.md#creategroupadmin) | **POST** /admin/groups |
*GroupsAdminApi* | [**deleteGroupAdmin**](doc//GroupsAdminApi.md#deletegroupadmin) | **DELETE** /admin/groups/{id} |
*GroupsAdminApi* | [**getGroupAdmin**](doc//GroupsAdminApi.md#getgroupadmin) | **GET** /admin/groups/{id} |
*GroupsAdminApi* | [**getUsersForGroupAdmin**](doc//GroupsAdminApi.md#getusersforgroupadmin) | **GET** /admin/groups/{id}/users |
*GroupsAdminApi* | [**removeUserFromGroupAdmin**](doc//GroupsAdminApi.md#removeuserfromgroupadmin) | **DELETE** /admin/groups/{id}/user/{userId} |
*GroupsAdminApi* | [**removeUsersFromGroupAdmin**](doc//GroupsAdminApi.md#removeusersfromgroupadmin) | **DELETE** /admin/groups/{id}/user |
*GroupsAdminApi* | [**searchGroupsAdmin**](doc//GroupsAdminApi.md#searchgroupsadmin) | **GET** /admin/groups |
*GroupsAdminApi* | [**updateGroupAdmin**](doc//GroupsAdminApi.md#updategroupadmin) | **PUT** /admin/groups/{id} |
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
@ -292,6 +309,12 @@ Class | Method | HTTP request | Description
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
- [AddUsersDto](doc//AddUsersDto.md)
- [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
- [AlbumGroupCreateAllDto](doc//AlbumGroupCreateAllDto.md)
- [AlbumGroupDeleteAllDto](doc//AlbumGroupDeleteAllDto.md)
- [AlbumGroupDto](doc//AlbumGroupDto.md)
- [AlbumGroupMetadata](doc//AlbumGroupMetadata.md)
- [AlbumGroupResponseDto](doc//AlbumGroupResponseDto.md)
- [AlbumGroupUpdateDto](doc//AlbumGroupUpdateDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AlbumStatisticsResponseDto](doc//AlbumStatisticsResponseDto.md)
- [AlbumUserAddDto](doc//AlbumUserAddDto.md)
@ -360,6 +383,15 @@ Class | Method | HTTP request | Description
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
- [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md)
- [GroupAdminCreateDto](doc//GroupAdminCreateDto.md)
- [GroupAdminResponseDto](doc//GroupAdminResponseDto.md)
- [GroupAdminUpdateDto](doc//GroupAdminUpdateDto.md)
- [GroupResponseDto](doc//GroupResponseDto.md)
- [GroupUserCreateAllDto](doc//GroupUserCreateAllDto.md)
- [GroupUserDeleteAllDto](doc//GroupUserDeleteAllDto.md)
- [GroupUserDto](doc//GroupUserDto.md)
- [GroupUserMetadata](doc//GroupUserMetadata.md)
- [GroupUserResponseDto](doc//GroupUserResponseDto.md)
- [ImageFormat](doc//ImageFormat.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)

View File

@ -39,6 +39,8 @@ part 'api/deprecated_api.dart';
part 'api/download_api.dart';
part 'api/duplicates_api.dart';
part 'api/faces_api.dart';
part 'api/groups_api.dart';
part 'api/groups_admin_api.dart';
part 'api/jobs_api.dart';
part 'api/libraries_api.dart';
part 'api/map_api.dart';
@ -72,6 +74,12 @@ part 'model/activity_response_dto.dart';
part 'model/activity_statistics_response_dto.dart';
part 'model/add_users_dto.dart';
part 'model/admin_onboarding_update_dto.dart';
part 'model/album_group_create_all_dto.dart';
part 'model/album_group_delete_all_dto.dart';
part 'model/album_group_dto.dart';
part 'model/album_group_metadata.dart';
part 'model/album_group_response_dto.dart';
part 'model/album_group_update_dto.dart';
part 'model/album_response_dto.dart';
part 'model/album_statistics_response_dto.dart';
part 'model/album_user_add_dto.dart';
@ -140,6 +148,15 @@ part 'model/face_dto.dart';
part 'model/facial_recognition_config.dart';
part 'model/folders_response.dart';
part 'model/folders_update.dart';
part 'model/group_admin_create_dto.dart';
part 'model/group_admin_response_dto.dart';
part 'model/group_admin_update_dto.dart';
part 'model/group_response_dto.dart';
part 'model/group_user_create_all_dto.dart';
part 'model/group_user_delete_all_dto.dart';
part 'model/group_user_dto.dart';
part 'model/group_user_metadata.dart';
part 'model/group_user_response_dto.dart';
part 'model/image_format.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';

View File

@ -91,6 +91,66 @@ class AlbumsApi {
return null;
}
/// This endpoint requires the `albumGroup.create` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [AlbumGroupCreateAllDto] albumGroupCreateAllDto (required):
Future<Response> addGroupsToAlbumWithHttpInfo(String id, AlbumGroupCreateAllDto albumGroupCreateAllDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/groups'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = albumGroupCreateAllDto;
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,
);
}
/// This endpoint requires the `albumGroup.create` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [AlbumGroupCreateAllDto] albumGroupCreateAllDto (required):
Future<List<AlbumGroupResponseDto>?> addGroupsToAlbum(String id, AlbumGroupCreateAllDto albumGroupCreateAllDto,) async {
final response = await addGroupsToAlbumWithHttpInfo(id, albumGroupCreateAllDto,);
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<AlbumGroupResponseDto>') as List)
.cast<AlbumGroupResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint requires the `albumUser.create` permission.
///
/// Note: This method returns the HTTP [Response].
@ -432,6 +492,62 @@ class AlbumsApi {
return null;
}
/// This endpoint requires the `albumGroup.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getGroupsForAlbumWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/groups'
.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,
);
}
/// This endpoint requires the `albumGroup.read` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<List<AlbumGroupResponseDto>?> getGroupsForAlbum(String id,) async {
final response = await getGroupsForAlbumWithHttpInfo(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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AlbumGroupResponseDto>') as List)
.cast<AlbumGroupResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint requires the `albumAsset.delete` permission.
///
/// Note: This method returns the HTTP [Response].
@ -492,6 +608,55 @@ class AlbumsApi {
return null;
}
/// This endpoint requires the `albumGroup.delete` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [AlbumGroupDeleteAllDto] albumGroupDeleteAllDto (required):
Future<Response> removeGroupsFromAlbumWithHttpInfo(String id, AlbumGroupDeleteAllDto albumGroupDeleteAllDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/groups'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = albumGroupDeleteAllDto;
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,
);
}
/// This endpoint requires the `albumGroup.delete` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [AlbumGroupDeleteAllDto] albumGroupDeleteAllDto (required):
Future<void> removeGroupsFromAlbum(String id, AlbumGroupDeleteAllDto albumGroupDeleteAllDto,) async {
final response = await removeGroupsFromAlbumWithHttpInfo(id, albumGroupDeleteAllDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// This endpoint requires the `albumUser.delete` permission.
///
/// Note: This method returns the HTTP [Response].
@ -542,6 +707,68 @@ class AlbumsApi {
}
}
/// This endpoint requires the `albumGroup.update` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] groupId (required):
///
/// * [String] id (required):
///
/// * [AlbumGroupUpdateDto] albumGroupUpdateDto (required):
Future<Response> updateAlbumGroupWithHttpInfo(String groupId, String id, AlbumGroupUpdateDto albumGroupUpdateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/groups/{groupId}'
.replaceAll('{groupId}', groupId)
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = albumGroupUpdateDto;
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,
);
}
/// This endpoint requires the `albumGroup.update` permission.
///
/// Parameters:
///
/// * [String] groupId (required):
///
/// * [String] id (required):
///
/// * [AlbumGroupUpdateDto] albumGroupUpdateDto (required):
Future<AlbumGroupResponseDto?> updateAlbumGroup(String groupId, String id, AlbumGroupUpdateDto albumGroupUpdateDto,) async {
final response = await updateAlbumGroupWithHttpInfo(groupId, id, albumGroupUpdateDto,);
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), 'AlbumGroupResponseDto',) as AlbumGroupResponseDto;
}
return null;
}
/// This endpoint requires the `album.update` permission.
///
/// Note: This method returns the HTTP [Response].

View File

@ -0,0 +1,506 @@
//
// 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 GroupsAdminApi {
GroupsAdminApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// This endpoint is an admin-only route, and requires the `adminGroupUser.create` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [GroupUserCreateAllDto] groupUserCreateAllDto (required):
Future<Response> addUsersToGroupAdminWithHttpInfo(String id, GroupUserCreateAllDto groupUserCreateAllDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{id}/users'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = groupUserCreateAllDto;
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,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroupUser.create` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [GroupUserCreateAllDto] groupUserCreateAllDto (required):
Future<List<GroupUserResponseDto>?> addUsersToGroupAdmin(String id, GroupUserCreateAllDto groupUserCreateAllDto,) async {
final response = await addUsersToGroupAdminWithHttpInfo(id, groupUserCreateAllDto,);
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<GroupUserResponseDto>') as List)
.cast<GroupUserResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint is an admin-only route, and requires the `adminGroup.create` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [GroupAdminCreateDto] groupAdminCreateDto (required):
Future<Response> createGroupAdminWithHttpInfo(GroupAdminCreateDto groupAdminCreateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups';
// ignore: prefer_final_locals
Object? postBody = groupAdminCreateDto;
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,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroup.create` permission.
///
/// Parameters:
///
/// * [GroupAdminCreateDto] groupAdminCreateDto (required):
Future<GroupAdminResponseDto?> createGroupAdmin(GroupAdminCreateDto groupAdminCreateDto,) async {
final response = await createGroupAdminWithHttpInfo(groupAdminCreateDto,);
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), 'GroupAdminResponseDto',) as GroupAdminResponseDto;
}
return null;
}
/// This endpoint is an admin-only route, and requires the `adminGroup.delete` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteGroupAdminWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{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,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroup.delete` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteGroupAdmin(String id,) async {
final response = await deleteGroupAdminWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// This endpoint is an admin-only route, and requires the `adminGroup.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getGroupAdminWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{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,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroup.read` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<GroupAdminResponseDto?> getGroupAdmin(String id,) async {
final response = await getGroupAdminWithHttpInfo(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), 'GroupAdminResponseDto',) as GroupAdminResponseDto;
}
return null;
}
/// This endpoint is an admin-only route, and requires the `adminGroupUser.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getUsersForGroupAdminWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{id}/users'
.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,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroupUser.read` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<List<GroupUserResponseDto>?> getUsersForGroupAdmin(String id,) async {
final response = await getUsersForGroupAdminWithHttpInfo(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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<GroupUserResponseDto>') as List)
.cast<GroupUserResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint requires the `adminGroupUser.delete` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [String] userId (required):
Future<Response> removeUserFromGroupAdminWithHttpInfo(String id, String userId,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{id}/user/{userId}'
.replaceAll('{id}', id)
.replaceAll('{userId}', userId);
// 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,
);
}
/// This endpoint requires the `adminGroupUser.delete` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [String] userId (required):
Future<void> removeUserFromGroupAdmin(String id, String userId,) async {
final response = await removeUserFromGroupAdminWithHttpInfo(id, userId,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// This endpoint requires the `adminGroupUser.delete` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [GroupUserDeleteAllDto] groupUserDeleteAllDto (required):
Future<Response> removeUsersFromGroupAdminWithHttpInfo(String id, GroupUserDeleteAllDto groupUserDeleteAllDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{id}/user'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = groupUserDeleteAllDto;
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,
);
}
/// This endpoint requires the `adminGroupUser.delete` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [GroupUserDeleteAllDto] groupUserDeleteAllDto (required):
Future<void> removeUsersFromGroupAdmin(String id, GroupUserDeleteAllDto groupUserDeleteAllDto,) async {
final response = await removeUsersFromGroupAdminWithHttpInfo(id, groupUserDeleteAllDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// This endpoint is an admin-only route, and requires the `adminGroup.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id:
///
/// * [String] userId:
Future<Response> searchGroupsAdminWithHttpInfo({ String? id, String? userId, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups';
// 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 (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroup.read` permission.
///
/// Parameters:
///
/// * [String] id:
///
/// * [String] userId:
Future<List<GroupAdminResponseDto>?> searchGroupsAdmin({ String? id, String? userId, }) async {
final response = await searchGroupsAdminWithHttpInfo( id: id, userId: userId, );
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<GroupAdminResponseDto>') as List)
.cast<GroupAdminResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint is an admin-only route, and requires the `adminGroup.update` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [GroupAdminUpdateDto] groupAdminUpdateDto (required):
Future<Response> updateGroupAdminWithHttpInfo(String id, GroupAdminUpdateDto groupAdminUpdateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/groups/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = groupAdminUpdateDto;
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,
);
}
/// This endpoint is an admin-only route, and requires the `adminGroup.update` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [GroupAdminUpdateDto] groupAdminUpdateDto (required):
Future<GroupAdminResponseDto?> updateGroupAdmin(String id, GroupAdminUpdateDto groupAdminUpdateDto,) async {
final response = await updateGroupAdminWithHttpInfo(id, groupAdminUpdateDto,);
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), 'GroupAdminResponseDto',) as GroupAdminResponseDto;
}
return null;
}
}

219
mobile/openapi/lib/api/groups_api.dart generated Normal file
View File

@ -0,0 +1,219 @@
//
// 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 GroupsApi {
GroupsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// This endpoint requires the `group.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getMyGroupWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/groups/{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,
);
}
/// This endpoint requires the `group.read` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<GroupResponseDto?> getMyGroup(String id,) async {
final response = await getMyGroupWithHttpInfo(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), 'GroupResponseDto',) as GroupResponseDto;
}
return null;
}
/// This endpoint is an admin-only route, and requires the `group.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getMyGroupUsersWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/groups/{id}/users'
.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,
);
}
/// This endpoint is an admin-only route, and requires the `group.read` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<List<GroupUserResponseDto>?> getMyGroupUsers(String id,) async {
final response = await getMyGroupUsersWithHttpInfo(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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<GroupUserResponseDto>') as List)
.cast<GroupUserResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint requires the `group.delete` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> leaveMyGroupWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/groups/{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,
);
}
/// This endpoint requires the `group.delete` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<void> leaveMyGroup(String id,) async {
final response = await leaveMyGroupWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// This endpoint requires the `group.read` permission.
///
/// Note: This method returns the HTTP [Response].
Future<Response> searchMyGroupsWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/groups';
// 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,
);
}
/// This endpoint requires the `group.read` permission.
Future<List<GroupResponseDto>?> searchMyGroups() async {
final response = await searchMyGroupsWithHttpInfo();
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<GroupResponseDto>') as List)
.cast<GroupResponseDto>()
.toList(growable: false);
}
return null;
}
}

View File

@ -200,6 +200,18 @@ class ApiClient {
return AddUsersDto.fromJson(value);
case 'AdminOnboardingUpdateDto':
return AdminOnboardingUpdateDto.fromJson(value);
case 'AlbumGroupCreateAllDto':
return AlbumGroupCreateAllDto.fromJson(value);
case 'AlbumGroupDeleteAllDto':
return AlbumGroupDeleteAllDto.fromJson(value);
case 'AlbumGroupDto':
return AlbumGroupDto.fromJson(value);
case 'AlbumGroupMetadata':
return AlbumGroupMetadata.fromJson(value);
case 'AlbumGroupResponseDto':
return AlbumGroupResponseDto.fromJson(value);
case 'AlbumGroupUpdateDto':
return AlbumGroupUpdateDto.fromJson(value);
case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value);
case 'AlbumStatisticsResponseDto':
@ -336,6 +348,24 @@ class ApiClient {
return FoldersResponse.fromJson(value);
case 'FoldersUpdate':
return FoldersUpdate.fromJson(value);
case 'GroupAdminCreateDto':
return GroupAdminCreateDto.fromJson(value);
case 'GroupAdminResponseDto':
return GroupAdminResponseDto.fromJson(value);
case 'GroupAdminUpdateDto':
return GroupAdminUpdateDto.fromJson(value);
case 'GroupResponseDto':
return GroupResponseDto.fromJson(value);
case 'GroupUserCreateAllDto':
return GroupUserCreateAllDto.fromJson(value);
case 'GroupUserDeleteAllDto':
return GroupUserDeleteAllDto.fromJson(value);
case 'GroupUserDto':
return GroupUserDto.fromJson(value);
case 'GroupUserMetadata':
return GroupUserMetadata.fromJson(value);
case 'GroupUserResponseDto':
return GroupUserResponseDto.fromJson(value);
case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value);
case 'JobCommand':

View File

@ -0,0 +1,99 @@
//
// 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 AlbumGroupCreateAllDto {
/// Returns a new [AlbumGroupCreateAllDto] instance.
AlbumGroupCreateAllDto({
this.groups = const [],
});
List<AlbumGroupDto> groups;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumGroupCreateAllDto &&
_deepEquality.equals(other.groups, groups);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(groups.hashCode);
@override
String toString() => 'AlbumGroupCreateAllDto[groups=$groups]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'groups'] = this.groups;
return json;
}
/// Returns a new [AlbumGroupCreateAllDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumGroupCreateAllDto? fromJson(dynamic value) {
upgradeDto(value, "AlbumGroupCreateAllDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AlbumGroupCreateAllDto(
groups: AlbumGroupDto.listFromJson(json[r'groups']),
);
}
return null;
}
static List<AlbumGroupCreateAllDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumGroupCreateAllDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumGroupCreateAllDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumGroupCreateAllDto> mapFromJson(dynamic json) {
final map = <String, AlbumGroupCreateAllDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumGroupCreateAllDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumGroupCreateAllDto-objects as value to a dart map
static Map<String, List<AlbumGroupCreateAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumGroupCreateAllDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AlbumGroupCreateAllDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'groups',
};
}

View 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 AlbumGroupDeleteAllDto {
/// Returns a new [AlbumGroupDeleteAllDto] instance.
AlbumGroupDeleteAllDto({
this.groupIds = const [],
});
List<String> groupIds;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumGroupDeleteAllDto &&
_deepEquality.equals(other.groupIds, groupIds);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(groupIds.hashCode);
@override
String toString() => 'AlbumGroupDeleteAllDto[groupIds=$groupIds]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'groupIds'] = this.groupIds;
return json;
}
/// Returns a new [AlbumGroupDeleteAllDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumGroupDeleteAllDto? fromJson(dynamic value) {
upgradeDto(value, "AlbumGroupDeleteAllDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AlbumGroupDeleteAllDto(
groupIds: json[r'groupIds'] is Iterable
? (json[r'groupIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
);
}
return null;
}
static List<AlbumGroupDeleteAllDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumGroupDeleteAllDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumGroupDeleteAllDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumGroupDeleteAllDto> mapFromJson(dynamic json) {
final map = <String, AlbumGroupDeleteAllDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumGroupDeleteAllDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumGroupDeleteAllDto-objects as value to a dart map
static Map<String, List<AlbumGroupDeleteAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumGroupDeleteAllDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AlbumGroupDeleteAllDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'groupIds',
};
}

View File

@ -0,0 +1,116 @@
//
// 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 AlbumGroupDto {
/// Returns a new [AlbumGroupDto] instance.
AlbumGroupDto({
required this.groupId,
this.role,
});
String groupId;
///
/// 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.
///
AlbumUserRole? role;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumGroupDto &&
other.groupId == groupId &&
other.role == role;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(groupId.hashCode) +
(role == null ? 0 : role!.hashCode);
@override
String toString() => 'AlbumGroupDto[groupId=$groupId, role=$role]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'groupId'] = this.groupId;
if (this.role != null) {
json[r'role'] = this.role;
} else {
// json[r'role'] = null;
}
return json;
}
/// Returns a new [AlbumGroupDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumGroupDto? fromJson(dynamic value) {
upgradeDto(value, "AlbumGroupDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AlbumGroupDto(
groupId: mapValueOfType<String>(json, r'groupId')!,
role: AlbumUserRole.fromJson(json[r'role']),
);
}
return null;
}
static List<AlbumGroupDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumGroupDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumGroupDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumGroupDto> mapFromJson(dynamic json) {
final map = <String, AlbumGroupDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumGroupDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumGroupDto-objects as value to a dart map
static Map<String, List<AlbumGroupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumGroupDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AlbumGroupDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'groupId',
};
}

View File

@ -0,0 +1,107 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AlbumGroupMetadata {
/// Returns a new [AlbumGroupMetadata] instance.
AlbumGroupMetadata({
required this.createdAt,
required this.updatedAt,
});
DateTime createdAt;
DateTime updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumGroupMetadata &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'AlbumGroupMetadata[createdAt=$createdAt, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json;
}
/// Returns a new [AlbumGroupMetadata] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumGroupMetadata? fromJson(dynamic value) {
upgradeDto(value, "AlbumGroupMetadata");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AlbumGroupMetadata(
createdAt: mapDateTime(json, r'createdAt', r'')!,
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
);
}
return null;
}
static List<AlbumGroupMetadata> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumGroupMetadata>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumGroupMetadata.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumGroupMetadata> mapFromJson(dynamic json) {
final map = <String, AlbumGroupMetadata>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumGroupMetadata.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumGroupMetadata-objects as value to a dart map
static Map<String, List<AlbumGroupMetadata>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumGroupMetadata>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AlbumGroupMetadata.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'createdAt',
'updatedAt',
};
}

View File

@ -0,0 +1,127 @@
//
// 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 AlbumGroupResponseDto {
/// Returns a new [AlbumGroupResponseDto] instance.
AlbumGroupResponseDto({
required this.description,
required this.id,
required this.metadata,
required this.name,
});
String? description;
String id;
AlbumGroupMetadata metadata;
String name;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumGroupResponseDto &&
other.description == description &&
other.id == id &&
other.metadata == metadata &&
other.name == name;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(id.hashCode) +
(metadata.hashCode) +
(name.hashCode);
@override
String toString() => 'AlbumGroupResponseDto[description=$description, id=$id, metadata=$metadata, name=$name]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'id'] = this.id;
json[r'metadata'] = this.metadata;
json[r'name'] = this.name;
return json;
}
/// Returns a new [AlbumGroupResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumGroupResponseDto? fromJson(dynamic value) {
upgradeDto(value, "AlbumGroupResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AlbumGroupResponseDto(
description: mapValueOfType<String>(json, r'description'),
id: mapValueOfType<String>(json, r'id')!,
metadata: AlbumGroupMetadata.fromJson(json[r'metadata'])!,
name: mapValueOfType<String>(json, r'name')!,
);
}
return null;
}
static List<AlbumGroupResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumGroupResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumGroupResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumGroupResponseDto> mapFromJson(dynamic json) {
final map = <String, AlbumGroupResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumGroupResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumGroupResponseDto-objects as value to a dart map
static Map<String, List<AlbumGroupResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumGroupResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AlbumGroupResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'description',
'id',
'metadata',
'name',
};
}

View File

@ -0,0 +1,99 @@
//
// 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 AlbumGroupUpdateDto {
/// Returns a new [AlbumGroupUpdateDto] instance.
AlbumGroupUpdateDto({
required this.role,
});
AlbumUserRole role;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumGroupUpdateDto &&
other.role == role;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(role.hashCode);
@override
String toString() => 'AlbumGroupUpdateDto[role=$role]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'role'] = this.role;
return json;
}
/// Returns a new [AlbumGroupUpdateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumGroupUpdateDto? fromJson(dynamic value) {
upgradeDto(value, "AlbumGroupUpdateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AlbumGroupUpdateDto(
role: AlbumUserRole.fromJson(json[r'role'])!,
);
}
return null;
}
static List<AlbumGroupUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumGroupUpdateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumGroupUpdateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumGroupUpdateDto> mapFromJson(dynamic json) {
final map = <String, AlbumGroupUpdateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumGroupUpdateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumGroupUpdateDto-objects as value to a dart map
static Map<String, List<AlbumGroupUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumGroupUpdateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AlbumGroupUpdateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'role',
};
}

View File

@ -17,6 +17,7 @@ class CreateAlbumDto {
this.albumUsers = const [],
this.assetIds = const [],
this.description,
this.groups = const [],
});
String albumName;
@ -33,12 +34,15 @@ class CreateAlbumDto {
///
String? description;
List<AlbumGroupDto> groups;
@override
bool operator ==(Object other) => identical(this, other) || other is CreateAlbumDto &&
other.albumName == albumName &&
_deepEquality.equals(other.albumUsers, albumUsers) &&
_deepEquality.equals(other.assetIds, assetIds) &&
other.description == description;
other.description == description &&
_deepEquality.equals(other.groups, groups);
@override
int get hashCode =>
@ -46,10 +50,11 @@ class CreateAlbumDto {
(albumName.hashCode) +
(albumUsers.hashCode) +
(assetIds.hashCode) +
(description == null ? 0 : description!.hashCode);
(description == null ? 0 : description!.hashCode) +
(groups.hashCode);
@override
String toString() => 'CreateAlbumDto[albumName=$albumName, albumUsers=$albumUsers, assetIds=$assetIds, description=$description]';
String toString() => 'CreateAlbumDto[albumName=$albumName, albumUsers=$albumUsers, assetIds=$assetIds, description=$description, groups=$groups]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -61,6 +66,7 @@ class CreateAlbumDto {
} else {
// json[r'description'] = null;
}
json[r'groups'] = this.groups;
return json;
}
@ -79,6 +85,7 @@ class CreateAlbumDto {
? (json[r'assetIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
description: mapValueOfType<String>(json, r'description'),
groups: AlbumGroupDto.listFromJson(json[r'groups']),
);
}
return null;

View File

@ -0,0 +1,117 @@
//
// 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 GroupAdminCreateDto {
/// Returns a new [GroupAdminCreateDto] instance.
GroupAdminCreateDto({
this.description,
required this.name,
this.users = const [],
});
String? description;
String name;
List<GroupUserDto> users;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupAdminCreateDto &&
other.description == description &&
other.name == name &&
_deepEquality.equals(other.users, users);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(name.hashCode) +
(users.hashCode);
@override
String toString() => 'GroupAdminCreateDto[description=$description, name=$name, users=$users]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'name'] = this.name;
json[r'users'] = this.users;
return json;
}
/// Returns a new [GroupAdminCreateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupAdminCreateDto? fromJson(dynamic value) {
upgradeDto(value, "GroupAdminCreateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupAdminCreateDto(
description: mapValueOfType<String>(json, r'description'),
name: mapValueOfType<String>(json, r'name')!,
users: GroupUserDto.listFromJson(json[r'users']),
);
}
return null;
}
static List<GroupAdminCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupAdminCreateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupAdminCreateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupAdminCreateDto> mapFromJson(dynamic json) {
final map = <String, GroupAdminCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupAdminCreateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupAdminCreateDto-objects as value to a dart map
static Map<String, List<GroupAdminCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupAdminCreateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupAdminCreateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'name',
};
}

View File

@ -0,0 +1,135 @@
//
// 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 GroupAdminResponseDto {
/// Returns a new [GroupAdminResponseDto] instance.
GroupAdminResponseDto({
required this.createdAt,
required this.description,
required this.id,
required this.name,
required this.updatedAt,
});
DateTime createdAt;
String? description;
String id;
String name;
DateTime updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupAdminResponseDto &&
other.createdAt == createdAt &&
other.description == description &&
other.id == id &&
other.name == name &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(description == null ? 0 : description!.hashCode) +
(id.hashCode) +
(name.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'GroupAdminResponseDto[createdAt=$createdAt, description=$description, id=$id, name=$name, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'id'] = this.id;
json[r'name'] = this.name;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json;
}
/// Returns a new [GroupAdminResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupAdminResponseDto? fromJson(dynamic value) {
upgradeDto(value, "GroupAdminResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupAdminResponseDto(
createdAt: mapDateTime(json, r'createdAt', r'')!,
description: mapValueOfType<String>(json, r'description'),
id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!,
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
);
}
return null;
}
static List<GroupAdminResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupAdminResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupAdminResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupAdminResponseDto> mapFromJson(dynamic json) {
final map = <String, GroupAdminResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupAdminResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupAdminResponseDto-objects as value to a dart map
static Map<String, List<GroupAdminResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupAdminResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupAdminResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'createdAt',
'description',
'id',
'name',
'updatedAt',
};
}

View File

@ -0,0 +1,119 @@
//
// 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 GroupAdminUpdateDto {
/// Returns a new [GroupAdminUpdateDto] instance.
GroupAdminUpdateDto({
this.description,
this.name,
});
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.
///
String? name;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupAdminUpdateDto &&
other.description == description &&
other.name == name;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(name == null ? 0 : name!.hashCode);
@override
String toString() => 'GroupAdminUpdateDto[description=$description, name=$name]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
if (this.name != null) {
json[r'name'] = this.name;
} else {
// json[r'name'] = null;
}
return json;
}
/// Returns a new [GroupAdminUpdateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupAdminUpdateDto? fromJson(dynamic value) {
upgradeDto(value, "GroupAdminUpdateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupAdminUpdateDto(
description: mapValueOfType<String>(json, r'description'),
name: mapValueOfType<String>(json, r'name'),
);
}
return null;
}
static List<GroupAdminUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupAdminUpdateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupAdminUpdateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupAdminUpdateDto> mapFromJson(dynamic json) {
final map = <String, GroupAdminUpdateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupAdminUpdateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupAdminUpdateDto-objects as value to a dart map
static Map<String, List<GroupAdminUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupAdminUpdateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupAdminUpdateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@ -0,0 +1,119 @@
//
// 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 GroupResponseDto {
/// Returns a new [GroupResponseDto] instance.
GroupResponseDto({
required this.description,
required this.id,
required this.name,
});
String? description;
String id;
String name;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupResponseDto &&
other.description == description &&
other.id == id &&
other.name == name;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(id.hashCode) +
(name.hashCode);
@override
String toString() => 'GroupResponseDto[description=$description, id=$id, name=$name]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'id'] = this.id;
json[r'name'] = this.name;
return json;
}
/// Returns a new [GroupResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupResponseDto? fromJson(dynamic value) {
upgradeDto(value, "GroupResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupResponseDto(
description: mapValueOfType<String>(json, r'description'),
id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!,
);
}
return null;
}
static List<GroupResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupResponseDto> mapFromJson(dynamic json) {
final map = <String, GroupResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupResponseDto-objects as value to a dart map
static Map<String, List<GroupResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'description',
'id',
'name',
};
}

View File

@ -0,0 +1,99 @@
//
// 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 GroupUserCreateAllDto {
/// Returns a new [GroupUserCreateAllDto] instance.
GroupUserCreateAllDto({
this.users = const [],
});
List<GroupUserDto> users;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupUserCreateAllDto &&
_deepEquality.equals(other.users, users);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(users.hashCode);
@override
String toString() => 'GroupUserCreateAllDto[users=$users]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'users'] = this.users;
return json;
}
/// Returns a new [GroupUserCreateAllDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupUserCreateAllDto? fromJson(dynamic value) {
upgradeDto(value, "GroupUserCreateAllDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupUserCreateAllDto(
users: GroupUserDto.listFromJson(json[r'users']),
);
}
return null;
}
static List<GroupUserCreateAllDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupUserCreateAllDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupUserCreateAllDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupUserCreateAllDto> mapFromJson(dynamic json) {
final map = <String, GroupUserCreateAllDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupUserCreateAllDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupUserCreateAllDto-objects as value to a dart map
static Map<String, List<GroupUserCreateAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupUserCreateAllDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupUserCreateAllDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'users',
};
}

View 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 GroupUserDeleteAllDto {
/// Returns a new [GroupUserDeleteAllDto] instance.
GroupUserDeleteAllDto({
this.userIds = const [],
});
List<String> userIds;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupUserDeleteAllDto &&
_deepEquality.equals(other.userIds, userIds);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(userIds.hashCode);
@override
String toString() => 'GroupUserDeleteAllDto[userIds=$userIds]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'userIds'] = this.userIds;
return json;
}
/// Returns a new [GroupUserDeleteAllDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupUserDeleteAllDto? fromJson(dynamic value) {
upgradeDto(value, "GroupUserDeleteAllDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupUserDeleteAllDto(
userIds: json[r'userIds'] is Iterable
? (json[r'userIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
);
}
return null;
}
static List<GroupUserDeleteAllDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupUserDeleteAllDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupUserDeleteAllDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupUserDeleteAllDto> mapFromJson(dynamic json) {
final map = <String, GroupUserDeleteAllDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupUserDeleteAllDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupUserDeleteAllDto-objects as value to a dart map
static Map<String, List<GroupUserDeleteAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupUserDeleteAllDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupUserDeleteAllDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'userIds',
};
}

View File

@ -0,0 +1,99 @@
//
// 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 GroupUserDto {
/// Returns a new [GroupUserDto] instance.
GroupUserDto({
required this.userId,
});
String userId;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupUserDto &&
other.userId == userId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(userId.hashCode);
@override
String toString() => 'GroupUserDto[userId=$userId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'userId'] = this.userId;
return json;
}
/// Returns a new [GroupUserDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupUserDto? fromJson(dynamic value) {
upgradeDto(value, "GroupUserDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupUserDto(
userId: mapValueOfType<String>(json, r'userId')!,
);
}
return null;
}
static List<GroupUserDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupUserDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupUserDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupUserDto> mapFromJson(dynamic json) {
final map = <String, GroupUserDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupUserDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupUserDto-objects as value to a dart map
static Map<String, List<GroupUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupUserDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupUserDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'userId',
};
}

View File

@ -0,0 +1,107 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class GroupUserMetadata {
/// Returns a new [GroupUserMetadata] instance.
GroupUserMetadata({
required this.createdAt,
required this.updatedAt,
});
DateTime createdAt;
DateTime updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupUserMetadata &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'GroupUserMetadata[createdAt=$createdAt, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json;
}
/// Returns a new [GroupUserMetadata] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupUserMetadata? fromJson(dynamic value) {
upgradeDto(value, "GroupUserMetadata");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupUserMetadata(
createdAt: mapDateTime(json, r'createdAt', r'')!,
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
);
}
return null;
}
static List<GroupUserMetadata> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupUserMetadata>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupUserMetadata.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupUserMetadata> mapFromJson(dynamic json) {
final map = <String, GroupUserMetadata>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupUserMetadata.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupUserMetadata-objects as value to a dart map
static Map<String, List<GroupUserMetadata>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupUserMetadata>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupUserMetadata.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'createdAt',
'updatedAt',
};
}

View File

@ -0,0 +1,147 @@
//
// 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 GroupUserResponseDto {
/// Returns a new [GroupUserResponseDto] instance.
GroupUserResponseDto({
required this.avatarColor,
required this.email,
required this.id,
required this.metadata,
required this.name,
required this.profileChangedAt,
required this.profileImagePath,
});
UserAvatarColor avatarColor;
String email;
String id;
GroupUserMetadata metadata;
String name;
DateTime profileChangedAt;
String profileImagePath;
@override
bool operator ==(Object other) => identical(this, other) || other is GroupUserResponseDto &&
other.avatarColor == avatarColor &&
other.email == email &&
other.id == id &&
other.metadata == metadata &&
other.name == name &&
other.profileChangedAt == profileChangedAt &&
other.profileImagePath == profileImagePath;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(avatarColor.hashCode) +
(email.hashCode) +
(id.hashCode) +
(metadata.hashCode) +
(name.hashCode) +
(profileChangedAt.hashCode) +
(profileImagePath.hashCode);
@override
String toString() => 'GroupUserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, metadata=$metadata, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor;
json[r'email'] = this.email;
json[r'id'] = this.id;
json[r'metadata'] = this.metadata;
json[r'name'] = this.name;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath;
return json;
}
/// Returns a new [GroupUserResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GroupUserResponseDto? fromJson(dynamic value) {
upgradeDto(value, "GroupUserResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return GroupUserResponseDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!,
metadata: GroupUserMetadata.fromJson(json[r'metadata'])!,
name: mapValueOfType<String>(json, r'name')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
);
}
return null;
}
static List<GroupUserResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <GroupUserResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GroupUserResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GroupUserResponseDto> mapFromJson(dynamic json) {
final map = <String, GroupUserResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GroupUserResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GroupUserResponseDto-objects as value to a dart map
static Map<String, List<GroupUserResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GroupUserResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = GroupUserResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'avatarColor',
'email',
'id',
'metadata',
'name',
'profileChangedAt',
'profileImagePath',
};
}

View File

@ -54,6 +54,10 @@ class Permission {
static const albumUserPeriodCreate = Permission._(r'albumUser.create');
static const albumUserPeriodUpdate = Permission._(r'albumUser.update');
static const albumUserPeriodDelete = Permission._(r'albumUser.delete');
static const albumGroupPeriodCreate = Permission._(r'albumGroup.create');
static const albumGroupPeriodRead = Permission._(r'albumGroup.read');
static const albumGroupPeriodUpdate = Permission._(r'albumGroup.update');
static const albumGroupPeriodDelete = Permission._(r'albumGroup.delete');
static const authPeriodChangePassword = Permission._(r'auth.changePassword');
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
static const archivePeriodRead = Permission._(r'archive.read');
@ -63,6 +67,8 @@ class Permission {
static const facePeriodRead = Permission._(r'face.read');
static const facePeriodUpdate = Permission._(r'face.update');
static const facePeriodDelete = Permission._(r'face.delete');
static const groupPeriodRead = Permission._(r'group.read');
static const groupPeriodDelete = Permission._(r'group.delete');
static const jobPeriodCreate = Permission._(r'job.create');
static const jobPeriodRead = Permission._(r'job.read');
static const libraryPeriodCreate = Permission._(r'library.create');
@ -145,6 +151,14 @@ class Permission {
static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read');
static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update');
static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete');
static const adminGroupPeriodCreate = Permission._(r'adminGroup.create');
static const adminGroupPeriodRead = Permission._(r'adminGroup.read');
static const adminGroupPeriodUpdate = Permission._(r'adminGroup.update');
static const adminGroupPeriodDelete = Permission._(r'adminGroup.delete');
static const adminGroupUserPeriodCreate = Permission._(r'adminGroupUser.create');
static const adminGroupUserPeriodRead = Permission._(r'adminGroupUser.read');
static const adminGroupUserPeriodUpdate = Permission._(r'adminGroupUser.update');
static const adminGroupUserPeriodDelete = Permission._(r'adminGroupUser.delete');
static const adminUserPeriodCreate = Permission._(r'adminUser.create');
static const adminUserPeriodRead = Permission._(r'adminUser.read');
static const adminUserPeriodUpdate = Permission._(r'adminUser.update');
@ -183,6 +197,10 @@ class Permission {
albumUserPeriodCreate,
albumUserPeriodUpdate,
albumUserPeriodDelete,
albumGroupPeriodCreate,
albumGroupPeriodRead,
albumGroupPeriodUpdate,
albumGroupPeriodDelete,
authPeriodChangePassword,
authDevicePeriodDelete,
archivePeriodRead,
@ -192,6 +210,8 @@ class Permission {
facePeriodRead,
facePeriodUpdate,
facePeriodDelete,
groupPeriodRead,
groupPeriodDelete,
jobPeriodCreate,
jobPeriodRead,
libraryPeriodCreate,
@ -274,6 +294,14 @@ class Permission {
userProfileImagePeriodRead,
userProfileImagePeriodUpdate,
userProfileImagePeriodDelete,
adminGroupPeriodCreate,
adminGroupPeriodRead,
adminGroupPeriodUpdate,
adminGroupPeriodDelete,
adminGroupUserPeriodCreate,
adminGroupUserPeriodRead,
adminGroupUserPeriodUpdate,
adminGroupUserPeriodDelete,
adminUserPeriodCreate,
adminUserPeriodRead,
adminUserPeriodUpdate,
@ -347,6 +375,10 @@ class PermissionTypeTransformer {
case r'albumUser.create': return Permission.albumUserPeriodCreate;
case r'albumUser.update': return Permission.albumUserPeriodUpdate;
case r'albumUser.delete': return Permission.albumUserPeriodDelete;
case r'albumGroup.create': return Permission.albumGroupPeriodCreate;
case r'albumGroup.read': return Permission.albumGroupPeriodRead;
case r'albumGroup.update': return Permission.albumGroupPeriodUpdate;
case r'albumGroup.delete': return Permission.albumGroupPeriodDelete;
case r'auth.changePassword': return Permission.authPeriodChangePassword;
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
case r'archive.read': return Permission.archivePeriodRead;
@ -356,6 +388,8 @@ class PermissionTypeTransformer {
case r'face.read': return Permission.facePeriodRead;
case r'face.update': return Permission.facePeriodUpdate;
case r'face.delete': return Permission.facePeriodDelete;
case r'group.read': return Permission.groupPeriodRead;
case r'group.delete': return Permission.groupPeriodDelete;
case r'job.create': return Permission.jobPeriodCreate;
case r'job.read': return Permission.jobPeriodRead;
case r'library.create': return Permission.libraryPeriodCreate;
@ -438,6 +472,14 @@ class PermissionTypeTransformer {
case r'userProfileImage.read': return Permission.userProfileImagePeriodRead;
case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate;
case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete;
case r'adminGroup.create': return Permission.adminGroupPeriodCreate;
case r'adminGroup.read': return Permission.adminGroupPeriodRead;
case r'adminGroup.update': return Permission.adminGroupPeriodUpdate;
case r'adminGroup.delete': return Permission.adminGroupPeriodDelete;
case r'adminGroupUser.create': return Permission.adminGroupUserPeriodCreate;
case r'adminGroupUser.read': return Permission.adminGroupUserPeriodRead;
case r'adminGroupUser.update': return Permission.adminGroupUserPeriodUpdate;
case r'adminGroupUser.delete': return Permission.adminGroupUserPeriodDelete;
case r'adminUser.create': return Permission.adminUserPeriodCreate;
case r'adminUser.read': return Permission.adminUserPeriodRead;
case r'adminUser.update': return Permission.adminUserPeriodUpdate;

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,44 @@ export type ActivityStatisticsResponseDto = {
comments: number;
likes: number;
};
export type GroupAdminResponseDto = {
createdAt: string;
description: string | null;
id: string;
name: string;
updatedAt: string;
};
export type GroupUserDto = {
userId: string;
};
export type GroupAdminCreateDto = {
description?: string | null;
name: string;
users?: GroupUserDto[];
};
export type GroupAdminUpdateDto = {
description?: string | null;
name?: string;
};
export type GroupUserDeleteAllDto = {
userIds: string[];
};
export type GroupUserMetadata = {
createdAt: string;
updatedAt: string;
};
export type GroupUserResponseDto = {
avatarColor: UserAvatarColor;
email: string;
id: string;
metadata: GroupUserMetadata;
name: string;
profileChangedAt: string;
profileImagePath: string;
};
export type GroupUserCreateAllDto = {
users: GroupUserDto[];
};
export type NotificationCreateDto = {
data?: object;
description?: string | null;
@ -378,11 +416,16 @@ export type AlbumUserCreateDto = {
role: AlbumUserRole;
userId: string;
};
export type AlbumGroupDto = {
groupId: string;
role?: AlbumUserRole;
};
export type CreateAlbumDto = {
albumName: string;
albumUsers?: AlbumUserCreateDto[];
assetIds?: string[];
description?: string;
groups?: AlbumGroupDto[];
};
export type AlbumStatisticsResponseDto = {
notShared: number;
@ -404,6 +447,25 @@ export type BulkIdResponseDto = {
id: string;
success: boolean;
};
export type AlbumGroupDeleteAllDto = {
groupIds: string[];
};
export type AlbumGroupMetadata = {
createdAt: string;
updatedAt: string;
};
export type AlbumGroupResponseDto = {
description: string | null;
id: string;
metadata: AlbumGroupMetadata;
name: string;
};
export type AlbumGroupCreateAllDto = {
groups: AlbumGroupDto[];
};
export type AlbumGroupUpdateDto = {
role: AlbumUserRole;
};
export type UpdateAlbumUserDto = {
role: AlbumUserRole;
};
@ -627,6 +689,11 @@ export type AssetFaceDeleteDto = {
export type FaceDto = {
id: string;
};
export type GroupResponseDto = {
description: string | null;
id: string;
name: string;
};
export type JobCountsDto = {
active: number;
completed: number;
@ -1644,6 +1711,132 @@ export function deleteActivity({ id }: {
method: "DELETE"
}));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroup.read` permission.
*/
export function searchGroupsAdmin({ id, userId }: {
id?: string;
userId?: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupAdminResponseDto[];
}>(`/admin/groups${QS.query(QS.explode({
id,
userId
}))}`, {
...opts
}));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroup.create` permission.
*/
export function createGroupAdmin({ groupAdminCreateDto }: {
groupAdminCreateDto: GroupAdminCreateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: GroupAdminResponseDto;
}>("/admin/groups", oazapfts.json({
...opts,
method: "POST",
body: groupAdminCreateDto
})));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroup.delete` permission.
*/
export function deleteGroupAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/admin/groups/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroup.read` permission.
*/
export function getGroupAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupAdminResponseDto;
}>(`/admin/groups/${encodeURIComponent(id)}`, {
...opts
}));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroup.update` permission.
*/
export function updateGroupAdmin({ id, groupAdminUpdateDto }: {
id: string;
groupAdminUpdateDto: GroupAdminUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupAdminResponseDto;
}>(`/admin/groups/${encodeURIComponent(id)}`, oazapfts.json({
...opts,
method: "PUT",
body: groupAdminUpdateDto
})));
}
/**
* This endpoint requires the `adminGroupUser.delete` permission.
*/
export function removeUsersFromGroupAdmin({ id, groupUserDeleteAllDto }: {
id: string;
groupUserDeleteAllDto: GroupUserDeleteAllDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/admin/groups/${encodeURIComponent(id)}/user`, oazapfts.json({
...opts,
method: "DELETE",
body: groupUserDeleteAllDto
})));
}
/**
* This endpoint requires the `adminGroupUser.delete` permission.
*/
export function removeUserFromGroupAdmin({ id, userId }: {
id: string;
userId: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/admin/groups/${encodeURIComponent(id)}/user/${encodeURIComponent(userId)}`, {
...opts,
method: "DELETE"
}));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroupUser.read` permission.
*/
export function getUsersForGroupAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupUserResponseDto[];
}>(`/admin/groups/${encodeURIComponent(id)}/users`, {
...opts
}));
}
/**
* This endpoint is an admin-only route, and requires the `adminGroupUser.create` permission.
*/
export function addUsersToGroupAdmin({ id, groupUserCreateAllDto }: {
id: string;
groupUserCreateAllDto: GroupUserCreateAllDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupUserResponseDto[];
}>(`/admin/groups/${encodeURIComponent(id)}/users`, oazapfts.json({
...opts,
method: "PUT",
body: groupUserCreateAllDto
})));
}
export function createNotification({ notificationCreateDto }: {
notificationCreateDto: NotificationCreateDto;
}, opts?: Oazapfts.RequestOpts) {
@ -1948,6 +2141,65 @@ export function addAssetsToAlbum({ id, key, slug, bulkIdsDto }: {
body: bulkIdsDto
})));
}
/**
* This endpoint requires the `albumGroup.delete` permission.
*/
export function removeGroupsFromAlbum({ id, albumGroupDeleteAllDto }: {
id: string;
albumGroupDeleteAllDto: AlbumGroupDeleteAllDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/albums/${encodeURIComponent(id)}/groups`, oazapfts.json({
...opts,
method: "DELETE",
body: albumGroupDeleteAllDto
})));
}
/**
* This endpoint requires the `albumGroup.read` permission.
*/
export function getGroupsForAlbum({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AlbumGroupResponseDto[];
}>(`/albums/${encodeURIComponent(id)}/groups`, {
...opts
}));
}
/**
* This endpoint requires the `albumGroup.create` permission.
*/
export function addGroupsToAlbum({ id, albumGroupCreateAllDto }: {
id: string;
albumGroupCreateAllDto: AlbumGroupCreateAllDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AlbumGroupResponseDto[];
}>(`/albums/${encodeURIComponent(id)}/groups`, oazapfts.json({
...opts,
method: "PUT",
body: albumGroupCreateAllDto
})));
}
/**
* This endpoint requires the `albumGroup.update` permission.
*/
export function updateAlbumGroup({ groupId, id, albumGroupUpdateDto }: {
groupId: string;
id: string;
albumGroupUpdateDto: AlbumGroupUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AlbumGroupResponseDto;
}>(`/albums/${encodeURIComponent(id)}/groups/${encodeURIComponent(groupId)}`, oazapfts.json({
...opts,
method: "PUT",
body: albumGroupUpdateDto
})));
}
/**
* This endpoint requires the `albumUser.delete` permission.
*/
@ -2547,6 +2799,54 @@ export function reassignFacesById({ id, faceDto }: {
body: faceDto
})));
}
/**
* This endpoint requires the `group.read` permission.
*/
export function searchMyGroups(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupResponseDto[];
}>("/groups", {
...opts
}));
}
/**
* This endpoint requires the `group.delete` permission.
*/
export function leaveMyGroup({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/groups/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
/**
* This endpoint requires the `group.read` permission.
*/
export function getMyGroup({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupResponseDto;
}>(`/groups/${encodeURIComponent(id)}`, {
...opts
}));
}
/**
* This endpoint is an admin-only route, and requires the `group.read` permission.
*/
export function getMyGroupUsers({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: GroupUserResponseDto[];
}>(`/groups/${encodeURIComponent(id)}/users`, {
...opts
}));
}
/**
* This endpoint is an admin-only route, and requires the `job.read` permission.
*/
@ -4569,6 +4869,10 @@ export enum Permission {
AlbumUserCreate = "albumUser.create",
AlbumUserUpdate = "albumUser.update",
AlbumUserDelete = "albumUser.delete",
AlbumGroupCreate = "albumGroup.create",
AlbumGroupRead = "albumGroup.read",
AlbumGroupUpdate = "albumGroup.update",
AlbumGroupDelete = "albumGroup.delete",
AuthChangePassword = "auth.changePassword",
AuthDeviceDelete = "authDevice.delete",
ArchiveRead = "archive.read",
@ -4578,6 +4882,8 @@ export enum Permission {
FaceRead = "face.read",
FaceUpdate = "face.update",
FaceDelete = "face.delete",
GroupRead = "group.read",
GroupDelete = "group.delete",
JobCreate = "job.create",
JobRead = "job.read",
LibraryCreate = "library.create",
@ -4660,6 +4966,14 @@ export enum Permission {
UserProfileImageRead = "userProfileImage.read",
UserProfileImageUpdate = "userProfileImage.update",
UserProfileImageDelete = "userProfileImage.delete",
AdminGroupCreate = "adminGroup.create",
AdminGroupRead = "adminGroup.read",
AdminGroupUpdate = "adminGroup.update",
AdminGroupDelete = "adminGroup.delete",
AdminGroupUserCreate = "adminGroupUser.create",
AdminGroupUserRead = "adminGroupUser.read",
AdminGroupUserUpdate = "adminGroupUser.update",
AdminGroupUserDelete = "adminGroupUser.delete",
AdminUserCreate = "adminUser.create",
AdminUserRead = "adminUser.read",
AdminUserUpdate = "adminUser.update",

View File

@ -1,5 +1,11 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
AlbumGroupCreateAllDto,
AlbumGroupDeleteAllDto,
AlbumGroupResponseDto,
AlbumGroupUpdateDto,
} from 'src/dtos/album-group.dto';
import {
AddUsersDto,
AlbumInfoDto,
@ -15,7 +21,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AlbumService } from 'src/services/album.service';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
import { GroupIdAndIdParamDto, ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ApiTags('Albums')
@Controller('albums')
@ -86,6 +92,43 @@ export class AlbumController {
return this.service.removeAssets(auth, id, dto);
}
@Get(':id/groups')
@Authenticated({ permission: Permission.AlbumGroupRead })
getGroupsForAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AlbumGroupResponseDto[]> {
return this.service.getGroups(auth, id);
}
@Put(':id/groups')
@Authenticated({ permission: Permission.AlbumGroupCreate })
addGroupsToAlbum(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AlbumGroupCreateAllDto,
): Promise<AlbumGroupResponseDto[]> {
return this.service.upsertGroups(auth, id, dto);
}
@Delete(':id/groups')
@Authenticated({ permission: Permission.AlbumGroupDelete })
@HttpCode(HttpStatus.NO_CONTENT)
removeGroupsFromAlbum(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AlbumGroupDeleteAllDto,
): Promise<void> {
return this.service.removeGroups(auth, id, dto);
}
@Put(':id/groups/:groupId')
@Authenticated({ permission: Permission.AlbumGroupUpdate })
updateAlbumGroup(
@Auth() auth: AuthDto,
@Param() { id, groupId }: GroupIdAndIdParamDto,
@Body() dto: AlbumGroupUpdateDto,
): Promise<AlbumGroupResponseDto> {
return this.service.updateGroup(auth, id, groupId, dto);
}
@Put(':id/users')
@Authenticated({ permission: Permission.AlbumUserCreate })
addUsersToAlbum(

View File

@ -0,0 +1,85 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { GroupUserCreateAllDto, GroupUserDeleteAllDto, GroupUserResponseDto } from 'src/dtos/group-user.dto';
import {
GroupAdminCreateDto,
GroupAdminResponseDto,
GroupAdminSearchDto,
GroupAdminUpdateDto,
} from 'src/dtos/group.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { GroupAdminService } from 'src/services/group-admin.service';
import { UserIdAndIdParamDto, UUIDParamDto } from 'src/validation';
@ApiTags('Groups (admin)')
@Controller('admin/groups')
export class GroupAdminController {
constructor(private service: GroupAdminService) {}
@Get()
@Authenticated({ permission: Permission.AdminGroupRead, admin: true })
searchGroupsAdmin(@Auth() auth: AuthDto, @Query() dto: GroupAdminSearchDto): Promise<GroupAdminResponseDto[]> {
return this.service.search(auth, dto);
}
@Post()
@Authenticated({ permission: Permission.AdminGroupCreate, admin: true })
createGroupAdmin(@Auth() auth: AuthDto, @Body() dto: GroupAdminCreateDto): Promise<GroupAdminResponseDto> {
return this.service.create(auth, dto);
}
@Get(':id')
@Authenticated({ permission: Permission.AdminGroupRead, admin: true })
getGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupAdminResponseDto> {
return this.service.get(auth, id);
}
@Put(':id')
@Authenticated({ permission: Permission.AdminGroupUpdate, admin: true })
updateGroupAdmin(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: GroupAdminUpdateDto,
): Promise<GroupAdminResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.AdminGroupDelete, admin: true })
@HttpCode(HttpStatus.NO_CONTENT)
deleteGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
@Get(':id/users')
@Authenticated({ permission: Permission.AdminGroupUserRead, admin: true })
getUsersForGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupUserResponseDto[]> {
return this.service.getUsers(auth, id);
}
@Put(':id/users')
@Authenticated({ permission: Permission.AdminGroupUserCreate, admin: true })
addUsersToGroupAdmin(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: GroupUserCreateAllDto,
): Promise<GroupUserResponseDto[]> {
return this.service.addUsers(auth, id, dto);
}
@Delete(':id/user')
@Authenticated({ permission: Permission.AdminGroupUserDelete })
@HttpCode(HttpStatus.NO_CONTENT)
removeUsersFromGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: GroupUserDeleteAllDto) {
return this.service.removeUsers(auth, id, dto);
}
@Delete(':id/user/:userId')
@Authenticated({ permission: Permission.AdminGroupUserDelete })
@HttpCode(HttpStatus.NO_CONTENT)
removeUserFromGroupAdmin(@Auth() auth: AuthDto, @Param() { id, userId }: UserIdAndIdParamDto) {
return this.service.removeUser(auth, id, userId);
}
}

View File

@ -0,0 +1,40 @@
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { GroupUserResponseDto } from 'src/dtos/group-user.dto';
import { GroupResponseDto } from 'src/dtos/group.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { GroupService } from 'src/services/group.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Groups')
@Controller('groups')
export class GroupController {
constructor(private service: GroupService) {}
@Get()
@Authenticated({ permission: Permission.GroupRead })
searchMyGroups(@Auth() auth: AuthDto): Promise<GroupResponseDto[]> {
return this.service.search(auth);
}
@Get(':id')
@Authenticated({ permission: Permission.GroupRead })
getMyGroup(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupResponseDto> {
return this.service.get(auth, id);
}
@Delete(':id')
@Authenticated({ permission: Permission.GroupDelete })
@HttpCode(HttpStatus.NO_CONTENT)
leaveMyGroup(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(auth, id);
}
@Get(':id/users')
@Authenticated({ permission: Permission.GroupRead, admin: true })
getMyGroupUsers(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupUserResponseDto[]> {
return this.service.getUsers(auth, id);
}
}

View File

@ -8,6 +8,8 @@ import { AuthController } from 'src/controllers/auth.controller';
import { DownloadController } from 'src/controllers/download.controller';
import { DuplicateController } from 'src/controllers/duplicate.controller';
import { FaceController } from 'src/controllers/face.controller';
import { GroupAdminController } from 'src/controllers/group-admin.controller';
import { GroupController } from 'src/controllers/group.controller';
import { JobController } from 'src/controllers/job.controller';
import { LibraryController } from 'src/controllers/library.controller';
import { MapController } from 'src/controllers/map.controller';
@ -43,6 +45,8 @@ export const controllers = [
DownloadController,
DuplicateController,
FaceController,
GroupController,
GroupAdminController,
JobController,
LibraryController,
MapController,

View File

@ -144,6 +144,17 @@ export type UserAdmin = User & {
metadata: UserMetadataItem[];
};
export type Group = {
id: string;
name: string;
description: string | null;
};
export type GroupAdmin = Group & {
createdAt: Date;
updatedAt: Date;
};
export type StorageAsset = {
id: string;
ownerId: string;
@ -319,6 +330,7 @@ export const columns = {
'shared_link.allowDownload',
'shared_link.password',
],
groupAdmin: ['group.id', 'group.name', 'group.description', 'group.createdAt', 'group.updatedAt'],
user: userColumns,
userWithPrefix: userWithPrefixColumns,
userAdmin: [

View File

@ -0,0 +1,56 @@
import { Type } from 'class-transformer';
import { IsArray, ValidateNested } from 'class-validator';
import { Group } from 'src/database';
import { GroupResponseDto, mapGroup } from 'src/dtos/group.dto';
import { AlbumUserRole } from 'src/enum';
import { ValidateEnum, ValidateUUID } from 'src/validation';
export class AlbumGroupCreateAllDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => AlbumGroupDto)
groups!: AlbumGroupDto[];
}
export class AlbumGroupDeleteAllDto {
@ValidateUUID({ each: true })
groupIds!: string[];
}
export class AlbumGroupDto {
@ValidateUUID()
groupId!: string;
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', optional: true })
role?: AlbumUserRole;
}
export class AlbumGroupUpdateDto {
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
role!: AlbumUserRole;
}
export class AlbumGroupResponseDto extends GroupResponseDto {
metadata!: AlbumGroupMetadata;
}
export class AlbumGroupMetadata {
createdAt!: Date;
updatedAt!: Date;
}
type AlbumGroup = {
createdAt: Date;
updatedAt: Date;
group: Group;
};
export const mapAlbumGroup = (albumGroup: AlbumGroup): AlbumGroupResponseDto => {
return {
...mapGroup(albumGroup.group),
metadata: {
createdAt: albumGroup.createdAt,
updatedAt: albumGroup.updatedAt,
},
};
};

View File

@ -3,9 +3,10 @@ import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
import _ from 'lodash';
import { AlbumUser, AuthSharedLink, User } from 'src/database';
import { AlbumGroupDto } from 'src/dtos/album-group.dto';
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
import { AlbumUserRole, AssetOrder } from 'src/enum';
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
@ -50,6 +51,12 @@ export class CreateAlbumDto {
@Type(() => AlbumUserCreateDto)
albumUsers?: AlbumUserCreateDto[];
@Optional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AlbumGroupDto)
groups?: AlbumGroupDto[];
@ValidateUUID({ optional: true, each: true })
assetIds?: string[];
}

View File

@ -0,0 +1,46 @@
import { ArrayNotEmpty } from 'class-validator';
import { User } from 'src/database';
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
import { ValidateUUID } from 'src/validation';
export class GroupUserCreateAllDto {
@ArrayNotEmpty()
users!: GroupUserDto[];
}
export class GroupUserDeleteAllDto {
@ValidateUUID({ each: true })
userIds!: string[];
}
export class GroupUserDto {
@ValidateUUID()
userId!: string;
// TODO potentially add a role UserGroupRole field here
}
export class GroupUserResponseDto extends UserResponseDto {
metadata!: GroupUserMetadata;
}
export class GroupUserMetadata {
createdAt!: Date;
updatedAt!: Date;
}
type GroupUser = {
createdAt: Date;
updatedAt: Date;
user: User;
};
export const mapGroupUser = (groupUser: GroupUser): GroupUserResponseDto => {
return {
...mapUser(groupUser.user),
metadata: {
createdAt: groupUser.createdAt,
updatedAt: groupUser.updatedAt,
},
};
};

View File

@ -0,0 +1,69 @@
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { Group, GroupAdmin } from 'src/database';
import { GroupUserDto } from 'src/dtos/group-user.dto';
import { Optional, ValidateUUID } from 'src/validation';
export class GroupAdminSearchDto {
@ValidateUUID({ optional: true })
id?: string;
@ValidateUUID({ optional: true })
userId?: string;
}
export class GroupAdminCreateDto {
@IsString()
@IsNotEmpty()
name!: string;
@Optional({ nullable: true, emptyToNull: true })
@IsNotEmpty()
@IsString()
description?: string | null;
@Optional()
@ValidateNested({ each: true })
@Type(() => GroupUserDto)
@IsArray()
users?: GroupUserDto[];
}
export class GroupAdminUpdateDto {
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@Optional({ nullable: true, emptyToNull: true })
@IsNotEmpty()
@IsString()
description?: string | null;
}
export class GroupResponseDto {
id!: string;
name!: string;
description!: string | null;
}
export class GroupAdminResponseDto extends GroupResponseDto {
createdAt!: Date;
updatedAt!: Date;
}
export const mapGroup = (group: Group | GroupAdmin) => {
return {
id: group.id,
name: group.name,
description: group.description,
};
};
export const mapGroupAdmin = (group: GroupAdmin) => {
return {
...mapGroup(group),
createdAt: group.createdAt,
updatedAt: group.updatedAt,
};
};

View File

@ -111,6 +111,11 @@ export enum Permission {
AlbumUserUpdate = 'albumUser.update',
AlbumUserDelete = 'albumUser.delete',
AlbumGroupCreate = 'albumGroup.create',
AlbumGroupRead = 'albumGroup.read',
AlbumGroupUpdate = 'albumGroup.update',
AlbumGroupDelete = 'albumGroup.delete',
AuthChangePassword = 'auth.changePassword',
AuthDeviceDelete = 'authDevice.delete',
@ -125,6 +130,9 @@ export enum Permission {
FaceUpdate = 'face.update',
FaceDelete = 'face.delete',
GroupRead = 'group.read',
GroupDelete = 'group.delete',
JobCreate = 'job.create',
JobRead = 'job.read',
@ -230,6 +238,16 @@ export enum Permission {
UserProfileImageUpdate = 'userProfileImage.update',
UserProfileImageDelete = 'userProfileImage.delete',
AdminGroupCreate = 'adminGroup.create',
AdminGroupRead = 'adminGroup.read',
AdminGroupUpdate = 'adminGroup.update',
AdminGroupDelete = 'adminGroup.delete',
AdminGroupUserCreate = 'adminGroupUser.create',
AdminGroupUserRead = 'adminGroupUser.read',
AdminGroupUserUpdate = 'adminGroupUser.update',
AdminGroupUserDelete = 'adminGroupUser.delete',
AdminUserCreate = 'adminUser.create',
AdminUserRead = 'adminUser.read',
AdminUserUpdate = 'adminUser.update',

View File

@ -0,0 +1,87 @@
-- NOTE: This file is auto generated by ./sql-generator
-- AlbumGroupRepository.getAll
select
(
select
to_json(obj)
from
(
select
"group"."id",
"group"."name",
"group"."description"
from
"group"
where
"group"."id" = "album_group"."groupId"
) as obj
) as "group",
"album_group"."createdAt",
"album_group"."updatedAt"
from
"album_group"
inner join "group" on "album_group"."groupId" = "group"."id"
where
"albumId" = $1
order by
"group"."name"
-- AlbumGroupRepository.createAll
insert into
"album_group" ("albumId", "groupId", "role")
values
($1, $2, $3)
on conflict ("albumId", "groupId") do update
set
"role" = "excluded"."role"
returning
"createdAt",
"updatedAt",
(
select
to_json(obj)
from
(
select
"id",
"name",
"description"
from
"group"
where
"album_group"."groupId" = "group"."id"
) as obj
) as "group"
-- AlbumGroupRepository.deleteAll
delete from "album_group"
where
"albumId" = $1
and "groupId" in ($2)
-- AlbumGroupRepository.update
update "album_group"
set
"role" = $1
where
"albumId" = $2
and "groupId" = $3
returning
"createdAt",
"updatedAt",
(
select
to_json(obj)
from
(
select
"id",
"name",
"description"
from
"group"
where
"album_group"."groupId" = "group"."id"
) as obj
) as "group"

View File

@ -0,0 +1,11 @@
-- NOTE: This file is auto generated by ./sql-generator
-- GroupRepository.search
select
"id",
"name",
"description",
"createdAt",
"updatedAt"
from
"group"

View File

@ -0,0 +1,65 @@
-- NOTE: This file is auto generated by ./sql-generator
-- GroupUserRepository.getAll
select
(
select
to_json(obj)
from
(
select
"user2"."id",
"user2"."name",
"user2"."email",
"user2"."avatarColor",
"user2"."profileImagePath",
"user2"."profileChangedAt"
from
"user" as "user2"
where
"user2"."id" = "group_user"."userId"
) as obj
) as "user",
"group_user"."createdAt",
"group_user"."updatedAt"
from
"group_user"
inner join "user" on "group_user"."userId" = "user"."id"
where
"groupId" = $1
order by
"user"."name"
-- GroupUserRepository.createAll
insert into
"group_user" ("userId", "groupId")
values
($1, $2)
on conflict do nothing
returning
"createdAt",
"updatedAt",
(
select
to_json(obj)
from
(
select
"user2"."id",
"user2"."name",
"user2"."email",
"user2"."avatarColor",
"user2"."profileImagePath",
"user2"."profileChangedAt"
from
"user" as "user2"
where
"group_user"."userId" = "user2"."id"
) as obj
) as "user"
-- GroupUserRepository.deleteAll
delete from "group_user"
where
"groupId" = $1
and "userId" in ($2)

View File

@ -0,0 +1,98 @@
import { Injectable } from '@nestjs/common';
import { Kysely, NotNull, Updateable } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumGroupDto } from 'src/dtos/album-group.dto';
import { AlbumUserRole } from 'src/enum';
import { DB } from 'src/schema';
import { AlbumGroupTable } from 'src/schema/tables/album-group.table';
type AlbumGroup = { albumId: string; groupId: string };
@Injectable()
export class AlbumGroupRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID] })
getAll(albumId: string) {
return this.db
.selectFrom('album_group')
.where('albumId', '=', albumId)
.innerJoin('group', 'album_group.groupId', 'group.id')
.orderBy('group.name')
.select((eb) =>
jsonObjectFrom(
eb
.selectFrom('group')
.select(['group.id', 'group.name', 'group.description'])
.whereRef('group.id', '=', 'album_group.groupId'),
).as('group'),
)
.$narrowType<{ group: NotNull }>()
.select(['album_group.createdAt', 'album_group.updatedAt'])
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, [{ groupId: DummyValue.UUID, role: DummyValue.STRING }]] })
@Chunked({ paramIndex: 1 })
createAll(albumId: string, groups: AlbumGroupDto[]) {
return this.db
.insertInto('album_group')
.values(groups.map(({ groupId, role }) => ({ albumId, groupId, role: role ?? AlbumUserRole.Editor })))
.onConflict((oc) =>
oc.columns(['albumId', 'groupId']).doUpdateSet((eb) => ({
role: eb.ref('excluded.role'),
})),
)
.returning(['createdAt', 'updatedAt'])
.returning((eb) =>
jsonObjectFrom(
eb.selectFrom('group').whereRef('album_group.groupId', '=', 'group.id').select(['id', 'name', 'description']),
).as('group'),
)
.$narrowType<{ group: NotNull }>()
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
@Chunked({ paramIndex: 1 })
deleteAll(albumId: string, groupIds: string[]) {
if (groupIds.length === 0) {
return Promise.resolve();
}
return this.db.deleteFrom('album_group').where('albumId', '=', albumId).where('groupId', 'in', groupIds).execute();
}
async exists({ albumId, groupId }: AlbumGroup) {
const albumGroup = await this.db
.selectFrom('album_group')
.select(['albumId'])
.where('albumId', '=', albumId)
.where('groupId', '=', groupId)
.execute();
return !!albumGroup;
}
@GenerateSql({ params: [{ albumId: DummyValue.UUID, groupId: DummyValue.UUID }, { role: DummyValue.STRING }] })
update({ albumId, groupId }: AlbumGroup, dto: Updateable<AlbumGroupTable>) {
return this.db
.updateTable('album_group')
.set(dto)
.where('albumId', '=', albumId)
.where('groupId', '=', groupId)
.returning(['createdAt', 'updatedAt'])
.returning((eb) =>
jsonObjectFrom(
eb.selectFrom('group').whereRef('album_group.groupId', '=', 'group.id').select(['id', 'name', 'description']),
).as('group'),
)
.$narrowType<{ group: NotNull }>()
.executeTakeFirstOrThrow();
}
delete({ albumId, groupId }: AlbumGroup) {
return this.db.deleteFrom('album_group').where('albumId', '=', albumId).where('groupId', '=', groupId).execute();
}
}

View File

@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { Kysely, NotNull } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
import { DB } from 'src/schema';
type GroupUser = { groupId: string; userId: string };
@Injectable()
export class GroupUserRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID] })
getAll(groupId: string) {
return this.db
.selectFrom('group_user')
.where('groupId', '=', groupId)
.innerJoin('user', 'group_user.userId', 'user.id')
.orderBy('user.name', 'asc')
.select((eb) =>
jsonObjectFrom(
eb.selectFrom('user as user2').select(columns.userWithPrefix).whereRef('user2.id', '=', 'group_user.userId'),
).as('user'),
)
.$narrowType<{ user: NotNull }>()
.select(['group_user.createdAt', 'group_user.updatedAt'])
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
@Chunked({ paramIndex: 1 })
createAll(groupId: string, userIds: string[]) {
return this.db
.insertInto('group_user')
.values(userIds.map((userId) => ({ userId, groupId })))
.onConflict((oc) => oc.doNothing())
.returning(['createdAt', 'updatedAt'])
.returning((eb) =>
jsonObjectFrom(
eb.selectFrom('user as user2').whereRef('group_user.userId', '=', 'user2.id').select(columns.userWithPrefix),
).as('user'),
)
.$narrowType<{ user: NotNull }>()
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
@Chunked({ paramIndex: 1 })
deleteAll(groupId: string, usersIds: string[]) {
if (usersIds.length === 0) {
return Promise.resolve();
}
return this.db.deleteFrom('group_user').where('groupId', '=', groupId).where('userId', 'in', usersIds).execute();
}
get({ groupId, userId }: GroupUser) {
return this.db
.selectFrom('group_user')
.innerJoin('group', 'group.id', 'group_user.groupId')
.select(['group.id', 'group.name', 'group.description'])
.where('group_user.groupId', '=', groupId)
.where('group_user.userId', '=', userId)
.execute();
}
delete({ userId, groupId }: GroupUser) {
return this.db.deleteFrom('group_user').where('userId', '=', userId).where('groupId', '=', groupId).execute();
}
}

View File

@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { Insertable, Kysely, Updateable } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { GenerateSql } from 'src/decorators';
import { GroupUserDto } from 'src/dtos/group-user.dto';
import { DB } from 'src/schema';
import { GroupTable } from 'src/schema/tables/group.table';
@Injectable()
export class GroupRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql()
search(options: { id?: string; userId?: string } = {}) {
const { id, userId } = options;
return this.db
.selectFrom('group')
.select(['group.id', 'group.name', 'group.description', 'group.createdAt', 'group.updatedAt'])
.$if(!!id, (eb) => eb.where('group.id', '=', id!))
.$if(!!userId, (eb) =>
eb.innerJoin('group_user', 'group_user.groupId', 'group.id').where('group_user.userId', '=', userId!),
)
.orderBy('group.name', 'asc')
.execute();
}
create(group: Insertable<GroupTable>, users?: GroupUserDto[]) {
return this.db.transaction().execute(async (tx) => {
const newGroup = await tx
.insertInto('group')
.values(group)
.returning(columns.groupAdmin)
.executeTakeFirstOrThrow();
const groupId = newGroup.id;
if (users && users.length > 0) {
await tx
.insertInto('group_user')
.values(users.map(({ userId }) => ({ groupId, userId })))
.execute();
}
return newGroup;
});
}
get(id: string) {
return this.db.selectFrom('group').select(columns.groupAdmin).where('id', '=', id).executeTakeFirst();
}
update(id: string, group: Updateable<GroupTable>) {
return this.db
.updateTable('group')
.set(group)
.where('id', '=', id)
.returning(columns.groupAdmin)
.executeTakeFirstOrThrow();
}
delete(id: string) {
return this.db.deleteFrom('group').where('id', '=', id).executeTakeFirstOrThrow();
}
}

View File

@ -1,5 +1,6 @@
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumGroupRepository } from 'src/repositories/album-group.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
@ -14,6 +15,8 @@ import { DownloadRepository } from 'src/repositories/download.repository';
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
import { EmailRepository } from 'src/repositories/email.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { GroupUserRepository } from 'src/repositories/group-user.repository';
import { GroupRepository } from 'src/repositories/group.repository';
import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
@ -48,6 +51,7 @@ export const repositories = [
AccessRepository,
ActivityRepository,
AlbumRepository,
AlbumGroupRepository,
AlbumUserRepository,
AuditRepository,
ApiKeyRepository,
@ -61,6 +65,8 @@ export const repositories = [
DuplicateRepository,
EmailRepository,
EventRepository,
GroupRepository,
GroupUserRepository,
JobRepository,
LibraryRepository,
LoggingRepository,

View File

@ -165,6 +165,46 @@ export const album_user_delete_audit = registerFunction({
END`,
});
export const album_group_delete_audit = registerFunction({
name: 'album_group_delete_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO "album_audit" ("albumId", "userId")
SELECT OLD."albumId", "group_user"."userId"
FROM OLD INNER JOIN "group_user" ON "group_user"."groupId" = OLD."groupId";
IF pg_trigger_depth() = 1 THEN
INSERT INTO album_group_audit ("albumId", "groupId")
SELECT "albumId", "groupId"
FROM OLD;
END IF;
RETURN NULL;
END`,
});
export const group_user_delete_audit = registerFunction({
name: 'group_user_delete_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO group_audit ("groupId", "userId")
SELECT "groupId", "userId"
FROM OLD;
IF pg_trigger_depth() = 1 THEN
INSERT INTO group_user_audit ("groupId", "userId")
SELECT "groupId", "userId"
FROM OLD;
END IF;
RETURN NULL;
END`,
});
export const memory_delete_audit = registerFunction({
name: 'memory_delete_audit',
returnType: 'TRIGGER',

View File

@ -22,6 +22,8 @@ import { ActivityTable } from 'src/schema/tables/activity.table';
import { AlbumAssetAuditTable } from 'src/schema/tables/album-asset-audit.table';
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
import { AlbumAuditTable } from 'src/schema/tables/album-audit.table';
import { AlbumGroupAuditTable } from 'src/schema/tables/album-group-audit.table';
import { AlbumGroupTable } from 'src/schema/tables/album-group.table';
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
import { AlbumTable } from 'src/schema/tables/album.table';
@ -36,6 +38,10 @@ import { AssetTable } from 'src/schema/tables/asset.table';
import { AuditTable } from 'src/schema/tables/audit.table';
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
import { GroupAuditTable } from 'src/schema/tables/group-audit.table';
import { GroupUserAuditTable } from 'src/schema/tables/group-user-audit.table';
import { GroupUserTable } from 'src/schema/tables/group-user.table';
import { GroupTable } from 'src/schema/tables/group.table';
import { LibraryTable } from 'src/schema/tables/library.table';
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table';
@ -71,12 +77,14 @@ import { Database, Extensions, Generated, Int8 } from 'src/sql-tools';
export class ImmichDatabase {
tables = [
ActivityTable,
AlbumAssetTable,
AlbumAssetAuditTable,
AlbumAuditTable,
AlbumTable,
AlbumGroupTable,
AlbumGroupAuditTable,
AlbumUserAuditTable,
AlbumUserTable,
AlbumTable,
AlbumAssetTable,
AlbumAssetAuditTable,
ApiKeyTable,
AssetAuditTable,
AssetFaceTable,
@ -88,6 +96,10 @@ export class ImmichDatabase {
AssetExifTable,
FaceSearchTable,
GeodataPlacesTable,
GroupTable,
GroupAuditTable,
GroupUserTable,
GroupUserAuditTable,
LibraryTable,
MemoryTable,
MemoryAuditTable,
@ -154,6 +166,8 @@ export interface DB {
album_audit: AlbumAuditTable;
album_asset: AlbumAssetTable;
album_asset_audit: AlbumAssetAuditTable;
album_group: AlbumGroupTable;
album_group_audit: AlbumGroupAuditTable;
album_user: AlbumUserTable;
album_user_audit: AlbumUserAuditTable;
@ -173,6 +187,11 @@ export interface DB {
geodata_places: GeodataPlacesTable;
group: GroupTable;
group_audit: GroupAuditTable;
group_user: GroupUserTable;
group_user_audit: GroupUserAuditTable;
library: LibraryTable;
memory: MemoryTable;

View File

@ -0,0 +1,118 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE OR REPLACE FUNCTION group_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO group_audit ("groupId")
SELECT "id"
FROM OLD;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE OR REPLACE FUNCTION group_user_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO group_audit ("groupId", "userId")
SELECT "groupId", "userId"
FROM OLD;
IF pg_trigger_depth() = 1 THEN
INSERT INTO group_user_audit ("groupId", "userId")
SELECT "groupId", "userId"
FROM OLD;
END IF;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE TABLE "group_audit" (
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
"groupId" uuid NOT NULL,
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT "group_audit_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "group_audit_deletedAt_idx" ON "group_audit" ("deletedAt");`.execute(db);
await sql`CREATE TABLE "group_user_audit" (
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
"groupId" uuid NOT NULL,
"userId" uuid NOT NULL,
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT "group_user_audit_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "group_user_audit_groupId_idx" ON "group_user_audit" ("groupId");`.execute(db);
await sql`CREATE INDEX "group_user_audit_userId_idx" ON "group_user_audit" ("userId");`.execute(db);
await sql`CREATE INDEX "group_user_audit_deletedAt_idx" ON "group_user_audit" ("deletedAt");`.execute(db);
await sql`CREATE TABLE "group_user" (
"groupId" uuid NOT NULL,
"userId" uuid NOT NULL,
"createId" uuid NOT NULL DEFAULT immich_uuid_v7(),
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "group_user_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "album" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "group_user_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "group_user_pkey" PRIMARY KEY ("groupId", "userId")
);`.execute(db);
await sql`CREATE INDEX "group_user_groupId_idx" ON "group_user" ("groupId");`.execute(db);
await sql`CREATE INDEX "group_user_userId_idx" ON "group_user" ("userId");`.execute(db);
await sql`CREATE INDEX "group_user_createId_idx" ON "group_user" ("createId");`.execute(db);
await sql`CREATE INDEX "group_user_updateId_idx" ON "group_user" ("updateId");`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "group_user_delete_audit"
AFTER DELETE ON "group_user"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() <= 1)
EXECUTE FUNCTION group_user_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "group_user_updatedAt"
BEFORE UPDATE ON "group_user"
FOR EACH ROW
EXECUTE FUNCTION updated_at();`.execute(db);
await sql`CREATE TABLE "group" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" character varying NOT NULL,
"description" character varying,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
CONSTRAINT "group_name_uq" UNIQUE ("name"),
CONSTRAINT "group_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "group_updatedAt_id_idx" ON "group" ("updatedAt", "id");`.execute(db);
await sql`CREATE INDEX "group_updateId_idx" ON "group" ("updateId");`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "group_delete_audit"
AFTER DELETE ON "group"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE FUNCTION group_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "group_updatedAt"
BEFORE UPDATE ON "group"
FOR EACH ROW
EXECUTE FUNCTION updated_at();`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_group_delete_audit', '{"type":"function","name":"group_delete_audit","sql":"CREATE OR REPLACE FUNCTION group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO group_audit (\\"groupId\\")\\n SELECT \\"id\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_group_user_delete_audit', '{"type":"function","name":"group_user_delete_audit","sql":"CREATE OR REPLACE FUNCTION group_user_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO group_audit (\\"groupId\\", \\"userId\\")\\n SELECT \\"groupId\\", \\"userId\\"\\n FROM OLD;\\n\\n IF pg_trigger_depth() = 1 THEN\\n INSERT INTO group_user_audit (\\"groupId\\", \\"userId\\")\\n SELECT \\"groupId\\", \\"userId\\"\\n FROM OLD;\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_user_delete_audit', '{"type":"trigger","name":"group_user_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"group_user_delete_audit\\"\\n AFTER DELETE ON \\"group_user\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION group_user_delete_audit();"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_user_updatedAt', '{"type":"trigger","name":"group_user_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"group_user_updatedAt\\"\\n BEFORE UPDATE ON \\"group_user\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_delete_audit', '{"type":"trigger","name":"group_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"group_delete_audit\\"\\n AFTER DELETE ON \\"group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION group_delete_audit();"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_updatedAt', '{"type":"trigger","name":"group_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"group_updatedAt\\"\\n BEFORE UPDATE ON \\"group\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP TABLE "group_audit";`.execute(db);
await sql`DROP TABLE "group_user_audit";`.execute(db);
await sql`DROP TABLE "group_user";`.execute(db);
await sql`DROP TABLE "group";`.execute(db);
await sql`DROP FUNCTION group_delete_audit;`.execute(db);
await sql`DROP FUNCTION group_user_delete_audit;`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_group_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_group_user_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_user_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_user_updatedAt';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_updatedAt';`.execute(db);
}

View File

@ -0,0 +1,11 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "group_user" DROP CONSTRAINT "group_user_groupId_fkey";`.execute(db);
await sql`ALTER TABLE "group_user" ADD CONSTRAINT "group_user_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "group_user" DROP CONSTRAINT "group_user_groupId_fkey";`.execute(db);
await sql`ALTER TABLE "group_user" ADD CONSTRAINT "group_user_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "album" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
}

View File

@ -0,0 +1,33 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`DROP TRIGGER "group_delete_audit" ON "group";`.execute(db);
await sql`ALTER TABLE "group_audit" ADD "userId" uuid NOT NULL;`.execute(db);
await sql`DROP FUNCTION group_delete_audit;`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_group_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_delete_audit';`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`CREATE OR REPLACE FUNCTION public.group_delete_audit()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
INSERT INTO group_audit ("groupId")
SELECT "id"
FROM OLD;
RETURN NULL;
END
$function$
`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "group_delete_audit"
AFTER DELETE ON "group"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN ((pg_trigger_depth() = 0))
EXECUTE FUNCTION group_delete_audit();`.execute(db);
await sql`ALTER TABLE "group_audit" DROP COLUMN "userId";`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_group_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO group_audit (\\"groupId\\")\\n SELECT \\"id\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"group_delete_audit","type":"function"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"group_delete_audit\\"\\n AFTER DELETE ON \\"group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION group_delete_audit();","name":"group_delete_audit","type":"trigger"}'::jsonb);`.execute(db);
}

View File

@ -0,0 +1,70 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE OR REPLACE FUNCTION album_group_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO "album_audit" ("albumId", "userId")
SELECT OLD."albumId", "group_user"."userId"
FROM OLD INNER JOIN "group_user" ON "group_user"."groupId" = OLD."groupId";
IF pg_trigger_depth() = 1 THEN
INSERT INTO album_group_audit ("albumId", "groupId")
SELECT "albumId", "groupId"
FROM OLD;
END IF;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE TABLE "album_group_audit" (
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
"albumId" uuid NOT NULL,
"groupId" uuid NOT NULL,
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT "album_group_audit_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "album_group_audit_albumId_idx" ON "album_group_audit" ("albumId");`.execute(db);
await sql`CREATE INDEX "album_group_audit_groupId_idx" ON "album_group_audit" ("groupId");`.execute(db);
await sql`CREATE INDEX "album_group_audit_deletedAt_idx" ON "album_group_audit" ("deletedAt");`.execute(db);
await sql`CREATE TABLE "album_group" (
"albumId" uuid NOT NULL,
"groupId" uuid NOT NULL,
"role" character varying NOT NULL DEFAULT 'editor',
"createId" uuid NOT NULL DEFAULT immich_uuid_v7(),
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "album_group_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "album" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "album_group_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "album_group_pkey" PRIMARY KEY ("albumId", "groupId")
);`.execute(db);
await sql`CREATE INDEX "album_group_albumId_idx" ON "album_group" ("albumId");`.execute(db);
await sql`CREATE INDEX "album_group_groupId_idx" ON "album_group" ("groupId");`.execute(db);
await sql`CREATE INDEX "album_group_createId_idx" ON "album_group" ("createId");`.execute(db);
await sql`CREATE INDEX "album_group_updateId_idx" ON "album_group" ("updateId");`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "album_group_delete_audit"
AFTER DELETE ON "album_group"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() <= 1)
EXECUTE FUNCTION album_group_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "album_group_updatedAt"
BEFORE UPDATE ON "album_group"
FOR EACH ROW
EXECUTE FUNCTION updated_at();`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_group_delete_audit', '{"type":"function","name":"album_group_delete_audit","sql":"CREATE OR REPLACE FUNCTION album_group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO \\"album_audit\\" (\\"albumId\\", \\"userId\\")\\n SELECT OLD.\\"albumId\\", \\"group_user\\".\\"userId\\"\\n FROM OLD INNER JOIN \\"group_user\\" ON \\"group_user\\".\\"groupId\\" = OLD.\\"groupId\\";\\n\\n IF pg_trigger_depth() = 1 THEN\\n INSERT INTO album_group_audit (\\"albumId\\", \\"groupId\\")\\n SELECT \\"albumId\\", \\"groupId\\"\\n FROM OLD;\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_group_delete_audit', '{"type":"trigger","name":"album_group_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"album_group_delete_audit\\"\\n AFTER DELETE ON \\"album_group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION album_group_delete_audit();"}'::jsonb);`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_group_updatedAt', '{"type":"trigger","name":"album_group_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"album_group_updatedAt\\"\\n BEFORE UPDATE ON \\"album_group\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP TABLE "album_group_audit";`.execute(db);
await sql`DROP TABLE "album_group";`.execute(db);
await sql`DROP FUNCTION album_group_delete_audit;`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_group_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_group_delete_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_group_updatedAt';`.execute(db);
}

View File

@ -0,0 +1,17 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('album_group_audit')
export class AlbumGroupAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: Generated<string>;
@Column({ type: 'uuid', index: true })
albumId!: string;
@Column({ type: 'uuid', index: true })
groupId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
deletedAt!: Generated<Timestamp>;
}

View File

@ -0,0 +1,56 @@
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { album_group_delete_audit } from 'src/schema/functions';
import { AlbumTable } from 'src/schema/tables/album.table';
import { GroupTable } from 'src/schema/tables/group.table';
import {
AfterDeleteTrigger,
Column,
CreateDateColumn,
ForeignKeyColumn,
Generated,
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
@Table({ name: 'album_group' })
@UpdatedAtTrigger('album_group_updatedAt')
@AfterDeleteTrigger({
scope: 'statement',
function: album_group_delete_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() <= 1',
})
export class AlbumGroupTable {
@ForeignKeyColumn(() => AlbumTable, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
nullable: false,
primary: true,
})
albumId!: string;
@ForeignKeyColumn(() => GroupTable, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
nullable: false,
primary: true,
})
groupId!: string;
@Column({ type: 'character varying', default: AlbumUserRole.Editor })
role!: Generated<AlbumUserRole>;
@CreateIdColumn({ index: true })
createId!: Generated<string>;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@UpdateIdColumn({ index: true })
updateId!: Generated<string>;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
}

View File

@ -0,0 +1,17 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('group_audit')
export class GroupAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: Generated<string>;
@Column({ type: 'uuid' })
groupId!: string;
@Column({ type: 'uuid' })
userId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
deletedAt!: Generated<Timestamp>;
}

View File

@ -0,0 +1,17 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('group_user_audit')
export class GroupUserAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: Generated<string>;
@Column({ type: 'uuid', index: true })
groupId!: string;
@Column({ type: 'uuid', index: true })
userId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
deletedAt!: Generated<Timestamp>;
}

View File

@ -0,0 +1,51 @@
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { group_user_delete_audit } from 'src/schema/functions';
import { GroupTable } from 'src/schema/tables/group.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
CreateDateColumn,
ForeignKeyColumn,
Generated,
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
@Table({ name: 'group_user' })
@UpdatedAtTrigger('group_user_updatedAt')
@AfterDeleteTrigger({
scope: 'statement',
function: group_user_delete_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() <= 1',
})
export class GroupUserTable {
@ForeignKeyColumn(() => GroupTable, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
nullable: false,
primary: true,
})
groupId!: string;
@ForeignKeyColumn(() => UserTable, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
nullable: false,
primary: true,
})
userId!: string;
@CreateIdColumn({ index: true })
createId!: Generated<string>;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@UpdateIdColumn({ index: true })
updateId!: Generated<string>;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
}

View File

@ -0,0 +1,34 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import {
Column,
CreateDateColumn,
Generated,
Index,
PrimaryGeneratedColumn,
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
@Table('group')
@UpdatedAtTrigger('group_updatedAt')
@Index({ columns: ['updatedAt', 'id'] })
export class GroupTable {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@Column({ unique: true })
name!: string;
@Column({ nullable: true })
description!: string | null;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
@UpdateIdColumn({ index: true })
updateId!: Generated<string>;
}

View File

@ -1,4 +1,11 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import {
AlbumGroupCreateAllDto,
AlbumGroupDeleteAllDto,
AlbumGroupResponseDto,
AlbumGroupUpdateDto,
mapAlbumGroup,
} from 'src/dtos/album-group.dto';
import {
AddUsersDto,
AlbumInfoDto,
@ -204,6 +211,38 @@ export class AlbumService extends BaseService {
return results;
}
async getGroups(auth: AuthDto, id: string): Promise<AlbumGroupResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
const albumGroups = await this.albumGroupRepository.getAll(id);
return albumGroups.map((albumGroup) => mapAlbumGroup(albumGroup));
}
async upsertGroups(auth: AuthDto, id: string, { groups }: AlbumGroupCreateAllDto): Promise<AlbumGroupResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
const albumGroups = await this.albumGroupRepository.createAll(id, groups);
return albumGroups.map((albumGroup) => mapAlbumGroup(albumGroup));
}
async removeGroups(auth: AuthDto, id: string, dto: AlbumGroupDeleteAllDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
await this.albumGroupRepository.deleteAll(id, dto.groupIds);
}
async updateGroup(
auth: AuthDto,
id: string,
groupId: string,
dto: AlbumGroupUpdateDto,
): Promise<AlbumGroupResponseDto> {
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
const exists = await this.albumGroupRepository.exists({ albumId: id, groupId });
if (!exists) {
throw new BadRequestException('Album group not found');
}
const albumGroup = await this.albumGroupRepository.update({ albumId: id, groupId }, { role: dto.role });
return mapAlbumGroup(albumGroup);
}
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });

View File

@ -7,6 +7,7 @@ import { StorageCore } from 'src/cores/storage.core';
import { UserAdmin } from 'src/database';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumGroupRepository } from 'src/repositories/album-group.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
@ -21,6 +22,8 @@ import { DownloadRepository } from 'src/repositories/download.repository';
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
import { EmailRepository } from 'src/repositories/email.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { GroupUserRepository } from 'src/repositories/group-user.repository';
import { GroupRepository } from 'src/repositories/group.repository';
import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
@ -111,6 +114,7 @@ export class BaseService {
protected accessRepository: AccessRepository,
protected activityRepository: ActivityRepository,
protected albumRepository: AlbumRepository,
protected albumGroupRepository: AlbumGroupRepository,
protected albumUserRepository: AlbumUserRepository,
protected apiKeyRepository: ApiKeyRepository,
protected assetRepository: AssetRepository,
@ -124,6 +128,8 @@ export class BaseService {
protected duplicateRepository: DuplicateRepository,
protected emailRepository: EmailRepository,
protected eventRepository: EventRepository,
protected groupRepository: GroupRepository,
protected groupUserRepository: GroupUserRepository,
protected jobRepository: JobRepository,
protected libraryRepository: LibraryRepository,
protected machineLearningRepository: MachineLearningRepository,

View File

@ -0,0 +1,95 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { PostgresError } from 'postgres';
import { AuthDto } from 'src/dtos/auth.dto';
import {
GroupUserCreateAllDto,
GroupUserDeleteAllDto,
GroupUserResponseDto,
mapGroupUser,
} from 'src/dtos/group-user.dto';
import {
GroupAdminCreateDto,
GroupAdminResponseDto,
GroupAdminSearchDto,
GroupAdminUpdateDto,
mapGroupAdmin,
} from 'src/dtos/group.dto';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class GroupAdminService extends BaseService {
async search(auth: AuthDto, dto: GroupAdminSearchDto): Promise<GroupAdminResponseDto[]> {
const groups = await this.groupRepository.search(dto);
return groups.map((group) => mapGroupAdmin(group));
}
async create(auth: AuthDto, dto: GroupAdminCreateDto): Promise<GroupAdminResponseDto> {
try {
const { users, ...groupDto } = dto;
const group = await this.groupRepository.create(groupDto, users);
return mapGroupAdmin(group);
} catch (error) {
this.handleError(error);
}
}
async get(auth: AuthDto, id: string): Promise<GroupAdminResponseDto> {
const group = await this.findOrFail(id);
return mapGroupAdmin(group);
}
async update(auth: AuthDto, id: string, dto: GroupAdminUpdateDto): Promise<GroupAdminResponseDto> {
await this.findOrFail(id);
const updated = await this.groupRepository.update(id, { ...dto, updatedAt: new Date() });
return mapGroupAdmin(updated);
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.findOrFail(id);
await this.groupRepository.delete(id);
}
async getUsers(auth: AuthDto, id: string): Promise<GroupUserResponseDto[]> {
await this.findOrFail(id);
const users = await this.groupUserRepository.getAll(id);
return users.map((user) => mapGroupUser(user));
}
async addUsers(auth: AuthDto, id: string, { users }: GroupUserCreateAllDto): Promise<GroupUserResponseDto[]> {
await this.findOrFail(id);
const userIds = users.map(({ userId }) => userId);
const groupUsers = await this.groupUserRepository.createAll(id, userIds);
return groupUsers.map((groupUser) => mapGroupUser(groupUser));
}
async removeUsers(auth: AuthDto, id: string, dto: GroupUserDeleteAllDto): Promise<void> {
await this.findOrFail(id);
await this.groupUserRepository.deleteAll(id, dto.userIds);
}
async removeUser(auth: AuthDto, id: string, userId: string): Promise<void> {
await this.findOrFail(id);
const exists = await this.groupUserRepository.get({ groupId: id, userId });
if (!exists) {
throw new BadRequestException('Group does not include this user');
}
await this.groupUserRepository.delete({ groupId: id, userId });
}
private handleError(error: unknown): never {
if ((error as PostgresError).constraint_name === 'group_name_uq') {
throw new BadRequestException('Group with this name already exists');
}
throw error;
}
private async findOrFail(id: string) {
const group = await this.groupRepository.get(id);
if (!group) {
throw new BadRequestException('Group not found');
}
return group;
}
}

View File

@ -0,0 +1,38 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto';
import { GroupUserResponseDto, mapGroupUser } from 'src/dtos/group-user.dto';
import { GroupResponseDto, mapGroup } from 'src/dtos/group.dto';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class GroupService extends BaseService {
async search(auth: AuthDto): Promise<GroupResponseDto[]> {
const groups = await this.groupRepository.search({ userId: auth.user.id });
return groups.map((group) => mapGroup(group));
}
async get(auth: AuthDto, id: string): Promise<GroupResponseDto> {
const group = await this.findOrFail({ userId: auth.user.id, groupId: id });
return mapGroup(group);
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.findOrFail({ userId: auth.user.id, groupId: id });
await this.groupUserRepository.delete({ userId: auth.user.id, groupId: id });
}
async getUsers(auth: AuthDto, id: string): Promise<GroupUserResponseDto[]> {
await this.findOrFail({ userId: auth.user.id, groupId: id });
const users = await this.groupUserRepository.getAll(id);
return users.map((user) => mapGroupUser(user));
}
async findOrFail({ userId, groupId }: { userId: string; groupId: string }): Promise<GroupResponseDto> {
const [group] = await this.groupUserRepository.get({ userId, groupId });
if (!group) {
throw new BadRequestException('Group not found');
}
return group;
}
}

View File

@ -11,6 +11,8 @@ import { CliService } from 'src/services/cli.service';
import { DatabaseService } from 'src/services/database.service';
import { DownloadService } from 'src/services/download.service';
import { DuplicateService } from 'src/services/duplicate.service';
import { GroupAdminService } from 'src/services/group-admin.service';
import { GroupService } from 'src/services/group.service';
import { JobService } from 'src/services/job.service';
import { LibraryService } from 'src/services/library.service';
import { MapService } from 'src/services/map.service';
@ -54,6 +56,8 @@ export const services = [
DatabaseService,
DownloadService,
DuplicateService,
GroupAdminService,
GroupService,
JobService,
LibraryService,
MapService,

View File

@ -80,12 +80,20 @@ export const ValidateUUID = (options?: UUIDOptions & ApiPropertyOptions) => {
};
export class UUIDParamDto {
@IsNotEmpty()
@IsUUID('4')
@ApiProperty({ format: 'uuid' })
@ValidateUUID()
id!: string;
}
export class UserIdAndIdParamDto extends UUIDParamDto {
@ValidateUUID()
userId!: string;
}
export class GroupIdAndIdParamDto extends UUIDParamDto {
@ValidateUUID()
groupId!: string;
}
export class UUIDAssetIDParamDto {
@ValidateUUID()
id!: string;

View File

@ -26,6 +26,7 @@ import { DownloadRepository } from 'src/repositories/download.repository';
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
import { EmailRepository } from 'src/repositories/email.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { GroupRepository } from 'src/repositories/group.repository';
import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
@ -196,6 +197,7 @@ export type ServiceOverrides = {
duplicateRepository: DuplicateRepository;
email: EmailRepository;
event: EventRepository;
group: GroupRepository;
job: JobRepository;
library: LibraryRepository;
logger: LoggingRepository;
@ -266,6 +268,7 @@ export const newTestService = <T extends BaseService>(
email: automock(EmailRepository, { args: [loggerMock] }),
// eslint-disable-next-line no-sparse-arrays
event: automock(EventRepository, { args: [, , loggerMock], strict: false }),
group: automock(GroupRepository),
job: newJobRepositoryMock(),
apiKey: automock(ApiKeyRepository),
library: automock(LibraryRepository, { strict: false }),
@ -318,6 +321,7 @@ export const newTestService = <T extends BaseService>(
overrides.duplicateRepository || (mocks.duplicateRepository as As<DuplicateRepository>),
overrides.email || (mocks.email as As<EmailRepository>),
overrides.event || (mocks.event as As<EventRepository>),
overrides.group || (mocks.group as As<GroupRepository>),
overrides.job || (mocks.job as As<JobRepository>),
overrides.library || (mocks.library as As<LibraryRepository>),
overrides.machineLearning || (mocks.machineLearning as As<MachineLearningRepository>),

View File

@ -34,9 +34,8 @@
} from '$lib/utils/album-utils';
import { downloadAlbum } from '$lib/utils/asset-utils';
import type { ContextMenuPosition } from '$lib/utils/context-menu';
import { handleError } from '$lib/utils/handle-error';
import { normalizeSearchString } from '$lib/utils/string-utils';
import { addUsersToAlbum, deleteAlbum, isHttpError, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk';
import { deleteAlbum, getAlbumInfo, isHttpError, type AlbumResponseDto } from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { mdiDeleteOutline, mdiFolderDownloadOutline, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
import { groupBy } from 'lodash-es';
@ -324,25 +323,6 @@
updateRecentAlbumInfo(album);
};
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
if (!albumToShare) {
return;
}
try {
const album = await addUsersToAlbum({
id: albumToShare.id,
addUsersDto: {
albumUsers,
},
});
updateAlbumInfo(album);
} catch (error) {
handleError(error, $t('errors.unable_to_add_album_users'));
} finally {
albumToShare = null;
}
};
const handleSharedLinkCreated = (album: AlbumResponseDto) => {
album.shared = true;
album.hasSharedLink = true;
@ -356,11 +336,13 @@
albumToShare = contextMenuTargetAlbum;
closeAlbumContextMenu();
const result = await modalManager.show(AlbumShareModal, { album: albumToShare });
const action = await modalManager.show(AlbumShareModal, { album: albumToShare });
switch (result?.action) {
case 'sharedUsers': {
await handleAddUsers(result.data);
switch (action) {
case 'update': {
const album = await getAlbumInfo({ id: albumToShare.id, withoutAssets: true });
updateAlbumInfo(album);
albumToShare = null;
return;
}

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { Icon, type Size } from '@immich/ui';
import { mdiAccountMultipleOutline } from '@mdi/js';
type Group = {
name: string;
description: string | null;
};
interface Props {
group: Group;
size?: Size;
}
let { group, size = 'medium' }: Props = $props();
const getDescription = (group: Group) => {
return group.name + (group.description ? ` - ${group.description}` : '');
};
const title = $derived(getDescription(group));
const sizes: Record<Size, string> = {
tiny: 'h-5 w-5 text-xs',
small: 'h-7 w-7 text-sm',
medium: 'h-10 w-10 text-base',
large: 'h-12 w-12 text-lg',
giant: 'h-16 w-16 text-xl',
};
</script>
<!-- <Avatar name={group.name} {color} {size} /> -->
<span class="{sizes[size]} shrink-0 bg-subtle text-primary rounded-full p-2 border border-light" {title}>
<Icon size="100%" icon={mdiAccountMultipleOutline} />
</span>

View File

@ -18,6 +18,7 @@ export enum AssetAction {
export enum AppRoute {
ADMIN_USERS = '/admin/users',
ADMIN_GROUPS = '/admin/groups',
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
ADMIN_SETTINGS = '/admin/system-settings',
ADMIN_STATS = '/admin/server-status',

View File

@ -2,19 +2,24 @@
import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte';
import Dropdown from '$lib/components/elements/dropdown.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import GroupAvatar from '$lib/components/shared-components/GroupAvatar.svelte';
import { AppRoute } from '$lib/constants';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
import {
addGroupsToAlbum,
addUsersToAlbum,
AlbumUserRole,
getAllSharedLinks,
getGroupsForAlbum,
searchMyGroups,
searchUsers,
type AlbumResponseDto,
type AlbumUserAddDto,
type GroupResponseDto,
type SharedLinkResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Button, Link, Modal, ModalBody, Stack, Text } from '@immich/ui';
import { Button, Heading, Link, Modal, ModalBody, Stack, Text } from '@immich/ui';
import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -22,13 +27,25 @@
interface Props {
album: AlbumResponseDto;
onClose: (result?: { action: 'sharedLink' } | { action: 'sharedUsers'; data: AlbumUserAddDto[] }) => void;
onClose: (action?: 'sharedLink' | 'update') => void;
}
let { album, onClose }: Props = $props();
let users: UserResponseDto[] = $state([]);
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
let groups: GroupResponseDto[] = $state([]);
let selectedUsers: Record<string, { item: UserResponseDto; role: AlbumUserRole }> = $state({});
let selectedGroups: Record<string, { item: GroupResponseDto; role: AlbumUserRole }> = $state({});
type SelectedItem =
| { type: 'user'; item: UserResponseDto; role: AlbumUserRole }
| { type: 'group'; item: GroupResponseDto; role: AlbumUserRole };
const selectedItems: SelectedItem[] = $derived([
...Object.values(selectedGroups).map((item) => ({ ...item, type: 'group' }) as const),
...Object.values(selectedUsers).map((item) => ({ ...item, type: 'user' }) as const),
]);
let sharedLinkUrl = $state('');
const handleViewQrCode = (sharedLink: SharedLinkResponseDto) => {
@ -38,38 +55,81 @@
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
{ title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
{ title: $t('role_viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
{ title: $t('remove_user'), value: 'none' },
{ title: $t('remove'), value: 'none' },
];
let sharedLinks: SharedLinkResponseDto[] = $state([]);
onMount(async () => {
sharedLinks = await getAllSharedLinks({ albumId: album.id });
const data = await searchUsers();
const [allUsers, allMyGroups, albumGroups] = await Promise.all([
searchUsers(),
searchMyGroups(),
getGroupsForAlbum({ id: album.id }),
]);
// remove album owner
users = data.filter((user) => user.id !== album.ownerId);
users = allUsers
.filter((user) => user.id !== album.ownerId)
.filter((user) => !album.albumUsers.some(({ user: sharedUser }) => user.id === sharedUser.id));
// Remove the existed shared users from the album
for (const sharedUser of album.albumUsers) {
users = users.filter((user) => user.id !== sharedUser.user.id);
}
groups = allMyGroups.filter((myGroup) => !albumGroups.some(({ id }) => myGroup.id === id));
});
const handleToggle = (user: UserResponseDto) => {
const handleToggleUser = (user: UserResponseDto) => {
if (Object.keys(selectedUsers).includes(user.id)) {
delete selectedUsers[user.id];
} else {
selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
selectedUsers[user.id] = { item: user, role: AlbumUserRole.Editor };
}
};
const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
if (role === 'none') {
delete selectedUsers[user.id];
const handleToggleGroups = (group: GroupResponseDto) => {
if (Object.keys(selectedGroups).includes(group.id)) {
delete selectedGroups[group.id];
} else {
selectedUsers[user.id].role = role;
selectedGroups[group.id] = { item: group, role: AlbumUserRole.Editor };
}
};
const handleChangeRole = (selectedItem: SelectedItem, role: AlbumUserRole | 'none') => {
const { item, type } = selectedItem;
if (role === 'none') {
if (type === 'user') {
delete selectedUsers[item.id];
} else {
delete selectedGroups[item.id];
}
} else {
selectedItem.role = role;
}
};
const handleAdd = async () => {
const albumUsers = Object.values(selectedUsers).map(({ item: user, ...rest }) => ({ userId: user.id, ...rest }));
if (albumUsers.length > 0) {
await addUsersToAlbum({
id: album.id,
addUsersDto: {
albumUsers,
},
});
}
const groups = Object.values(selectedGroups).map(({ item: group, ...rest }) => ({ groupId: group.id, ...rest }));
if (groups.length > 0) {
await addGroupsToAlbum({
id: album.id,
albumGroupCreateAllDto: {
groups,
},
});
}
onClose('update');
selectedUsers = {};
selectedGroups = {};
sharedLinks = await getAllSharedLinks({ albumId: album.id });
};
</script>
{#if sharedLinkUrl}
@ -77,37 +137,45 @@
{:else}
<Modal size="small" title={$t('share')} {onClose}>
<ModalBody>
{#if Object.keys(selectedUsers).length > 0}
{#if selectedItems.length > 0}
<div class="mb-2 py-2 sticky">
<p class="text-xs font-medium">{$t('selected')}</p>
<div class="my-2">
{#each Object.values(selectedUsers) as { user } (user.id)}
{#key user.id}
<div class="flex place-items-center gap-4 p-4">
<Heading size="tiny">{$t('selected')}</Heading>
<div class="flex my-2 flex-col gap-2">
{#each selectedItems as selectedItem (selectedItem.item.id)}
<div class="flex place-items-center gap-2 px-2">
{#if selectedItem.type === 'group'}
<GroupAvatar group={selectedItem.item} />
<div class="text-start grow p-2">
<p class="text-immich-fg dark:text-immich-dark-fg">
{selectedItem.item.name}
</p>
<p class="text-xs">
{selectedItem.item.description}
</p>
</div>
{:else}
<div
class="flex h-10 w-10 items-center justify-center rounded-full border bg-green-600 text-3xl text-white"
>
<Icon path={mdiCheck} size={24} />
</div>
<!-- <UserAvatar {user} size="md" /> -->
<div class="text-start grow">
<div class="text-start grow p-2">
<p class="text-immich-fg dark:text-immich-dark-fg">
{user.name}
{selectedItem.item.name}
</p>
<p class="text-xs">
{user.email}
{selectedItem.item.email}
</p>
</div>
{/if}
<Dropdown
title={$t('role')}
options={roleOptions}
render={({ title, icon }) => ({ title, icon })}
onSelect={({ value }) => handleChangeRole(user, value)}
onSelect={({ value }) => handleChangeRole(selectedItem, value)}
/>
</div>
{/key}
{/each}
</div>
</div>
@ -121,16 +189,18 @@
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
<Text>{$t('users')}</Text>
<Heading size="tiny">{$t('users')}</Heading>
<div class="my-2">
{#each users as user (user.id)}
{#if !Object.keys(selectedUsers).includes(user.id)}
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
<div
class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl gap-2"
>
<button
type="button"
onclick={() => handleToggle(user)}
class="flex w-full place-items-center gap-4 p-4"
onclick={() => handleToggleUser(user)}
class="flex w-full place-items-center gap-4 p-2"
>
<UserAvatar {user} size="md" />
<div class="text-start grow">
@ -147,21 +217,41 @@
{/each}
</div>
{/if}
{#if groups.length > 0 && groups.length !== Object.keys(selectedGroups).length}
<Heading size="tiny">{$t('groups')}</Heading>
<div class="my-2">
{#each groups as group (group.id)}
{#if !Object.keys(selectedGroups).includes(group.id)}
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
<button
type="button"
onclick={() => handleToggleGroups(group)}
class="flex w-full place-items-center gap-4 p-2"
>
<GroupAvatar {group} />
<div class="text-start grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
{group.name}
</p>
<p class="text-xs">
{group.description}
</p>
</div>
</button>
</div>
{/if}
{/each}
</div>
{/if}
</div>
{#if users.length > 0}
{#if users.length > 0 || groups.length > 0}
<div class="py-3">
<Button
size="small"
fullWidth
shape="round"
disabled={Object.keys(selectedUsers).length === 0}
onclick={() =>
onClose({
action: 'sharedUsers',
data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
})}>{$t('add')}</Button
>
<Button size="small" fullWidth shape="round" disabled={selectedItems.length === 0} onclick={handleAdd}>
{$t('add')}
</Button>
</div>
{/if}
@ -181,12 +271,8 @@
</Stack>
{/if}
<Button
leadingIcon={mdiLink}
size="small"
shape="round"
fullWidth
onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button
<Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={() => onClose('sharedLink')}
>{$t('create_link')}</Button
>
</Stack>
</ModalBody>

View File

@ -0,0 +1,69 @@
<script lang="ts">
import { handleError } from '$lib/utils/handle-error';
import { createGroupAdmin, type GroupAdminResponseDto } from '@immich/sdk';
import { Alert, Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Stack } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
onClose: (group?: GroupAdminResponseDto) => void;
}
let { onClose }: Props = $props();
let error = $state('');
let name = $state('');
let description = $state('');
let isPending = $state(false);
let valid = $derived(name.length > 0);
const onSubmit = async (event: Event) => {
event.preventDefault();
if (!valid) {
return;
}
isPending = true;
error = '';
try {
const group = await createGroupAdmin({ groupAdminCreateDto: { name, description } });
onClose(group);
return;
} catch (error) {
handleError(error, $t('errors.unable_to_create_group'));
} finally {
isPending = false;
}
};
</script>
<Modal title={$t('create_new_group')} {onClose} size="small">
<ModalBody>
<form onsubmit={onSubmit} autocomplete="off" id="create-new-group-form">
{#if error}
<Alert color="danger" size="small" title={error} closable />
{/if}
<Stack gap={4}>
<Field label={$t('name')} required>
<Input bind:value={name} />
</Field>
<Field label={$t('description')}>
<Input bind:value={description} />
</Field>
</Stack>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button color="secondary" fullWidth onclick={() => onClose()} shape="round">{$t('cancel')}</Button>
<Button type="submit" disabled={!valid} fullWidth shape="round" form="create-new-group-form"
>{$t('create')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@ -0,0 +1,62 @@
<script lang="ts">
import { handleError } from '$lib/utils/handle-error';
import { updateGroupAdmin, type GroupAdminResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Stack } from '@immich/ui';
import { mdiAccountEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
group: GroupAdminResponseDto;
onClose: (group?: GroupAdminResponseDto) => void;
}
let { group, onClose }: Props = $props();
let name = $derived(group.name);
let description = $derived(group.description ?? '');
const handleEditGroup = async () => {
try {
const newGroup = await updateGroupAdmin({
id: group.id,
groupAdminUpdateDto: {
name,
description: description ?? null,
},
});
onClose(newGroup);
} catch (error) {
handleError(error, $t('errors.unable_to_update_group'));
}
};
const onSubmit = async (event: Event) => {
event.preventDefault();
await handleEditGroup();
};
</script>
<Modal title={$t('edit_group')} size="small" icon={mdiAccountEditOutline} {onClose}>
<ModalBody>
<form onsubmit={onSubmit} autocomplete="off" id="edit-group-form">
<Stack gap={4}>
<Field label={$t('name')} required>
<Input bind:value={name} />
</Field>
<Field label={$t('description')} required>
<Input bind:value={description} />
</Field>
</Stack>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth form="edit-group-form" onclick={() => onClose()}
>{$t('cancel')}</Button
>
<Button type="submit" shape="round" fullWidth form="edit-group-form">{$t('confirm')}</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@ -0,0 +1,175 @@
<script lang="ts">
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
addUsersToGroupAdmin,
removeUsersFromGroupAdmin,
searchUsersAdmin,
type GroupAdminResponseDto,
type GroupUserAdminResponseDto,
type UserAdminResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Button, Heading, HStack, Icon, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiCheck, mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
group: GroupAdminResponseDto;
users: GroupUserAdminResponseDto[];
onClose: (changed?: boolean) => void;
}
let { group, users, onClose }: Props = $props();
let allUsers: UserAdminResponseDto[] = $state([]);
let selectedUsers: Record<string, UserResponseDto> = $state(Object.fromEntries(users.map((user) => [user.id, user])));
handlePromiseError(
searchUsersAdmin({}).then((result) => {
console.log(result);
allUsers = result;
}),
);
const handleToggle = (user: UserResponseDto) => {
if (Object.keys(selectedUsers).includes(user.id)) {
delete selectedUsers[user.id];
} else {
selectedUsers[user.id] = user;
}
};
const handleConfirm = async () => {
try {
const existingUsers = Object.fromEntries(users.map((user) => [user.id, user]));
const addUsers: UserAdminResponseDto[] = [];
const removeUsers: UserAdminResponseDto[] = [];
for (const user of allUsers) {
const currentlyAdded = !!selectedUsers[user.id];
const previouslyAdded = !!existingUsers[user.id];
if (!previouslyAdded && currentlyAdded) {
addUsers.push(user);
continue;
}
if (previouslyAdded && !currentlyAdded) {
removeUsers.push(user);
continue;
}
}
if (addUsers.length > 0) {
await addUsersToGroupAdmin({
id: group.id,
groupUserCreateAllDto: {
users: addUsers.map((user) => ({ userId: user.id })),
},
});
}
if (removeUsers.length > 0) {
await removeUsersFromGroupAdmin({
id: group.id,
groupUserDeleteAllDto: { userIds: removeUsers.map(({ id }) => id) },
});
}
onClose(addUsers.length > 0 || removeUsers.length > 0);
} catch (error) {
handleError(error, $t('errors.unable_to_update_user'));
}
};
</script>
<Modal
title={users.length === 0 ? $t('add_users') : $t('edit_users')}
size="small"
icon={mdiAccountMultipleOutline}
{onClose}
>
<ModalBody>
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
{#if Object.values(selectedUsers).length === 0}
<div class="my-4">
<Text size="large">{$t('empty_group_message')}</Text>
</div>
{/if}
{#if Object.keys(selectedUsers).length > 0}
<div class="mb-2 py-2 sticky">
<Heading size="tiny">{$t('group_users')}</Heading>
<div class="my-2">
{#each Object.values(selectedUsers) as user (user.id)}
<div class="flex place-items-center gap-4 p-4">
<div
class="flex h-10 w-10 items-center justify-center rounded-full border bg-green-600 text-3xl text-white"
>
<Icon icon={mdiCheck} />
</div>
<!-- <UserAvatar {user} size="md" /> -->
<div class="text-start grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
{user.name}
</p>
<p class="text-xs">
{user.email}
</p>
</div>
<Button leadingIcon={mdiClose} color="secondary" size="small" onclick={() => handleToggle(user)}>
{$t('remove')}
</Button>
<!--
<Dropdown
title={$t('role')}
options={roleOptions}
render={({ title, icon }) => ({ title, icon })}
onSelect={({ value }) => handleChangeRole(user, value)}
/> -->
</div>
{/each}
</div>
</div>
{/if}
{#if allUsers.length > 0 && allUsers.length !== Object.keys(selectedUsers).length}
<Heading size="tiny">{$t('other_users')}</Heading>
<div class="my-2">
{#each allUsers as user (user.id)}
{#if !Object.keys(selectedUsers).includes(user.id)}
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
<button
type="button"
onclick={() => handleToggle(user)}
class="flex w-full place-items-center gap-4 p-4"
>
<UserAvatar {user} size="md" />
<div class="text-start grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
{user.name}
</p>
<p class="text-xs">
{user.email}
</p>
</div>
</button>
</div>
{/if}
{/each}
</div>
{/if}
</div>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" fullWidth onclick={handleConfirm}>{$t('confirm')}</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@ -2,13 +2,14 @@
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { AppRoute } from '$lib/constants';
import { NavbarItem } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js';
import { mdiAccountGroupOutline, mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js';
import { t } from 'svelte-i18n';
</script>
<div class="h-full flex flex-col justify-between gap-2">
<div class="flex flex-col pt-8 pe-4 gap-1">
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
<NavbarItem title={$t('groups')} href={AppRoute.ADMIN_GROUPS} icon={mdiAccountGroupOutline} />
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />

View File

@ -27,6 +27,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GroupAvatar from '$lib/components/shared-components/GroupAvatar.svelte';
import {
NotificationType,
notificationController,
@ -63,11 +64,9 @@
AssetOrder,
AssetVisibility,
addAssetsToAlbum,
addUsersToAlbum,
deleteAlbum,
getAlbumInfo,
updateAlbumInfo,
type AlbumUserAddDto,
} from '@immich/sdk';
import { Button, IconButton, modalManager } from '@immich/ui';
import {
@ -104,6 +103,7 @@
let isCreatingSharedAlbum = $state(false);
let isShowActivity = $state(false);
let albumOrder: AssetOrder | undefined = $state(data.album.order);
let groups = $derived(data.groups);
const assetInteraction = new AssetInteraction();
const timelineInteraction = new AssetInteraction();
@ -224,22 +224,6 @@
await setModeToView();
};
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
try {
await addUsersToAlbum({
id: album.id,
addUsersDto: {
albumUsers,
},
});
await refreshAlbum();
viewMode = AlbumPageViewMode.VIEW;
} catch (error) {
handleError(error, $t('errors.error_adding_users_to_album'));
}
};
const handleDownloadAlbum = async () => {
await downloadAlbum(album);
};
@ -385,16 +369,17 @@
);
const handleShare = async () => {
const result = await modalManager.show(AlbumShareModal, { album });
const action = await modalManager.show(AlbumShareModal, { album });
switch (result?.action) {
case 'sharedLink': {
await handleShareLink();
switch (action) {
case 'update': {
await refreshAlbum();
viewMode = AlbumPageViewMode.VIEW;
return;
}
case 'sharedUsers': {
await handleAddUsers(result.data);
case 'sharedLink': {
await handleShareLink();
return;
}
}
@ -470,7 +455,7 @@
{/if}
<!-- ALBUM SHARING -->
{#if album.albumUsers.length > 0 || (album.hasSharedLink && isOwned)}
{#if album.albumUsers.length > 0 || (album.hasSharedLink && isOwned) || groups.length > 0}
<div class="my-3 flex gap-x-1">
<!-- link -->
{#if album.hasSharedLink && isOwned}
@ -508,6 +493,12 @@
/>
{/if}
{#each groups as group (group.id)}
<!-- <button type="button" onclick={handleEditGroups}> -->
<GroupAvatar {group} />
<!-- </button> -->
{/each}
{#if isOwned}
<IconButton
shape="round"

View File

@ -1,17 +1,19 @@
import { authenticate } from '$lib/utils/auth';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getAlbumInfo } from '@immich/sdk';
import { getAlbumInfo, getGroupsForAlbum } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const [album, asset] = await Promise.all([
const [album, groups, asset] = await Promise.all([
getAlbumInfo({ id: params.albumId, withoutAssets: true }),
getGroupsForAlbum({ id: params.albumId }),
getAssetInfoFromParam(params),
]);
return {
album,
groups,
asset,
meta: {
title: album.albumName,

View File

@ -0,0 +1,79 @@
<script lang="ts">
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import GroupAvatar from '$lib/components/shared-components/GroupAvatar.svelte';
import { AppRoute } from '$lib/constants';
import GroupCreateModal from '$lib/modals/GroupCreateModal.svelte';
import { searchGroupsAdmin, type GroupAdminResponseDto } from '@immich/sdk';
import { Button, HStack, IconButton, Text, modalManager } from '@immich/ui';
import { mdiEyeOutline, mdiPlusBoxOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
let groups: GroupAdminResponseDto[] = $derived(data.groups);
const refresh = async () => {
groups = await searchGroupsAdmin({});
};
const handleCreate = async () => {
await modalManager.show(GroupCreateModal);
await refresh();
};
</script>
<AdminPageLayout title={data.meta.title}>
{#snippet buttons()}
<HStack gap={1}>
<Button leadingIcon={mdiPlusBoxOutline} onclick={handleCreate} size="small" variant="ghost" color="secondary">
<Text class="hidden md:block">{$t('create_group')}</Text>
</Button>
</HStack>
{/snippet}
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 lg:w-[850px]">
<table class="my-5 w-full">
<thead
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
<tr class="flex w-full place-items-center">
<th class="p-4 text-start w-4/12 text-sm font-medium">{$t('name')}</th>
<th class="p-4 text-start w-6/12 text-sm font-medium">{$t('description')}</th>
<th class="p-4 text-start w-2/12 text-sm font-medium">{$t('action')}</th>
</tr>
</thead>
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#if groups}
{#each groups as group (group.id)}
<tr
class="flex h-[80px] overflow-hidden w-full place-items-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
>
<td class="p-4 w-4/12 text-ellipsis break-all text-sm">
<div class="flex items-center gap-2">
<GroupAvatar {group} />
{group.name}
</div>
</td>
<td class="p-4 w-6/12 text-ellipsis break-all text-sm">{group.description}</td>
<td class="p-4 w-2/12 flex flex-row flex-wrap gap-x-2 gap-y-1 text-ellipsis break-all text-sm">
<IconButton
shape="round"
size="medium"
icon={mdiEyeOutline}
href={`${AppRoute.ADMIN_GROUPS}/${group.id}`}
aria-label={$t('view_group')}
/>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</section>
</section>
</AdminPageLayout>

View File

@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { searchGroupsAdmin } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const groups = await searchGroupsAdmin({});
const $t = await getFormatter();
return {
groups,
meta: {
title: $t('admin.group_management'),
},
};
}) satisfies PageLoad;

View File

@ -0,0 +1,191 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import GroupAvatar from '$lib/components/shared-components/GroupAvatar.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute } from '$lib/constants';
import GroupEditModal from '$lib/modals/GroupEditModal.svelte';
import GroupEditUsersModal from '$lib/modals/GroupEditUsersModal.svelte';
import { deleteGroupAdmin, getUsersForGroupAdmin } from '@immich/sdk';
import {
Alert,
Button,
Card,
CardBody,
CardHeader,
CardTitle,
Code,
Container,
Heading,
HStack,
Icon,
IconButton,
modalManager,
Stack,
Text,
} from '@immich/ui';
import {
mdiAccountMultipleOutline,
mdiAccountOutline,
mdiEyeOutline,
mdiPencilOutline,
mdiTrashCanOutline,
} from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
let group = $derived(data.group);
let users = $derived(data.users);
const handleEdit = async () => {
const result = await modalManager.show(GroupEditModal, { group });
if (result) {
group = result;
}
};
const handleDelete = async () => {
const confirmed = await modalManager.showDialog({
prompt: $t('confirm_delete_name', { values: { name: group.name } }),
confirmColor: 'danger',
icon: mdiTrashCanOutline,
});
if (confirmed) {
await deleteGroupAdmin({ id: group.id });
await goto(AppRoute.ADMIN_GROUPS);
}
};
const handleEditUsers = async () => {
const changed = await modalManager.show(GroupEditUsersModal, { group, users });
if (changed) {
users = await getUsersForGroupAdmin({ id: group.id });
}
};
</script>
<AdminPageLayout title={data.meta.title}>
{#snippet buttons()}
<HStack gap={0}>
<Button
color="secondary"
size="small"
variant="ghost"
leadingIcon={mdiPencilOutline}
onclick={() => handleEdit()}
>
<Text class="hidden md:block">{$t('edit_group')}</Text>
</Button>
<Button
color="danger"
size="small"
variant="ghost"
leadingIcon={mdiTrashCanOutline}
onclick={() => handleDelete()}
>
<Text class="hidden md:block">{$t('delete_group')}</Text>
</Button>
</HStack>
{/snippet}
<div>
<Container size="large" center>
<div class="col-span-full flex gap-4 items-center my-4">
<GroupAvatar {group} size="giant" />
<div class="flex flex-col gap-1">
<Heading tag="h1" size="large">{group.name}</Heading>
{#if group.description}
<Text color="muted">{group.description}</Text>
{/if}
</div>
</div>
<Stack gap={4}>
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center gap-2">
<div class="flex items-center gap-2 px-4 text-primary my-4">
<Icon icon={mdiAccountMultipleOutline} size="2rem" />
<Heading>Users</Heading>
</div>
<div>
<Button leadingIcon={mdiPencilOutline} color="primary" size="small" onclick={() => handleEditUsers()}
>{users.length === 0 ? $t('add_users') : $t('edit_users')}</Button
>
</div>
</div>
{#if users.length === 0}
<Alert color="secondary" title={$t('empty_group_message')} icon={mdiAccountMultipleOutline} />
{:else}
<table class="w-full">
<thead
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
<tr class="flex w-full place-items-center">
<th class="p-4 text-start w-4/12 text-sm font-medium">{$t('name')}</th>
<th class="p-4 text-start w-6/12 text-sm font-medium">{$t('email')}</th>
<th class="p-4 text-start w-2/12 text-sm font-medium">{$t('action')}</th>
</tr>
</thead>
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each users as user (user.id)}
<tr
class="flex h-[80px] overflow-hidden w-full place-items-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
>
<td class="px-4 py-2 w-4/12 text-ellipsis break-all text-sm">
<div class="flex items-center gap-2">
<UserAvatar {user} size="sm" />
{user.name}
</div>
</td>
<td class="px-4 py-2 w-6/12 text-ellipsis break-all text-sm">{user.email}</td>
<td class="px-4 w-2/12 flex flex-row flex-wrap gap-x-2 gap-y-1 text-ellipsis break-all text-sm">
<IconButton
shape="round"
size="medium"
icon={mdiEyeOutline}
href={`${AppRoute.ADMIN_USERS}/${user.id}`}
aria-label={$t('view_user')}
/>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Card color="secondary">
<CardHeader>
<div class="flex items-center gap-2 px-4 py-2 text-primary">
<Icon icon={mdiAccountOutline} size="1.5rem" />
<CardTitle>{$t('details')}</CardTitle>
</div>
</CardHeader>
<CardBody>
<div class="px-4 pb-7">
<Stack gap={2}>
<div>
<Heading tag="h3" size="tiny">{$t('created_at')}</Heading>
<Text>{group.createdAt}</Text>
</div>
<div>
<Heading tag="h3" size="tiny">{$t('updated_at')}</Heading>
<Text>{group.updatedAt}</Text>
</div>
<div>
<Heading tag="h3" size="tiny">{$t('id')}</Heading>
<Code>{group.id}</Code>
</div>
</Stack>
</div>
</CardBody>
</Card>
</Stack>
</Container>
</div>
</AdminPageLayout>

View File

@ -0,0 +1,26 @@
import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getUsersForGroupAdmin, searchGroupsAdmin } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true });
const $t = await getFormatter();
const [group] = await searchGroupsAdmin({ id: params.id }).catch(() => []);
if (!group) {
redirect(302, AppRoute.ADMIN_GROUPS);
}
const users = await getUsersForGroupAdmin({ id: params.id });
return {
group,
users,
meta: {
title: $t('admin.group_details'),
},
};
}) satisfies PageLoad;