Browse Source

refactor(server, web): create shared link (#2879)

* refactor: shared links

* chore: open api

* fix: tsc error
Jason Rasmussen 2 years ago
parent
commit
868f629f32
61 changed files with 1917 additions and 2577 deletions
  1. 9 12
      mobile/openapi/.openapi-generator/FILES
  2. 10 12
      mobile/openapi/README.md
  3. 0 56
      mobile/openapi/doc/AlbumApi.md
  4. 0 172
      mobile/openapi/doc/AssetApi.md
  5. 0 20
      mobile/openapi/doc/CreateAssetsShareLinkDto.md
  6. 196 20
      mobile/openapi/doc/SharedLinkApi.md
  7. 8 6
      mobile/openapi/doc/SharedLinkCreateDto.md
  8. 1 1
      mobile/openapi/doc/SharedLinkEditDto.md
  9. 1 1
      mobile/openapi/doc/SharedLinkResponseDto.md
  10. 3 4
      mobile/openapi/lib/api.dart
  11. 0 47
      mobile/openapi/lib/api/album_api.dart
  12. 0 157
      mobile/openapi/lib/api/asset_api.dart
  13. 0 253
      mobile/openapi/lib/api/share_api.dart
  14. 426 0
      mobile/openapi/lib/api/shared_link_api.dart
  15. 4 6
      mobile/openapi/lib/api_client.dart
  16. 0 194
      mobile/openapi/lib/model/create_album_share_link_dto.dart
  17. 63 75
      mobile/openapi/lib/model/shared_link_create_dto.dart
  18. 20 20
      mobile/openapi/lib/model/shared_link_edit_dto.dart
  19. 2 7
      mobile/openapi/lib/model/shared_link_response_dto.dart
  20. 0 5
      mobile/openapi/test/album_api_test.dart
  21. 0 15
      mobile/openapi/test/asset_api_test.dart
  22. 0 52
      mobile/openapi/test/create_album_share_link_dto_test.dart
  23. 19 4
      mobile/openapi/test/shared_link_api_test.dart
  24. 21 11
      mobile/openapi/test/shared_link_create_dto_test.dart
  25. 3 3
      mobile/openapi/test/shared_link_edit_dto_test.dart
  26. 13 4
      server/e2e/album.e2e-spec.ts
  27. 241 282
      server/immich-openapi-specs.json
  28. 3 0
      server/src/domain/access/access.repository.ts
  29. 28 5
      server/src/domain/auth/auth.service.ts
  30. 0 12
      server/src/domain/shared-link/dto/create-shared-link.dto.ts
  31. 0 18
      server/src/domain/shared-link/dto/edit-shared-link.dto.ts
  32. 0 2
      server/src/domain/shared-link/dto/index.ts
  33. 2 3
      server/src/domain/shared-link/index.ts
  34. 0 1
      server/src/domain/shared-link/response-dto/index.ts
  35. 3 3
      server/src/domain/shared-link/shared-link-response.dto.ts
  36. 0 80
      server/src/domain/shared-link/shared-link.core.ts
  37. 53 0
      server/src/domain/shared-link/shared-link.dto.ts
  38. 1 1
      server/src/domain/shared-link/shared-link.repository.ts
  39. 149 2
      server/src/domain/shared-link/shared-link.service.spec.ts
  40. 112 6
      server/src/domain/shared-link/shared-link.service.ts
  41. 1 7
      server/src/immich/api-v1/album/album.controller.ts
  42. 3 14
      server/src/immich/api-v1/album/album.service.spec.ts
  43. 9 34
      server/src/immich/api-v1/album/album.service.ts
  44. 0 35
      server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts
  45. 1 31
      server/src/immich/api-v1/asset/asset.controller.ts
  46. 2 91
      server/src/immich/api-v1/asset/asset.service.spec.ts
  47. 3 68
      server/src/immich/api-v1/asset/asset.service.ts
  48. 0 41
      server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts
  49. 38 5
      server/src/immich/controllers/shared-link.controller.ts
  50. 5 2
      server/src/infra/entities/shared-link.entity.ts
  51. 9 0
      server/src/infra/repositories/access.repository.ts
  52. 25 4
      server/test/fixtures.ts
  53. 3 0
      server/test/repositories/access.repository.mock.ts
  54. 6 6
      web/src/api/api.ts
  55. 258 517
      web/src/api/open-api/api.ts
  56. 1 1
      web/src/lib/components/album-page/user-selection-modal.svelte
  57. 45 14
      web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
  58. 14 16
      web/src/lib/components/share-page/individual-shared-viewer.svelte
  59. 62 79
      web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
  60. 1 1
      web/src/routes/(user)/share/[key]/+page.server.ts
  61. 40 39
      web/src/routes/(user)/sharing/sharedlinks/+page.svelte

+ 9 - 12
mobile/openapi/.openapi-generator/FILES

@@ -37,8 +37,6 @@ doc/CheckDuplicateAssetResponseDto.md
 doc/CheckExistingAssetsDto.md
 doc/CheckExistingAssetsResponseDto.md
 doc/CreateAlbumDto.md
-doc/CreateAlbumShareLinkDto.md
-doc/CreateAssetsShareLinkDto.md
 doc/CreateProfileImageResponseDto.md
 doc/CreateTagDto.md
 doc/CreateUserDto.md
@@ -48,7 +46,6 @@ doc/DeleteAssetDto.md
 doc/DeleteAssetResponseDto.md
 doc/DeleteAssetStatus.md
 doc/DownloadFilesDto.md
-doc/EditSharedLinkDto.md
 doc/ExifResponseDto.md
 doc/GetAssetByTimeBucketDto.md
 doc/GetAssetCountByTimeBucketDto.md
@@ -89,7 +86,9 @@ doc/ServerInfoResponseDto.md
 doc/ServerPingResponse.md
 doc/ServerStatsResponseDto.md
 doc/ServerVersionReponseDto.md
-doc/ShareApi.md
+doc/SharedLinkApi.md
+doc/SharedLinkCreateDto.md
+doc/SharedLinkEditDto.md
 doc/SharedLinkResponseDto.md
 doc/SharedLinkType.md
 doc/SignUpDto.md
@@ -128,7 +127,7 @@ lib/api/partner_api.dart
 lib/api/person_api.dart
 lib/api/search_api.dart
 lib/api/server_info_api.dart
-lib/api/share_api.dart
+lib/api/shared_link_api.dart
 lib/api/system_config_api.dart
 lib/api/tag_api.dart
 lib/api/user_api.dart
@@ -170,8 +169,6 @@ lib/model/check_duplicate_asset_response_dto.dart
 lib/model/check_existing_assets_dto.dart
 lib/model/check_existing_assets_response_dto.dart
 lib/model/create_album_dto.dart
-lib/model/create_album_share_link_dto.dart
-lib/model/create_assets_share_link_dto.dart
 lib/model/create_profile_image_response_dto.dart
 lib/model/create_tag_dto.dart
 lib/model/create_user_dto.dart
@@ -181,7 +178,6 @@ lib/model/delete_asset_dto.dart
 lib/model/delete_asset_response_dto.dart
 lib/model/delete_asset_status.dart
 lib/model/download_files_dto.dart
-lib/model/edit_shared_link_dto.dart
 lib/model/exif_response_dto.dart
 lib/model/get_asset_by_time_bucket_dto.dart
 lib/model/get_asset_count_by_time_bucket_dto.dart
@@ -216,6 +212,8 @@ lib/model/server_info_response_dto.dart
 lib/model/server_ping_response.dart
 lib/model/server_stats_response_dto.dart
 lib/model/server_version_reponse_dto.dart
+lib/model/shared_link_create_dto.dart
+lib/model/shared_link_edit_dto.dart
 lib/model/shared_link_response_dto.dart
 lib/model/shared_link_type.dart
 lib/model/sign_up_dto.dart
@@ -274,8 +272,6 @@ test/check_duplicate_asset_response_dto_test.dart
 test/check_existing_assets_dto_test.dart
 test/check_existing_assets_response_dto_test.dart
 test/create_album_dto_test.dart
-test/create_album_share_link_dto_test.dart
-test/create_assets_share_link_dto_test.dart
 test/create_profile_image_response_dto_test.dart
 test/create_tag_dto_test.dart
 test/create_user_dto_test.dart
@@ -285,7 +281,6 @@ test/delete_asset_dto_test.dart
 test/delete_asset_response_dto_test.dart
 test/delete_asset_status_test.dart
 test/download_files_dto_test.dart
-test/edit_shared_link_dto_test.dart
 test/exif_response_dto_test.dart
 test/get_asset_by_time_bucket_dto_test.dart
 test/get_asset_count_by_time_bucket_dto_test.dart
@@ -326,7 +321,9 @@ test/server_info_response_dto_test.dart
 test/server_ping_response_test.dart
 test/server_stats_response_dto_test.dart
 test/server_version_reponse_dto_test.dart
-test/share_api_test.dart
+test/shared_link_api_test.dart
+test/shared_link_create_dto_test.dart
+test/shared_link_edit_dto_test.dart
 test/shared_link_response_dto_test.dart
 test/shared_link_type_test.dart
 test/sign_up_dto_test.dart

+ 10 - 12
mobile/openapi/README.md

@@ -80,7 +80,6 @@ Class | Method | HTTP request | Description
 *AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets | 
 *AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | 
 *AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album | 
-*AlbumApi* | [**createAlbumSharedLink**](doc//AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link | 
 *AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} | 
 *AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{id}/download | 
 *AlbumApi* | [**getAlbumCount**](doc//AlbumApi.md#getalbumcount) | **GET** /album/count | 
@@ -89,11 +88,9 @@ Class | Method | HTTP request | Description
 *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | 
 *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | 
 *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | 
-*AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add | 
 *AssetApi* | [**bulkUploadCheck**](doc//AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check | 
 *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
-*AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | 
 *AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset | 
 *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download/{id} | 
 *AssetApi* | [**downloadFiles**](doc//AssetApi.md#downloadfiles) | **POST** /asset/download-files | 
@@ -111,7 +108,6 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 *AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
 *AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
-*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove | 
 *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | 
 *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} | 
 *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} | 
@@ -146,11 +142,14 @@ Class | Method | HTTP request | Description
 *ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 *ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
-*ShareApi* | [**getAllSharedLinks**](doc//ShareApi.md#getallsharedlinks) | **GET** /share | 
-*ShareApi* | [**getMySharedLink**](doc//ShareApi.md#getmysharedlink) | **GET** /share/me | 
-*ShareApi* | [**getSharedLinkById**](doc//ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} | 
-*ShareApi* | [**removeSharedLink**](doc//ShareApi.md#removesharedlink) | **DELETE** /share/{id} | 
-*ShareApi* | [**updateSharedLink**](doc//ShareApi.md#updatesharedlink) | **PATCH** /share/{id} | 
+*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets | 
+*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link | 
+*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link | 
+*SharedLinkApi* | [**getMySharedLink**](doc//SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me | 
+*SharedLinkApi* | [**getSharedLinkById**](doc//SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} | 
+*SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} | 
+*SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets | 
+*SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} | 
 *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | 
 *SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults | 
 *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | 
@@ -207,8 +206,6 @@ Class | Method | HTTP request | Description
  - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
  - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
  - [CreateAlbumDto](doc//CreateAlbumDto.md)
- - [CreateAlbumShareLinkDto](doc//CreateAlbumShareLinkDto.md)
- - [CreateAssetsShareLinkDto](doc//CreateAssetsShareLinkDto.md)
  - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
  - [CreateTagDto](doc//CreateTagDto.md)
  - [CreateUserDto](doc//CreateUserDto.md)
@@ -218,7 +215,6 @@ Class | Method | HTTP request | Description
  - [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
  - [DeleteAssetStatus](doc//DeleteAssetStatus.md)
  - [DownloadFilesDto](doc//DownloadFilesDto.md)
- - [EditSharedLinkDto](doc//EditSharedLinkDto.md)
  - [ExifResponseDto](doc//ExifResponseDto.md)
  - [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
  - [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
@@ -253,6 +249,8 @@ Class | Method | HTTP request | Description
  - [ServerPingResponse](doc//ServerPingResponse.md)
  - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
  - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
+ - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
+ - [SharedLinkEditDto](doc//SharedLinkEditDto.md)
  - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
  - [SharedLinkType](doc//SharedLinkType.md)
  - [SignUpDto](doc//SignUpDto.md)

+ 0 - 56
mobile/openapi/doc/AlbumApi.md

@@ -12,7 +12,6 @@ Method | HTTP request | Description
 [**addAssetsToAlbum**](AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets | 
 [**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | 
 [**createAlbum**](AlbumApi.md#createalbum) | **POST** /album | 
-[**createAlbumSharedLink**](AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link | 
 [**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} | 
 [**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{id}/download | 
 [**getAlbumCount**](AlbumApi.md#getalbumcount) | **GET** /album/count | 
@@ -194,61 +193,6 @@ Name | Type | Description  | Notes
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
-# **createAlbumSharedLink**
-> SharedLinkResponseDto createAlbumSharedLink(createAlbumShareLinkDto)
-
-
-
-### Example
-```dart
-import 'package:openapi/api.dart';
-// TODO Configure API key authorization: cookie
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
-// TODO Configure API key authorization: api_key
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
-// TODO Configure HTTP Bearer authorization: bearer
-// Case 1. Use String Token
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
-// Case 2. Use Function which generate token.
-// String yourTokenGeneratorFunction() { ... }
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
-
-final api_instance = AlbumApi();
-final createAlbumShareLinkDto = CreateAlbumShareLinkDto(); // CreateAlbumShareLinkDto | 
-
-try {
-    final result = api_instance.createAlbumSharedLink(createAlbumShareLinkDto);
-    print(result);
-} catch (e) {
-    print('Exception when calling AlbumApi->createAlbumSharedLink: $e\n');
-}
-```
-
-### Parameters
-
-Name | Type | Description  | Notes
-------------- | ------------- | ------------- | -------------
- **createAlbumShareLinkDto** | [**CreateAlbumShareLinkDto**](CreateAlbumShareLinkDto.md)|  | 
-
-### Return type
-
-[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
-
-### Authorization
-
-[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
-
-### HTTP request headers
-
- - **Content-Type**: application/json
- - **Accept**: application/json
-
-[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
-
 # **deleteAlbum**
 > deleteAlbum(id)
 

+ 0 - 172
mobile/openapi/doc/AssetApi.md

@@ -9,11 +9,9 @@ All URIs are relative to */api*
 
 Method | HTTP request | Description
 ------------- | ------------- | -------------
-[**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add | 
 [**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check | 
 [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
-[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | 
 [**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset | 
 [**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download/{id} | 
 [**downloadFiles**](AssetApi.md#downloadfiles) | **POST** /asset/download-files | 
@@ -31,70 +29,12 @@ Method | HTTP request | Description
 [**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 [**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
 [**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
-[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove | 
 [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | 
 [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} | 
 [**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} | 
 [**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload | 
 
 
-# **addAssetsToSharedLink**
-> SharedLinkResponseDto addAssetsToSharedLink(addAssetsDto, key)
-
-
-
-### Example
-```dart
-import 'package:openapi/api.dart';
-// TODO Configure API key authorization: cookie
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
-// TODO Configure API key authorization: api_key
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
-// TODO Configure HTTP Bearer authorization: bearer
-// Case 1. Use String Token
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
-// Case 2. Use Function which generate token.
-// String yourTokenGeneratorFunction() { ... }
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
-
-final api_instance = AssetApi();
-final addAssetsDto = AddAssetsDto(); // AddAssetsDto | 
-final key = key_example; // String | 
-
-try {
-    final result = api_instance.addAssetsToSharedLink(addAssetsDto, key);
-    print(result);
-} catch (e) {
-    print('Exception when calling AssetApi->addAssetsToSharedLink: $e\n');
-}
-```
-
-### Parameters
-
-Name | Type | Description  | Notes
-------------- | ------------- | ------------- | -------------
- **addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)|  | 
- **key** | **String**|  | [optional] 
-
-### Return type
-
-[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
-
-### Authorization
-
-[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
-
-### HTTP request headers
-
- - **Content-Type**: application/json
- - **Accept**: application/json
-
-[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
-
 # **bulkUploadCheck**
 > AssetBulkUploadCheckResponseDto bulkUploadCheck(assetBulkUploadCheckDto)
 
@@ -268,61 +208,6 @@ Name | Type | Description  | Notes
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
-# **createAssetsSharedLink**
-> SharedLinkResponseDto createAssetsSharedLink(createAssetsShareLinkDto)
-
-
-
-### Example
-```dart
-import 'package:openapi/api.dart';
-// TODO Configure API key authorization: cookie
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
-// TODO Configure API key authorization: api_key
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
-// TODO Configure HTTP Bearer authorization: bearer
-// Case 1. Use String Token
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
-// Case 2. Use Function which generate token.
-// String yourTokenGeneratorFunction() { ... }
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
-
-final api_instance = AssetApi();
-final createAssetsShareLinkDto = CreateAssetsShareLinkDto(); // CreateAssetsShareLinkDto | 
-
-try {
-    final result = api_instance.createAssetsSharedLink(createAssetsShareLinkDto);
-    print(result);
-} catch (e) {
-    print('Exception when calling AssetApi->createAssetsSharedLink: $e\n');
-}
-```
-
-### Parameters
-
-Name | Type | Description  | Notes
-------------- | ------------- | ------------- | -------------
- **createAssetsShareLinkDto** | [**CreateAssetsShareLinkDto**](CreateAssetsShareLinkDto.md)|  | 
-
-### Return type
-
-[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
-
-### Authorization
-
-[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
-
-### HTTP request headers
-
- - **Content-Type**: application/json
- - **Accept**: application/json
-
-[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
-
 # **deleteAsset**
 > List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
 
@@ -1274,63 +1159,6 @@ Name | Type | Description  | Notes
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
-# **removeAssetsFromSharedLink**
-> SharedLinkResponseDto removeAssetsFromSharedLink(removeAssetsDto, key)
-
-
-
-### Example
-```dart
-import 'package:openapi/api.dart';
-// TODO Configure API key authorization: cookie
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
-// TODO Configure API key authorization: api_key
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
-// TODO Configure HTTP Bearer authorization: bearer
-// Case 1. Use String Token
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
-// Case 2. Use Function which generate token.
-// String yourTokenGeneratorFunction() { ... }
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
-
-final api_instance = AssetApi();
-final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto | 
-final key = key_example; // String | 
-
-try {
-    final result = api_instance.removeAssetsFromSharedLink(removeAssetsDto, key);
-    print(result);
-} catch (e) {
-    print('Exception when calling AssetApi->removeAssetsFromSharedLink: $e\n');
-}
-```
-
-### Parameters
-
-Name | Type | Description  | Notes
-------------- | ------------- | ------------- | -------------
- **removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)|  | 
- **key** | **String**|  | [optional] 
-
-### Return type
-
-[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
-
-### Authorization
-
-[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
-
-### HTTP request headers
-
- - **Content-Type**: application/json
- - **Accept**: application/json
-
-[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
-
 # **searchAsset**
 > List<AssetResponseDto> searchAsset(searchAssetDto)
 

+ 0 - 20
mobile/openapi/doc/CreateAssetsShareLinkDto.md

@@ -1,20 +0,0 @@
-# openapi.model.CreateAssetsShareLinkDto
-
-## Load the model package
-```dart
-import 'package:openapi/api.dart';
-```
-
-## Properties
-Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
-**assetIds** | **List<String>** |  | [default to const []]
-**expiresAt** | [**DateTime**](DateTime.md) |  | [optional] 
-**allowUpload** | **bool** |  | [optional] 
-**allowDownload** | **bool** |  | [optional] 
-**showExif** | **bool** |  | [optional] 
-**description** | **String** |  | [optional] 
-
-[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
-
-

+ 196 - 20
mobile/openapi/doc/ShareApi.md → mobile/openapi/doc/SharedLinkApi.md

@@ -1,4 +1,4 @@
-# openapi.api.ShareApi
+# openapi.api.SharedLinkApi
 
 ## Load the API package
 ```dart
@@ -9,13 +9,130 @@ All URIs are relative to */api*
 
 Method | HTTP request | Description
 ------------- | ------------- | -------------
-[**getAllSharedLinks**](ShareApi.md#getallsharedlinks) | **GET** /share | 
-[**getMySharedLink**](ShareApi.md#getmysharedlink) | **GET** /share/me | 
-[**getSharedLinkById**](ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} | 
-[**removeSharedLink**](ShareApi.md#removesharedlink) | **DELETE** /share/{id} | 
-[**updateSharedLink**](ShareApi.md#updatesharedlink) | **PATCH** /share/{id} | 
+[**addSharedLinkAssets**](SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets | 
+[**createSharedLink**](SharedLinkApi.md#createsharedlink) | **POST** /shared-link | 
+[**getAllSharedLinks**](SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link | 
+[**getMySharedLink**](SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me | 
+[**getSharedLinkById**](SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} | 
+[**removeSharedLink**](SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} | 
+[**removeSharedLinkAssets**](SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets | 
+[**updateSharedLink**](SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} | 
 
 
+# **addSharedLinkAssets**
+> List<AssetIdsResponseDto> addSharedLinkAssets(id, assetIdsDto, key)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = SharedLinkApi();
+final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final assetIdsDto = AssetIdsDto(); // AssetIdsDto | 
+final key = key_example; // String | 
+
+try {
+    final result = api_instance.addSharedLinkAssets(id, assetIdsDto, key);
+    print(result);
+} catch (e) {
+    print('Exception when calling SharedLinkApi->addSharedLinkAssets: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **id** | **String**|  | 
+ **assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)|  | 
+ **key** | **String**|  | [optional] 
+
+### Return type
+
+[**List<AssetIdsResponseDto>**](AssetIdsResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **createSharedLink**
+> SharedLinkResponseDto createSharedLink(sharedLinkCreateDto)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = SharedLinkApi();
+final sharedLinkCreateDto = SharedLinkCreateDto(); // SharedLinkCreateDto | 
+
+try {
+    final result = api_instance.createSharedLink(sharedLinkCreateDto);
+    print(result);
+} catch (e) {
+    print('Exception when calling SharedLinkApi->createSharedLink: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **sharedLinkCreateDto** | [**SharedLinkCreateDto**](SharedLinkCreateDto.md)|  | 
+
+### Return type
+
+[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
 # **getAllSharedLinks**
 > List<SharedLinkResponseDto> getAllSharedLinks()
 
@@ -39,13 +156,13 @@ import 'package:openapi/api.dart';
 // String yourTokenGeneratorFunction() { ... }
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
-final api_instance = ShareApi();
+final api_instance = SharedLinkApi();
 
 try {
     final result = api_instance.getAllSharedLinks();
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->getAllSharedLinks: $e\n');
+    print('Exception when calling SharedLinkApi->getAllSharedLinks: $e\n');
 }
 ```
 
@@ -90,14 +207,14 @@ import 'package:openapi/api.dart';
 // String yourTokenGeneratorFunction() { ... }
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
-final api_instance = ShareApi();
+final api_instance = SharedLinkApi();
 final key = key_example; // String | 
 
 try {
     final result = api_instance.getMySharedLink(key);
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->getMySharedLink: $e\n');
+    print('Exception when calling SharedLinkApi->getMySharedLink: $e\n');
 }
 ```
 
@@ -145,14 +262,14 @@ import 'package:openapi/api.dart';
 // String yourTokenGeneratorFunction() { ... }
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
-final api_instance = ShareApi();
+final api_instance = SharedLinkApi();
 final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
 
 try {
     final result = api_instance.getSharedLinkById(id);
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->getSharedLinkById: $e\n');
+    print('Exception when calling SharedLinkApi->getSharedLinkById: $e\n');
 }
 ```
 
@@ -200,13 +317,13 @@ import 'package:openapi/api.dart';
 // String yourTokenGeneratorFunction() { ... }
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
-final api_instance = ShareApi();
+final api_instance = SharedLinkApi();
 final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
 
 try {
     api_instance.removeSharedLink(id);
 } catch (e) {
-    print('Exception when calling ShareApi->removeSharedLink: $e\n');
+    print('Exception when calling SharedLinkApi->removeSharedLink: $e\n');
 }
 ```
 
@@ -231,8 +348,67 @@ void (empty response body)
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
+# **removeSharedLinkAssets**
+> List<AssetIdsResponseDto> removeSharedLinkAssets(id, assetIdsDto, key)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = SharedLinkApi();
+final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final assetIdsDto = AssetIdsDto(); // AssetIdsDto | 
+final key = key_example; // String | 
+
+try {
+    final result = api_instance.removeSharedLinkAssets(id, assetIdsDto, key);
+    print(result);
+} catch (e) {
+    print('Exception when calling SharedLinkApi->removeSharedLinkAssets: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **id** | **String**|  | 
+ **assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)|  | 
+ **key** | **String**|  | [optional] 
+
+### Return type
+
+[**List<AssetIdsResponseDto>**](AssetIdsResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
 # **updateSharedLink**
-> SharedLinkResponseDto updateSharedLink(id, editSharedLinkDto)
+> SharedLinkResponseDto updateSharedLink(id, sharedLinkEditDto)
 
 
 
@@ -254,15 +430,15 @@ import 'package:openapi/api.dart';
 // String yourTokenGeneratorFunction() { ... }
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
-final api_instance = ShareApi();
+final api_instance = SharedLinkApi();
 final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
-final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto | 
+final sharedLinkEditDto = SharedLinkEditDto(); // SharedLinkEditDto | 
 
 try {
-    final result = api_instance.updateSharedLink(id, editSharedLinkDto);
+    final result = api_instance.updateSharedLink(id, sharedLinkEditDto);
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->updateSharedLink: $e\n');
+    print('Exception when calling SharedLinkApi->updateSharedLink: $e\n');
 }
 ```
 
@@ -271,7 +447,7 @@ try {
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
  **id** | **String**|  | 
- **editSharedLinkDto** | [**EditSharedLinkDto**](EditSharedLinkDto.md)|  | 
+ **sharedLinkEditDto** | [**SharedLinkEditDto**](SharedLinkEditDto.md)|  | 
 
 ### Return type
 

+ 8 - 6
mobile/openapi/doc/CreateAlbumShareLinkDto.md → mobile/openapi/doc/SharedLinkCreateDto.md

@@ -1,4 +1,4 @@
-# openapi.model.CreateAlbumShareLinkDto
+# openapi.model.SharedLinkCreateDto
 
 ## Load the model package
 ```dart
@@ -8,12 +8,14 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
-**albumId** | **String** |  | 
-**expiresAt** | [**DateTime**](DateTime.md) |  | [optional] 
-**allowUpload** | **bool** |  | [optional] 
-**allowDownload** | **bool** |  | [optional] 
-**showExif** | **bool** |  | [optional] 
+**type** | [**SharedLinkType**](SharedLinkType.md) |  | 
+**assetIds** | **List<String>** |  | [optional] [default to const []]
+**albumId** | **String** |  | [optional] 
 **description** | **String** |  | [optional] 
+**expiresAt** | [**DateTime**](DateTime.md) |  | [optional] 
+**allowUpload** | **bool** |  | [optional] [default to false]
+**allowDownload** | **bool** |  | [optional] [default to true]
+**showExif** | **bool** |  | [optional] [default to true]
 
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 

+ 1 - 1
mobile/openapi/doc/EditSharedLinkDto.md → mobile/openapi/doc/SharedLinkEditDto.md

@@ -1,4 +1,4 @@
-# openapi.model.EditSharedLinkDto
+# openapi.model.SharedLinkEditDto
 
 ## Load the model package
 ```dart

+ 1 - 1
mobile/openapi/doc/SharedLinkResponseDto.md

@@ -10,7 +10,7 @@ Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **type** | [**SharedLinkType**](SharedLinkType.md) |  | 
 **id** | **String** |  | 
-**description** | **String** |  | [optional] 
+**description** | **String** |  | 
 **userId** | **String** |  | 
 **key** | **String** |  | 
 **createdAt** | [**DateTime**](DateTime.md) |  | 

+ 3 - 4
mobile/openapi/lib/api.dart

@@ -38,7 +38,7 @@ part 'api/partner_api.dart';
 part 'api/person_api.dart';
 part 'api/search_api.dart';
 part 'api/server_info_api.dart';
-part 'api/share_api.dart';
+part 'api/shared_link_api.dart';
 part 'api/system_config_api.dart';
 part 'api/tag_api.dart';
 part 'api/user_api.dart';
@@ -73,8 +73,6 @@ part 'model/check_duplicate_asset_response_dto.dart';
 part 'model/check_existing_assets_dto.dart';
 part 'model/check_existing_assets_response_dto.dart';
 part 'model/create_album_dto.dart';
-part 'model/create_album_share_link_dto.dart';
-part 'model/create_assets_share_link_dto.dart';
 part 'model/create_profile_image_response_dto.dart';
 part 'model/create_tag_dto.dart';
 part 'model/create_user_dto.dart';
@@ -84,7 +82,6 @@ part 'model/delete_asset_dto.dart';
 part 'model/delete_asset_response_dto.dart';
 part 'model/delete_asset_status.dart';
 part 'model/download_files_dto.dart';
-part 'model/edit_shared_link_dto.dart';
 part 'model/exif_response_dto.dart';
 part 'model/get_asset_by_time_bucket_dto.dart';
 part 'model/get_asset_count_by_time_bucket_dto.dart';
@@ -119,6 +116,8 @@ part 'model/server_info_response_dto.dart';
 part 'model/server_ping_response.dart';
 part 'model/server_stats_response_dto.dart';
 part 'model/server_version_reponse_dto.dart';
+part 'model/shared_link_create_dto.dart';
+part 'model/shared_link_edit_dto.dart';
 part 'model/shared_link_response_dto.dart';
 part 'model/shared_link_type.dart';
 part 'model/sign_up_dto.dart';

+ 0 - 47
mobile/openapi/lib/api/album_api.dart

@@ -175,53 +175,6 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /album/create-shared-link' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
-  Future<Response> createAlbumSharedLinkWithHttpInfo(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/album/create-shared-link';
-
-    // ignore: prefer_final_locals
-    Object? postBody = createAlbumShareLinkDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'POST',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
-  Future<SharedLinkResponseDto?> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
-    final response = await createAlbumSharedLinkWithHttpInfo(createAlbumShareLinkDto,);
-    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-
   /// Performs an HTTP 'DELETE /album/{id}' operation and returns the [Response].
   /// Parameters:
   ///

+ 0 - 157
mobile/openapi/lib/api/asset_api.dart

@@ -16,61 +16,6 @@ class AssetApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'PATCH /asset/shared-link/add' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [AddAssetsDto] addAssetsDto (required):
-  ///
-  /// * [String] key:
-  Future<Response> addAssetsToSharedLinkWithHttpInfo(AddAssetsDto addAssetsDto, { String? key, }) async {
-    // ignore: prefer_const_declarations
-    final path = r'/asset/shared-link/add';
-
-    // ignore: prefer_final_locals
-    Object? postBody = addAssetsDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    if (key != null) {
-      queryParams.addAll(_queryParams('', 'key', key));
-    }
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'PATCH',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [AddAssetsDto] addAssetsDto (required):
-  ///
-  /// * [String] key:
-  Future<SharedLinkResponseDto?> addAssetsToSharedLink(AddAssetsDto addAssetsDto, { String? key, }) async {
-    final response = await addAssetsToSharedLinkWithHttpInfo(addAssetsDto,  key: key, );
-    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-
   /// Checks if assets exist by checksums
   ///
   /// Note: This method returns the HTTP [Response].
@@ -235,53 +180,6 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/shared-link' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
-  Future<Response> createAssetsSharedLinkWithHttpInfo(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/asset/shared-link';
-
-    // ignore: prefer_final_locals
-    Object? postBody = createAssetsShareLinkDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'POST',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
-  Future<SharedLinkResponseDto?> createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
-    final response = await createAssetsSharedLinkWithHttpInfo(createAssetsShareLinkDto,);
-    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-
   /// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
   /// Parameters:
   ///
@@ -1225,61 +1123,6 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'PATCH /asset/shared-link/remove' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [RemoveAssetsDto] removeAssetsDto (required):
-  ///
-  /// * [String] key:
-  Future<Response> removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto, { String? key, }) async {
-    // ignore: prefer_const_declarations
-    final path = r'/asset/shared-link/remove';
-
-    // ignore: prefer_final_locals
-    Object? postBody = removeAssetsDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    if (key != null) {
-      queryParams.addAll(_queryParams('', 'key', key));
-    }
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'PATCH',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [RemoveAssetsDto] removeAssetsDto (required):
-  ///
-  /// * [String] key:
-  Future<SharedLinkResponseDto?> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String? key, }) async {
-    final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto,  key: key, );
-    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-
   /// Performs an HTTP 'POST /asset/search' operation and returns the [Response].
   /// Parameters:
   ///

+ 0 - 253
mobile/openapi/lib/api/share_api.dart

@@ -1,253 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// 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 ShareApi {
-  ShareApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
-
-  final ApiClient apiClient;
-
-  /// Performs an HTTP 'GET /share' operation and returns the [Response].
-  Future<Response> getAllSharedLinksWithHttpInfo() async {
-    // ignore: prefer_const_declarations
-    final path = r'/share';
-
-    // ignore: prefer_final_locals
-    Object? postBody;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>[];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'GET',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  Future<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
-    final response = await getAllSharedLinksWithHttpInfo();
-    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<SharedLinkResponseDto>') as List)
-        .cast<SharedLinkResponseDto>()
-        .toList();
-
-    }
-    return null;
-  }
-
-  /// Performs an HTTP 'GET /share/me' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [String] key:
-  Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
-    // ignore: prefer_const_declarations
-    final path = r'/share/me';
-
-    // ignore: prefer_final_locals
-    Object? postBody;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    if (key != null) {
-      queryParams.addAll(_queryParams('', 'key', key));
-    }
-
-    const contentTypes = <String>[];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'GET',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [String] key:
-  Future<SharedLinkResponseDto?> getMySharedLink({ String? key, }) async {
-    final response = await getMySharedLinkWithHttpInfo( key: key, );
-    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-
-  /// Performs an HTTP 'GET /share/{id}' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  Future<Response> getSharedLinkByIdWithHttpInfo(String id,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/share/{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(
-      path,
-      'GET',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  Future<SharedLinkResponseDto?> getSharedLinkById(String id,) async {
-    final response = await getSharedLinkByIdWithHttpInfo(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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-
-  /// Performs an HTTP 'DELETE /share/{id}' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  Future<Response> removeSharedLinkWithHttpInfo(String id,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/share/{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(
-      path,
-      'DELETE',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  Future<void> removeSharedLink(String id,) async {
-    final response = await removeSharedLinkWithHttpInfo(id,);
-    if (response.statusCode >= HttpStatus.badRequest) {
-      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
-    }
-  }
-
-  /// Performs an HTTP 'PATCH /share/{id}' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  ///
-  /// * [EditSharedLinkDto] editSharedLinkDto (required):
-  Future<Response> updateSharedLinkWithHttpInfo(String id, EditSharedLinkDto editSharedLinkDto,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/share/{id}'
-      .replaceAll('{id}', id);
-
-    // ignore: prefer_final_locals
-    Object? postBody = editSharedLinkDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'PATCH',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  ///
-  /// * [EditSharedLinkDto] editSharedLinkDto (required):
-  Future<SharedLinkResponseDto?> updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto,) async {
-    final response = await updateSharedLinkWithHttpInfo(id, editSharedLinkDto,);
-    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
-    
-    }
-    return null;
-  }
-}

+ 426 - 0
mobile/openapi/lib/api/shared_link_api.dart

@@ -0,0 +1,426 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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 SharedLinkApi {
+  SharedLinkApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
+
+  final ApiClient apiClient;
+
+  /// Performs an HTTP 'PUT /shared-link/{id}/assets' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  ///
+  /// * [AssetIdsDto] assetIdsDto (required):
+  ///
+  /// * [String] key:
+  Future<Response> addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link/{id}/assets'
+      .replaceAll('{id}', id);
+
+    // ignore: prefer_final_locals
+    Object? postBody = assetIdsDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    if (key != null) {
+      queryParams.addAll(_queryParams('', 'key', key));
+    }
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'PUT',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  ///
+  /// * [AssetIdsDto] assetIdsDto (required):
+  ///
+  /// * [String] key:
+  Future<List<AssetIdsResponseDto>?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
+    final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,  key: key, );
+    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<AssetIdsResponseDto>') as List)
+        .cast<AssetIdsResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'POST /shared-link' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [SharedLinkCreateDto] sharedLinkCreateDto (required):
+  Future<Response> createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link';
+
+    // ignore: prefer_final_locals
+    Object? postBody = sharedLinkCreateDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [SharedLinkCreateDto] sharedLinkCreateDto (required):
+  Future<SharedLinkResponseDto?> createSharedLink(SharedLinkCreateDto sharedLinkCreateDto,) async {
+    final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto,);
+    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
+    
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'GET /shared-link' operation and returns the [Response].
+  Future<Response> getAllSharedLinksWithHttpInfo() async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  Future<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
+    final response = await getAllSharedLinksWithHttpInfo();
+    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<SharedLinkResponseDto>') as List)
+        .cast<SharedLinkResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] key:
+  Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link/me';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    if (key != null) {
+      queryParams.addAll(_queryParams('', 'key', key));
+    }
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] key:
+  Future<SharedLinkResponseDto?> getMySharedLink({ String? key, }) async {
+    final response = await getMySharedLinkWithHttpInfo( key: key, );
+    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
+    
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'GET /shared-link/{id}' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  Future<Response> getSharedLinkByIdWithHttpInfo(String id,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link/{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(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  Future<SharedLinkResponseDto?> getSharedLinkById(String id,) async {
+    final response = await getSharedLinkByIdWithHttpInfo(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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
+    
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'DELETE /shared-link/{id}' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  Future<Response> removeSharedLinkWithHttpInfo(String id,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link/{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(
+      path,
+      'DELETE',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  Future<void> removeSharedLink(String id,) async {
+    final response = await removeSharedLinkWithHttpInfo(id,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+  }
+
+  /// Performs an HTTP 'DELETE /shared-link/{id}/assets' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  ///
+  /// * [AssetIdsDto] assetIdsDto (required):
+  ///
+  /// * [String] key:
+  Future<Response> removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link/{id}/assets'
+      .replaceAll('{id}', id);
+
+    // ignore: prefer_final_locals
+    Object? postBody = assetIdsDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    if (key != null) {
+      queryParams.addAll(_queryParams('', 'key', key));
+    }
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'DELETE',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  ///
+  /// * [AssetIdsDto] assetIdsDto (required):
+  ///
+  /// * [String] key:
+  Future<List<AssetIdsResponseDto>?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, }) async {
+    final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto,  key: key, );
+    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<AssetIdsResponseDto>') as List)
+        .cast<AssetIdsResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'PATCH /shared-link/{id}' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  ///
+  /// * [SharedLinkEditDto] sharedLinkEditDto (required):
+  Future<Response> updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/shared-link/{id}'
+      .replaceAll('{id}', id);
+
+    // ignore: prefer_final_locals
+    Object? postBody = sharedLinkEditDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'PATCH',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] id (required):
+  ///
+  /// * [SharedLinkEditDto] sharedLinkEditDto (required):
+  Future<SharedLinkResponseDto?> updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto,) async {
+    final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto,);
+    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), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
+    
+    }
+    return null;
+  }
+}

+ 4 - 6
mobile/openapi/lib/api_client.dart

@@ -241,10 +241,6 @@ class ApiClient {
           return CheckExistingAssetsResponseDto.fromJson(value);
         case 'CreateAlbumDto':
           return CreateAlbumDto.fromJson(value);
-        case 'CreateAlbumShareLinkDto':
-          return CreateAlbumShareLinkDto.fromJson(value);
-        case 'CreateAssetsShareLinkDto':
-          return CreateAssetsShareLinkDto.fromJson(value);
         case 'CreateProfileImageResponseDto':
           return CreateProfileImageResponseDto.fromJson(value);
         case 'CreateTagDto':
@@ -263,8 +259,6 @@ class ApiClient {
           return DeleteAssetStatusTypeTransformer().decode(value);
         case 'DownloadFilesDto':
           return DownloadFilesDto.fromJson(value);
-        case 'EditSharedLinkDto':
-          return EditSharedLinkDto.fromJson(value);
         case 'ExifResponseDto':
           return ExifResponseDto.fromJson(value);
         case 'GetAssetByTimeBucketDto':
@@ -333,6 +327,10 @@ class ApiClient {
           return ServerStatsResponseDto.fromJson(value);
         case 'ServerVersionReponseDto':
           return ServerVersionReponseDto.fromJson(value);
+        case 'SharedLinkCreateDto':
+          return SharedLinkCreateDto.fromJson(value);
+        case 'SharedLinkEditDto':
+          return SharedLinkEditDto.fromJson(value);
         case 'SharedLinkResponseDto':
           return SharedLinkResponseDto.fromJson(value);
         case 'SharedLinkType':

+ 0 - 194
mobile/openapi/lib/model/create_album_share_link_dto.dart

@@ -1,194 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// 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 CreateAlbumShareLinkDto {
-  /// Returns a new [CreateAlbumShareLinkDto] instance.
-  CreateAlbumShareLinkDto({
-    required this.albumId,
-    this.expiresAt,
-    this.allowUpload,
-    this.allowDownload,
-    this.showExif,
-    this.description,
-  });
-
-  String albumId;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  DateTime? expiresAt;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  bool? allowUpload;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  bool? allowDownload;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  bool? showExif;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  String? description;
-
-  @override
-  bool operator ==(Object other) => identical(this, other) || other is CreateAlbumShareLinkDto &&
-     other.albumId == albumId &&
-     other.expiresAt == expiresAt &&
-     other.allowUpload == allowUpload &&
-     other.allowDownload == allowDownload &&
-     other.showExif == showExif &&
-     other.description == description;
-
-  @override
-  int get hashCode =>
-    // ignore: unnecessary_parenthesis
-    (albumId.hashCode) +
-    (expiresAt == null ? 0 : expiresAt!.hashCode) +
-    (allowUpload == null ? 0 : allowUpload!.hashCode) +
-    (allowDownload == null ? 0 : allowDownload!.hashCode) +
-    (showExif == null ? 0 : showExif!.hashCode) +
-    (description == null ? 0 : description!.hashCode);
-
-  @override
-  String toString() => 'CreateAlbumShareLinkDto[albumId=$albumId, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, description=$description]';
-
-  Map<String, dynamic> toJson() {
-    final json = <String, dynamic>{};
-      json[r'albumId'] = this.albumId;
-    if (this.expiresAt != null) {
-      json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String();
-    } else {
-      // json[r'expiresAt'] = null;
-    }
-    if (this.allowUpload != null) {
-      json[r'allowUpload'] = this.allowUpload;
-    } else {
-      // json[r'allowUpload'] = null;
-    }
-    if (this.allowDownload != null) {
-      json[r'allowDownload'] = this.allowDownload;
-    } else {
-      // json[r'allowDownload'] = null;
-    }
-    if (this.showExif != null) {
-      json[r'showExif'] = this.showExif;
-    } else {
-      // json[r'showExif'] = null;
-    }
-    if (this.description != null) {
-      json[r'description'] = this.description;
-    } else {
-      // json[r'description'] = null;
-    }
-    return json;
-  }
-
-  /// Returns a new [CreateAlbumShareLinkDto] instance and imports its values from
-  /// [value] if it's a [Map], null otherwise.
-  // ignore: prefer_constructors_over_static_methods
-  static CreateAlbumShareLinkDto? fromJson(dynamic value) {
-    if (value is Map) {
-      final json = value.cast<String, dynamic>();
-
-      // Ensure that the map contains the required keys.
-      // Note 1: the values aren't checked for validity beyond being non-null.
-      // Note 2: this code is stripped in release mode!
-      assert(() {
-        requiredKeys.forEach((key) {
-          assert(json.containsKey(key), 'Required key "CreateAlbumShareLinkDto[$key]" is missing from JSON.');
-          assert(json[key] != null, 'Required key "CreateAlbumShareLinkDto[$key]" has a null value in JSON.');
-        });
-        return true;
-      }());
-
-      return CreateAlbumShareLinkDto(
-        albumId: mapValueOfType<String>(json, r'albumId')!,
-        expiresAt: mapDateTime(json, r'expiresAt', ''),
-        allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
-        allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
-        showExif: mapValueOfType<bool>(json, r'showExif'),
-        description: mapValueOfType<String>(json, r'description'),
-      );
-    }
-    return null;
-  }
-
-  static List<CreateAlbumShareLinkDto> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <CreateAlbumShareLinkDto>[];
-    if (json is List && json.isNotEmpty) {
-      for (final row in json) {
-        final value = CreateAlbumShareLinkDto.fromJson(row);
-        if (value != null) {
-          result.add(value);
-        }
-      }
-    }
-    return result.toList(growable: growable);
-  }
-
-  static Map<String, CreateAlbumShareLinkDto> mapFromJson(dynamic json) {
-    final map = <String, CreateAlbumShareLinkDto>{};
-    if (json is Map && json.isNotEmpty) {
-      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
-      for (final entry in json.entries) {
-        final value = CreateAlbumShareLinkDto.fromJson(entry.value);
-        if (value != null) {
-          map[entry.key] = value;
-        }
-      }
-    }
-    return map;
-  }
-
-  // maps a json object with a list of CreateAlbumShareLinkDto-objects as value to a dart map
-  static Map<String, List<CreateAlbumShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<CreateAlbumShareLinkDto>>{};
-    if (json is Map && json.isNotEmpty) {
-      // ignore: parameter_assignments
-      json = json.cast<String, dynamic>();
-      for (final entry in json.entries) {
-        map[entry.key] = CreateAlbumShareLinkDto.listFromJson(entry.value, growable: growable,);
-      }
-    }
-    return map;
-  }
-
-  /// The list of required keys that must be present in a JSON.
-  static const requiredKeys = <String>{
-    'albumId',
-  };
-}
-

+ 63 - 75
mobile/openapi/lib/model/create_assets_share_link_dto.dart → mobile/openapi/lib/model/shared_link_create_dto.dart

@@ -10,17 +10,21 @@
 
 part of openapi.api;
 
-class CreateAssetsShareLinkDto {
-  /// Returns a new [CreateAssetsShareLinkDto] instance.
-  CreateAssetsShareLinkDto({
+class SharedLinkCreateDto {
+  /// Returns a new [SharedLinkCreateDto] instance.
+  SharedLinkCreateDto({
+    required this.type,
     this.assetIds = const [],
-    this.expiresAt,
-    this.allowUpload,
-    this.allowDownload,
-    this.showExif,
+    this.albumId,
     this.description,
+    this.expiresAt,
+    this.allowUpload = false,
+    this.allowDownload = true,
+    this.showExif = true,
   });
 
+  SharedLinkType type;
+
   List<String> assetIds;
 
   ///
@@ -29,7 +33,7 @@ class CreateAssetsShareLinkDto {
   /// source code must fall back to having a nullable type.
   /// Consider adding a "default:" property in the specification file to hide this note.
   ///
-  DateTime? expiresAt;
+  String? albumId;
 
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -37,89 +41,71 @@ class CreateAssetsShareLinkDto {
   /// source code must fall back to having a nullable type.
   /// Consider adding a "default:" property in the specification file to hide this note.
   ///
-  bool? allowUpload;
+  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.
-  ///
-  bool? allowDownload;
+  DateTime? expiresAt;
 
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  bool? showExif;
+  bool allowUpload;
 
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  String? description;
+  bool allowDownload;
+
+  bool showExif;
 
   @override
-  bool operator ==(Object other) => identical(this, other) || other is CreateAssetsShareLinkDto &&
+  bool operator ==(Object other) => identical(this, other) || other is SharedLinkCreateDto &&
+     other.type == type &&
      other.assetIds == assetIds &&
+     other.albumId == albumId &&
+     other.description == description &&
      other.expiresAt == expiresAt &&
      other.allowUpload == allowUpload &&
      other.allowDownload == allowDownload &&
-     other.showExif == showExif &&
-     other.description == description;
+     other.showExif == showExif;
 
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
+    (type.hashCode) +
     (assetIds.hashCode) +
+    (albumId == null ? 0 : albumId!.hashCode) +
+    (description == null ? 0 : description!.hashCode) +
     (expiresAt == null ? 0 : expiresAt!.hashCode) +
-    (allowUpload == null ? 0 : allowUpload!.hashCode) +
-    (allowDownload == null ? 0 : allowDownload!.hashCode) +
-    (showExif == null ? 0 : showExif!.hashCode) +
-    (description == null ? 0 : description!.hashCode);
+    (allowUpload.hashCode) +
+    (allowDownload.hashCode) +
+    (showExif.hashCode);
 
   @override
-  String toString() => 'CreateAssetsShareLinkDto[assetIds=$assetIds, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, description=$description]';
+  String toString() => 'SharedLinkCreateDto[type=$type, assetIds=$assetIds, albumId=$albumId, description=$description, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
+      json[r'type'] = this.type;
       json[r'assetIds'] = this.assetIds;
+    if (this.albumId != null) {
+      json[r'albumId'] = this.albumId;
+    } else {
+      // json[r'albumId'] = null;
+    }
+    if (this.description != null) {
+      json[r'description'] = this.description;
+    } else {
+      // json[r'description'] = null;
+    }
     if (this.expiresAt != null) {
       json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String();
     } else {
       // json[r'expiresAt'] = null;
     }
-    if (this.allowUpload != null) {
       json[r'allowUpload'] = this.allowUpload;
-    } else {
-      // json[r'allowUpload'] = null;
-    }
-    if (this.allowDownload != null) {
       json[r'allowDownload'] = this.allowDownload;
-    } else {
-      // json[r'allowDownload'] = null;
-    }
-    if (this.showExif != null) {
       json[r'showExif'] = this.showExif;
-    } else {
-      // json[r'showExif'] = null;
-    }
-    if (this.description != null) {
-      json[r'description'] = this.description;
-    } else {
-      // json[r'description'] = null;
-    }
     return json;
   }
 
-  /// Returns a new [CreateAssetsShareLinkDto] instance and imports its values from
+  /// Returns a new [SharedLinkCreateDto] instance and imports its values from
   /// [value] if it's a [Map], null otherwise.
   // ignore: prefer_constructors_over_static_methods
-  static CreateAssetsShareLinkDto? fromJson(dynamic value) {
+  static SharedLinkCreateDto? fromJson(dynamic value) {
     if (value is Map) {
       final json = value.cast<String, dynamic>();
 
@@ -128,31 +114,33 @@ class CreateAssetsShareLinkDto {
       // Note 2: this code is stripped in release mode!
       assert(() {
         requiredKeys.forEach((key) {
-          assert(json.containsKey(key), 'Required key "CreateAssetsShareLinkDto[$key]" is missing from JSON.');
-          assert(json[key] != null, 'Required key "CreateAssetsShareLinkDto[$key]" has a null value in JSON.');
+          assert(json.containsKey(key), 'Required key "SharedLinkCreateDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "SharedLinkCreateDto[$key]" has a null value in JSON.');
         });
         return true;
       }());
 
-      return CreateAssetsShareLinkDto(
+      return SharedLinkCreateDto(
+        type: SharedLinkType.fromJson(json[r'type'])!,
         assetIds: json[r'assetIds'] is Iterable
             ? (json[r'assetIds'] as Iterable).cast<String>().toList(growable: false)
             : const [],
-        expiresAt: mapDateTime(json, r'expiresAt', ''),
-        allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
-        allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
-        showExif: mapValueOfType<bool>(json, r'showExif'),
+        albumId: mapValueOfType<String>(json, r'albumId'),
         description: mapValueOfType<String>(json, r'description'),
+        expiresAt: mapDateTime(json, r'expiresAt', ''),
+        allowUpload: mapValueOfType<bool>(json, r'allowUpload') ?? false,
+        allowDownload: mapValueOfType<bool>(json, r'allowDownload') ?? true,
+        showExif: mapValueOfType<bool>(json, r'showExif') ?? true,
       );
     }
     return null;
   }
 
-  static List<CreateAssetsShareLinkDto> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <CreateAssetsShareLinkDto>[];
+  static List<SharedLinkCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SharedLinkCreateDto>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
-        final value = CreateAssetsShareLinkDto.fromJson(row);
+        final value = SharedLinkCreateDto.fromJson(row);
         if (value != null) {
           result.add(value);
         }
@@ -161,12 +149,12 @@ class CreateAssetsShareLinkDto {
     return result.toList(growable: growable);
   }
 
-  static Map<String, CreateAssetsShareLinkDto> mapFromJson(dynamic json) {
-    final map = <String, CreateAssetsShareLinkDto>{};
+  static Map<String, SharedLinkCreateDto> mapFromJson(dynamic json) {
+    final map = <String, SharedLinkCreateDto>{};
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
-        final value = CreateAssetsShareLinkDto.fromJson(entry.value);
+        final value = SharedLinkCreateDto.fromJson(entry.value);
         if (value != null) {
           map[entry.key] = value;
         }
@@ -175,14 +163,14 @@ class CreateAssetsShareLinkDto {
     return map;
   }
 
-  // maps a json object with a list of CreateAssetsShareLinkDto-objects as value to a dart map
-  static Map<String, List<CreateAssetsShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<CreateAssetsShareLinkDto>>{};
+  // maps a json object with a list of SharedLinkCreateDto-objects as value to a dart map
+  static Map<String, List<SharedLinkCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SharedLinkCreateDto>>{};
     if (json is Map && json.isNotEmpty) {
       // ignore: parameter_assignments
       json = json.cast<String, dynamic>();
       for (final entry in json.entries) {
-        map[entry.key] = CreateAssetsShareLinkDto.listFromJson(entry.value, growable: growable,);
+        map[entry.key] = SharedLinkCreateDto.listFromJson(entry.value, growable: growable,);
       }
     }
     return map;
@@ -190,7 +178,7 @@ class CreateAssetsShareLinkDto {
 
   /// The list of required keys that must be present in a JSON.
   static const requiredKeys = <String>{
-    'assetIds',
+    'type',
   };
 }
 

+ 20 - 20
mobile/openapi/lib/model/edit_shared_link_dto.dart → mobile/openapi/lib/model/shared_link_edit_dto.dart

@@ -10,9 +10,9 @@
 
 part of openapi.api;
 
-class EditSharedLinkDto {
-  /// Returns a new [EditSharedLinkDto] instance.
-  EditSharedLinkDto({
+class SharedLinkEditDto {
+  /// Returns a new [SharedLinkEditDto] instance.
+  SharedLinkEditDto({
     this.description,
     this.expiresAt,
     this.allowUpload,
@@ -55,7 +55,7 @@ class EditSharedLinkDto {
   bool? showExif;
 
   @override
-  bool operator ==(Object other) => identical(this, other) || other is EditSharedLinkDto &&
+  bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
      other.description == description &&
      other.expiresAt == expiresAt &&
      other.allowUpload == allowUpload &&
@@ -72,7 +72,7 @@ class EditSharedLinkDto {
     (showExif == null ? 0 : showExif!.hashCode);
 
   @override
-  String toString() => 'EditSharedLinkDto[description=$description, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]';
+  String toString() => 'SharedLinkEditDto[description=$description, expiresAt=$expiresAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -104,10 +104,10 @@ class EditSharedLinkDto {
     return json;
   }
 
-  /// Returns a new [EditSharedLinkDto] instance and imports its values from
+  /// Returns a new [SharedLinkEditDto] instance and imports its values from
   /// [value] if it's a [Map], null otherwise.
   // ignore: prefer_constructors_over_static_methods
-  static EditSharedLinkDto? fromJson(dynamic value) {
+  static SharedLinkEditDto? fromJson(dynamic value) {
     if (value is Map) {
       final json = value.cast<String, dynamic>();
 
@@ -116,13 +116,13 @@ class EditSharedLinkDto {
       // Note 2: this code is stripped in release mode!
       assert(() {
         requiredKeys.forEach((key) {
-          assert(json.containsKey(key), 'Required key "EditSharedLinkDto[$key]" is missing from JSON.');
-          assert(json[key] != null, 'Required key "EditSharedLinkDto[$key]" has a null value in JSON.');
+          assert(json.containsKey(key), 'Required key "SharedLinkEditDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "SharedLinkEditDto[$key]" has a null value in JSON.');
         });
         return true;
       }());
 
-      return EditSharedLinkDto(
+      return SharedLinkEditDto(
         description: mapValueOfType<String>(json, r'description'),
         expiresAt: mapDateTime(json, r'expiresAt', ''),
         allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
@@ -133,11 +133,11 @@ class EditSharedLinkDto {
     return null;
   }
 
-  static List<EditSharedLinkDto> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <EditSharedLinkDto>[];
+  static List<SharedLinkEditDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SharedLinkEditDto>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
-        final value = EditSharedLinkDto.fromJson(row);
+        final value = SharedLinkEditDto.fromJson(row);
         if (value != null) {
           result.add(value);
         }
@@ -146,12 +146,12 @@ class EditSharedLinkDto {
     return result.toList(growable: growable);
   }
 
-  static Map<String, EditSharedLinkDto> mapFromJson(dynamic json) {
-    final map = <String, EditSharedLinkDto>{};
+  static Map<String, SharedLinkEditDto> mapFromJson(dynamic json) {
+    final map = <String, SharedLinkEditDto>{};
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
-        final value = EditSharedLinkDto.fromJson(entry.value);
+        final value = SharedLinkEditDto.fromJson(entry.value);
         if (value != null) {
           map[entry.key] = value;
         }
@@ -160,14 +160,14 @@ class EditSharedLinkDto {
     return map;
   }
 
-  // maps a json object with a list of EditSharedLinkDto-objects as value to a dart map
-  static Map<String, List<EditSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<EditSharedLinkDto>>{};
+  // maps a json object with a list of SharedLinkEditDto-objects as value to a dart map
+  static Map<String, List<SharedLinkEditDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SharedLinkEditDto>>{};
     if (json is Map && json.isNotEmpty) {
       // ignore: parameter_assignments
       json = json.cast<String, dynamic>();
       for (final entry in json.entries) {
-        map[entry.key] = EditSharedLinkDto.listFromJson(entry.value, growable: growable,);
+        map[entry.key] = SharedLinkEditDto.listFromJson(entry.value, growable: growable,);
       }
     }
     return map;

+ 2 - 7
mobile/openapi/lib/model/shared_link_response_dto.dart

@@ -15,7 +15,7 @@ class SharedLinkResponseDto {
   SharedLinkResponseDto({
     required this.type,
     required this.id,
-    this.description,
+    required this.description,
     required this.userId,
     required this.key,
     required this.createdAt,
@@ -31,12 +31,6 @@ class SharedLinkResponseDto {
 
   String id;
 
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
   String? description;
 
   String userId;
@@ -206,6 +200,7 @@ class SharedLinkResponseDto {
   static const requiredKeys = <String>{
     'type',
     'id',
+    'description',
     'userId',
     'key',
     'createdAt',

+ 0 - 5
mobile/openapi/test/album_api_test.dart

@@ -32,11 +32,6 @@ void main() {
       // TODO
     });
 
-    //Future<SharedLinkResponseDto> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto) async
-    test('test createAlbumSharedLink', () async {
-      // TODO
-    });
-
     //Future deleteAlbum(String id) async
     test('test deleteAlbum', () async {
       // TODO

+ 0 - 15
mobile/openapi/test/asset_api_test.dart

@@ -17,11 +17,6 @@ void main() {
   // final instance = AssetApi();
 
   group('tests for AssetApi', () {
-    //Future<SharedLinkResponseDto> addAssetsToSharedLink(AddAssetsDto addAssetsDto, { String key }) async
-    test('test addAssetsToSharedLink', () async {
-      // TODO
-    });
-
     // Checks if assets exist by checksums
     //
     //Future<AssetBulkUploadCheckResponseDto> bulkUploadCheck(AssetBulkUploadCheckDto assetBulkUploadCheckDto) async
@@ -43,11 +38,6 @@ void main() {
       // TODO
     });
 
-    //Future<SharedLinkResponseDto> createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto) async
-    test('test createAssetsSharedLink', () async {
-      // TODO
-    });
-
     //Future<List<DeleteAssetResponseDto>> deleteAsset(DeleteAssetDto deleteAssetDto) async
     test('test deleteAsset', () async {
       // TODO
@@ -141,11 +131,6 @@ void main() {
       // TODO
     });
 
-    //Future<SharedLinkResponseDto> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto, { String key }) async
-    test('test removeAssetsFromSharedLink', () async {
-      // TODO
-    });
-
     //Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async
     test('test searchAsset', () async {
       // TODO

+ 0 - 52
mobile/openapi/test/create_album_share_link_dto_test.dart

@@ -1,52 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// 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
-
-import 'package:openapi/api.dart';
-import 'package:test/test.dart';
-
-// tests for CreateAlbumShareLinkDto
-void main() {
-  // final instance = CreateAlbumShareLinkDto();
-
-  group('test CreateAlbumShareLinkDto', () {
-    // String albumId
-    test('to test the property `albumId`', () async {
-      // TODO
-    });
-
-    // DateTime expiresAt
-    test('to test the property `expiresAt`', () async {
-      // TODO
-    });
-
-    // bool allowUpload
-    test('to test the property `allowUpload`', () async {
-      // TODO
-    });
-
-    // bool allowDownload
-    test('to test the property `allowDownload`', () async {
-      // TODO
-    });
-
-    // bool showExif
-    test('to test the property `showExif`', () async {
-      // TODO
-    });
-
-    // String description
-    test('to test the property `description`', () async {
-      // TODO
-    });
-
-
-  });
-
-}

+ 19 - 4
mobile/openapi/test/share_api_test.dart → mobile/openapi/test/shared_link_api_test.dart

@@ -12,11 +12,21 @@ import 'package:openapi/api.dart';
 import 'package:test/test.dart';
 
 
-/// tests for ShareApi
+/// tests for SharedLinkApi
 void main() {
-  // final instance = ShareApi();
+  // final instance = SharedLinkApi();
+
+  group('tests for SharedLinkApi', () {
+    //Future<List<AssetIdsResponseDto>> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String key }) async
+    test('test addSharedLinkAssets', () async {
+      // TODO
+    });
+
+    //Future<SharedLinkResponseDto> createSharedLink(SharedLinkCreateDto sharedLinkCreateDto) async
+    test('test createSharedLink', () async {
+      // TODO
+    });
 
-  group('tests for ShareApi', () {
     //Future<List<SharedLinkResponseDto>> getAllSharedLinks() async
     test('test getAllSharedLinks', () async {
       // TODO
@@ -37,7 +47,12 @@ void main() {
       // TODO
     });
 
-    //Future<SharedLinkResponseDto> updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto) async
+    //Future<List<AssetIdsResponseDto>> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String key }) async
+    test('test removeSharedLinkAssets', () async {
+      // TODO
+    });
+
+    //Future<SharedLinkResponseDto> updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto) async
     test('test updateSharedLink', () async {
       // TODO
     });

+ 21 - 11
mobile/openapi/test/create_assets_share_link_dto_test.dart → mobile/openapi/test/shared_link_create_dto_test.dart

@@ -11,41 +11,51 @@
 import 'package:openapi/api.dart';
 import 'package:test/test.dart';
 
-// tests for CreateAssetsShareLinkDto
+// tests for SharedLinkCreateDto
 void main() {
-  // final instance = CreateAssetsShareLinkDto();
+  // final instance = SharedLinkCreateDto();
+
+  group('test SharedLinkCreateDto', () {
+    // SharedLinkType type
+    test('to test the property `type`', () async {
+      // TODO
+    });
 
-  group('test CreateAssetsShareLinkDto', () {
     // List<String> assetIds (default value: const [])
     test('to test the property `assetIds`', () async {
       // TODO
     });
 
+    // String albumId
+    test('to test the property `albumId`', () async {
+      // TODO
+    });
+
+    // String description
+    test('to test the property `description`', () async {
+      // TODO
+    });
+
     // DateTime expiresAt
     test('to test the property `expiresAt`', () async {
       // TODO
     });
 
-    // bool allowUpload
+    // bool allowUpload (default value: false)
     test('to test the property `allowUpload`', () async {
       // TODO
     });
 
-    // bool allowDownload
+    // bool allowDownload (default value: true)
     test('to test the property `allowDownload`', () async {
       // TODO
     });
 
-    // bool showExif
+    // bool showExif (default value: true)
     test('to test the property `showExif`', () async {
       // TODO
     });
 
-    // String description
-    test('to test the property `description`', () async {
-      // TODO
-    });
-
 
   });
 

+ 3 - 3
mobile/openapi/test/edit_shared_link_dto_test.dart → mobile/openapi/test/shared_link_edit_dto_test.dart

@@ -11,11 +11,11 @@
 import 'package:openapi/api.dart';
 import 'package:test/test.dart';
 
-// tests for EditSharedLinkDto
+// tests for SharedLinkEditDto
 void main() {
-  // final instance = EditSharedLinkDto();
+  // final instance = SharedLinkEditDto();
 
-  group('test EditSharedLinkDto', () {
+  group('test SharedLinkEditDto', () {
     // String description
     test('to test the property `description`', () async {
       // TODO

+ 13 - 4
server/e2e/album.e2e-spec.ts

@@ -1,7 +1,14 @@
-import { AlbumResponseDto, AuthService, CreateAlbumDto, SharedLinkResponseDto, UserService } from '@app/domain';
-import { CreateAlbumShareLinkDto } from '@app/immich/api-v1/album/dto/create-album-shared-link.dto';
+import {
+  AlbumResponseDto,
+  AuthService,
+  CreateAlbumDto,
+  SharedLinkCreateDto,
+  SharedLinkResponseDto,
+  UserService,
+} from '@app/domain';
 import { AppModule } from '@app/immich/app.module';
 import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator';
+import { SharedLinkType } from '@app/infra/entities';
 import { INestApplication } from '@nestjs/common';
 import { Test, TestingModule } from '@nestjs/testing';
 import request from 'supertest';
@@ -14,8 +21,10 @@ async function _createAlbum(app: INestApplication, data: CreateAlbumDto) {
   return res.body as AlbumResponseDto;
 }
 
-async function _createAlbumSharedLink(app: INestApplication, data: CreateAlbumShareLinkDto) {
-  const res = await request(app.getHttpServer()).post('/album/create-shared-link').send(data);
+async function _createAlbumSharedLink(app: INestApplication, data: Omit<SharedLinkCreateDto, 'type'>) {
+  const res = await request(app.getHttpServer())
+    .post('/shared-link')
+    .send({ ...data, type: SharedLinkType.ALBUM });
   expect(res.status).toEqual(201);
   return res.body as SharedLinkResponseDto;
 }

+ 241 - 282
server/immich-openapi-specs.json

@@ -127,48 +127,6 @@
         ]
       }
     },
-    "/album/create-shared-link": {
-      "post": {
-        "operationId": "createAlbumSharedLink",
-        "parameters": [],
-        "requestBody": {
-          "required": true,
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/CreateAlbumShareLinkDto"
-              }
-            }
-          }
-        },
-        "responses": {
-          "201": {
-            "description": "",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/SharedLinkResponseDto"
-                }
-              }
-            }
-          }
-        },
-        "tags": [
-          "Album"
-        ],
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ]
-      }
-    },
     "/album/{id}": {
       "patch": {
         "operationId": "updateAlbumInfo",
@@ -1660,150 +1618,6 @@
         ]
       }
     },
-    "/asset/shared-link": {
-      "post": {
-        "operationId": "createAssetsSharedLink",
-        "parameters": [],
-        "requestBody": {
-          "required": true,
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/CreateAssetsShareLinkDto"
-              }
-            }
-          }
-        },
-        "responses": {
-          "201": {
-            "description": "",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/SharedLinkResponseDto"
-                }
-              }
-            }
-          }
-        },
-        "tags": [
-          "Asset"
-        ],
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ]
-      }
-    },
-    "/asset/shared-link/add": {
-      "patch": {
-        "operationId": "addAssetsToSharedLink",
-        "parameters": [
-          {
-            "name": "key",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "type": "string"
-            }
-          }
-        ],
-        "requestBody": {
-          "required": true,
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/AddAssetsDto"
-              }
-            }
-          }
-        },
-        "responses": {
-          "200": {
-            "description": "",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/SharedLinkResponseDto"
-                }
-              }
-            }
-          }
-        },
-        "tags": [
-          "Asset"
-        ],
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ]
-      }
-    },
-    "/asset/shared-link/remove": {
-      "patch": {
-        "operationId": "removeAssetsFromSharedLink",
-        "parameters": [
-          {
-            "name": "key",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "type": "string"
-            }
-          }
-        ],
-        "requestBody": {
-          "required": true,
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/RemoveAssetsDto"
-              }
-            }
-          }
-        },
-        "responses": {
-          "200": {
-            "description": "",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/SharedLinkResponseDto"
-                }
-              }
-            }
-          }
-        },
-        "tags": [
-          "Asset"
-        ],
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ]
-      }
-    },
     "/asset/stat/archive": {
       "get": {
         "operationId": "getArchivedAssetCountByUserId",
@@ -3264,7 +3078,7 @@
         ]
       }
     },
-    "/share": {
+    "/shared-link": {
       "get": {
         "operationId": "getAllSharedLinks",
         "parameters": [],
@@ -3284,7 +3098,47 @@
           }
         },
         "tags": [
-          "share"
+          "Shared Link"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ]
+      },
+      "post": {
+        "operationId": "createSharedLink",
+        "parameters": [],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/SharedLinkCreateDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SharedLinkResponseDto"
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Shared Link"
         ],
         "security": [
           {
@@ -3299,7 +3153,7 @@
         ]
       }
     },
-    "/share/me": {
+    "/shared-link/me": {
       "get": {
         "operationId": "getMySharedLink",
         "parameters": [
@@ -3325,7 +3179,7 @@
           }
         },
         "tags": [
-          "share"
+          "Shared Link"
         ],
         "security": [
           {
@@ -3340,7 +3194,7 @@
         ]
       }
     },
-    "/share/{id}": {
+    "/shared-link/{id}": {
       "get": {
         "operationId": "getSharedLinkById",
         "parameters": [
@@ -3367,7 +3221,7 @@
           }
         },
         "tags": [
-          "share"
+          "Shared Link"
         ],
         "security": [
           {
@@ -3399,7 +3253,7 @@
           "content": {
             "application/json": {
               "schema": {
-                "$ref": "#/components/schemas/EditSharedLinkDto"
+                "$ref": "#/components/schemas/SharedLinkEditDto"
               }
             }
           }
@@ -3417,7 +3271,7 @@
           }
         },
         "tags": [
-          "share"
+          "Shared Link"
         ],
         "security": [
           {
@@ -3450,7 +3304,131 @@
           }
         },
         "tags": [
-          "share"
+          "Shared Link"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ]
+      }
+    },
+    "/shared-link/{id}/assets": {
+      "put": {
+        "operationId": "addSharedLinkAssets",
+        "parameters": [
+          {
+            "name": "id",
+            "required": true,
+            "in": "path",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "key",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/AssetIdsDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/AssetIdsResponseDto"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Shared Link"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ]
+      },
+      "delete": {
+        "operationId": "removeSharedLinkAssets",
+        "parameters": [
+          {
+            "name": "id",
+            "required": true,
+            "in": "path",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "key",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/AssetIdsDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/AssetIdsResponseDto"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Shared Link"
         ],
         "security": [
           {
@@ -5089,34 +5067,6 @@
           "albumName"
         ]
       },
-      "CreateAlbumShareLinkDto": {
-        "type": "object",
-        "properties": {
-          "albumId": {
-            "type": "string",
-            "format": "uuid"
-          },
-          "expiresAt": {
-            "format": "date-time",
-            "type": "string"
-          },
-          "allowUpload": {
-            "type": "boolean"
-          },
-          "allowDownload": {
-            "type": "boolean"
-          },
-          "showExif": {
-            "type": "boolean"
-          },
-          "description": {
-            "type": "string"
-          }
-        },
-        "required": [
-          "albumId"
-        ]
-      },
       "CreateAssetDto": {
         "type": "object",
         "properties": {
@@ -5176,42 +5126,6 @@
           "fileExtension"
         ]
       },
-      "CreateAssetsShareLinkDto": {
-        "type": "object",
-        "properties": {
-          "assetIds": {
-            "title": "Array asset IDs to be shared",
-            "example": [
-              "bf973405-3f2a-48d2-a687-2ed4167164be",
-              "dd41870b-5d00-46d2-924e-1d8489a0aa0f",
-              "fad77c3f-deef-4e7e-9608-14c1aa4e559a"
-            ],
-            "type": "array",
-            "items": {
-              "type": "string"
-            }
-          },
-          "expiresAt": {
-            "format": "date-time",
-            "type": "string"
-          },
-          "allowUpload": {
-            "type": "boolean"
-          },
-          "allowDownload": {
-            "type": "boolean"
-          },
-          "showExif": {
-            "type": "boolean"
-          },
-          "description": {
-            "type": "string"
-          }
-        },
-        "required": [
-          "assetIds"
-        ]
-      },
       "CreateProfileImageDto": {
         "type": "object",
         "properties": {
@@ -5392,28 +5306,6 @@
           "assetIds"
         ]
       },
-      "EditSharedLinkDto": {
-        "type": "object",
-        "properties": {
-          "description": {
-            "type": "string"
-          },
-          "expiresAt": {
-            "format": "date-time",
-            "type": "string",
-            "nullable": true
-          },
-          "allowUpload": {
-            "type": "boolean"
-          },
-          "allowDownload": {
-            "type": "boolean"
-          },
-          "showExif": {
-            "type": "boolean"
-          }
-        }
-      },
       "ExifResponseDto": {
         "type": "object",
         "properties": {
@@ -6160,6 +6052,71 @@
           "patch"
         ]
       },
+      "SharedLinkCreateDto": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "$ref": "#/components/schemas/SharedLinkType"
+          },
+          "assetIds": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "format": "uuid"
+            }
+          },
+          "albumId": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "description": {
+            "type": "string"
+          },
+          "expiresAt": {
+            "format": "date-time",
+            "type": "string",
+            "nullable": true,
+            "default": null
+          },
+          "allowUpload": {
+            "type": "boolean",
+            "default": false
+          },
+          "allowDownload": {
+            "type": "boolean",
+            "default": true
+          },
+          "showExif": {
+            "type": "boolean",
+            "default": true
+          }
+        },
+        "required": [
+          "type"
+        ]
+      },
+      "SharedLinkEditDto": {
+        "type": "object",
+        "properties": {
+          "description": {
+            "type": "string"
+          },
+          "expiresAt": {
+            "format": "date-time",
+            "type": "string",
+            "nullable": true
+          },
+          "allowUpload": {
+            "type": "boolean"
+          },
+          "allowDownload": {
+            "type": "boolean"
+          },
+          "showExif": {
+            "type": "boolean"
+          }
+        }
+      },
       "SharedLinkResponseDto": {
         "type": "object",
         "properties": {
@@ -6170,7 +6127,8 @@
             "type": "string"
           },
           "description": {
-            "type": "string"
+            "type": "string",
+            "nullable": true
           },
           "userId": {
             "type": "string"
@@ -6209,6 +6167,7 @@
         "required": [
           "type",
           "id",
+          "description",
           "userId",
           "key",
           "createdAt",

+ 3 - 0
server/src/domain/access/access.repository.ts

@@ -2,8 +2,11 @@ export const IAccessRepository = 'IAccessRepository';
 
 export interface IAccessRepository {
   hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
+
   hasAlbumAssetAccess(userId: string, assetId: string): Promise<boolean>;
   hasOwnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
   hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
   hasSharedLinkAssetAccess(userId: string, assetId: string): Promise<boolean>;
+
+  hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean>;
 }

+ 28 - 5
server/src/domain/auth/auth.service.ts

@@ -13,7 +13,7 @@ import { IKeyRepository } from '../api-key';
 import { APIKeyCore } from '../api-key/api-key.core';
 import { ICryptoRepository } from '../crypto/crypto.repository';
 import { OAuthCore } from '../oauth/oauth.core';
-import { ISharedLinkRepository, SharedLinkCore } from '../shared-link';
+import { ISharedLinkRepository } from '../shared-link';
 import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
 import { IUserRepository, UserCore } from '../user';
 import { IUserTokenRepository, UserTokenCore } from '../user-token';
@@ -35,7 +35,6 @@ export class AuthService {
   private authCore: AuthCore;
   private oauthCore: OAuthCore;
   private userCore: UserCore;
-  private shareCore: SharedLinkCore;
   private keyCore: APIKeyCore;
 
   private logger = new Logger(AuthService.name);
@@ -45,7 +44,7 @@ export class AuthService {
     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
     @Inject(IUserRepository) userRepository: IUserRepository,
     @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
-    @Inject(ISharedLinkRepository) shareRepository: ISharedLinkRepository,
+    @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
     @Inject(IKeyRepository) keyRepository: IKeyRepository,
     @Inject(INITIAL_SYSTEM_CONFIG)
     initialConfig: SystemConfig,
@@ -54,7 +53,6 @@ export class AuthService {
     this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
     this.oauthCore = new OAuthCore(configRepository, initialConfig);
     this.userCore = new UserCore(userRepository, cryptoRepository);
-    this.shareCore = new SharedLinkCore(shareRepository, cryptoRepository);
     this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
   }
 
@@ -147,7 +145,7 @@ export class AuthService {
     const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;
 
     if (shareKey) {
-      return this.shareCore.validate(shareKey);
+      return this.validateSharedLink(shareKey);
     }
 
     if (userToken) {
@@ -193,4 +191,29 @@ export class AuthService {
     const cookies = cookieParser.parse(headers.cookie || '');
     return cookies[IMMICH_ACCESS_COOKIE] || null;
   }
+
+  async validateSharedLink(key: string | string[]): Promise<AuthUserDto | null> {
+    key = Array.isArray(key) ? key[0] : key;
+
+    const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
+    const link = await this.sharedLinkRepository.getByKey(bytes);
+    if (link) {
+      if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
+        const user = link.user;
+        if (user) {
+          return {
+            id: user.id,
+            email: user.email,
+            isAdmin: user.isAdmin,
+            isPublicUser: true,
+            sharedLinkId: link.id,
+            isAllowUpload: link.allowUpload,
+            isAllowDownload: link.allowDownload,
+            isShowExif: link.showExif,
+          };
+        }
+      }
+    }
+    throw new UnauthorizedException('Invalid share key');
+  }
 }

+ 0 - 12
server/src/domain/shared-link/dto/create-shared-link.dto.ts

@@ -1,12 +0,0 @@
-import { AlbumEntity, AssetEntity, SharedLinkType } from '@app/infra/entities';
-
-export class CreateSharedLinkDto {
-  description?: string;
-  expiresAt?: Date;
-  type!: SharedLinkType;
-  assets!: AssetEntity[];
-  album?: AlbumEntity;
-  allowUpload?: boolean;
-  allowDownload?: boolean;
-  showExif?: boolean;
-}

+ 0 - 18
server/src/domain/shared-link/dto/edit-shared-link.dto.ts

@@ -1,18 +0,0 @@
-import { IsOptional } from 'class-validator';
-
-export class EditSharedLinkDto {
-  @IsOptional()
-  description?: string;
-
-  @IsOptional()
-  expiresAt?: Date | null;
-
-  @IsOptional()
-  allowUpload?: boolean;
-
-  @IsOptional()
-  allowDownload?: boolean;
-
-  @IsOptional()
-  showExif?: boolean;
-}

+ 0 - 2
server/src/domain/shared-link/dto/index.ts

@@ -1,2 +0,0 @@
-export * from './create-shared-link.dto';
-export * from './edit-shared-link.dto';

+ 2 - 3
server/src/domain/shared-link/index.ts

@@ -1,5 +1,4 @@
-export * from './dto';
-export * from './response-dto';
-export * from './shared-link.core';
+export * from './shared-link-response.dto';
+export * from './shared-link.dto';
 export * from './shared-link.repository';
 export * from './shared-link.service';

+ 0 - 1
server/src/domain/shared-link/response-dto/index.ts

@@ -1 +0,0 @@
-export * from './shared-link-response.dto';

+ 3 - 3
server/src/domain/shared-link/response-dto/shared-link-response.dto.ts → server/src/domain/shared-link/shared-link-response.dto.ts

@@ -1,12 +1,12 @@
 import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities';
 import { ApiProperty } from '@nestjs/swagger';
 import _ from 'lodash';
-import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album';
-import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset';
+import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../album';
+import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../asset';
 
 export class SharedLinkResponseDto {
   id!: string;
-  description?: string;
+  description!: string | null;
   userId!: string;
   key!: string;
 

+ 0 - 80
server/src/domain/shared-link/shared-link.core.ts

@@ -1,80 +0,0 @@
-import { AssetEntity, SharedLinkEntity } from '@app/infra/entities';
-import { BadRequestException, ForbiddenException, Logger, UnauthorizedException } from '@nestjs/common';
-import { AuthUserDto } from '../auth';
-import { ICryptoRepository } from '../crypto';
-import { CreateSharedLinkDto } from './dto';
-import { ISharedLinkRepository } from './shared-link.repository';
-
-export class SharedLinkCore {
-  readonly logger = new Logger(SharedLinkCore.name);
-
-  constructor(private repository: ISharedLinkRepository, private cryptoRepository: ICryptoRepository) {}
-
-  // TODO: move to SharedLinkController/SharedLinkService
-  create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
-    return this.repository.create({
-      key: Buffer.from(this.cryptoRepository.randomBytes(50)),
-      description: dto.description,
-      userId,
-      createdAt: new Date(),
-      expiresAt: dto.expiresAt ?? null,
-      type: dto.type,
-      assets: dto.assets,
-      album: dto.album,
-      allowUpload: dto.allowUpload ?? false,
-      allowDownload: dto.allowDownload ?? true,
-      showExif: dto.showExif ?? true,
-    });
-  }
-
-  async addAssets(userId: string, id: string, assets: AssetEntity[]) {
-    const link = await this.repository.get(userId, id);
-    if (!link) {
-      throw new BadRequestException('Shared link not found');
-    }
-
-    return this.repository.update({ ...link, assets: [...link.assets, ...assets] });
-  }
-
-  async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
-    const link = await this.repository.get(userId, id);
-    if (!link) {
-      throw new BadRequestException('Shared link not found');
-    }
-
-    const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
-
-    return this.repository.update({ ...link, assets: newAssets });
-  }
-
-  checkDownloadAccess(user: AuthUserDto) {
-    if (user.isPublicUser && !user.isAllowDownload) {
-      throw new ForbiddenException();
-    }
-  }
-
-  async validate(key: string | string[]): Promise<AuthUserDto | null> {
-    key = Array.isArray(key) ? key[0] : key;
-
-    const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
-    const link = await this.repository.getByKey(bytes);
-    if (link) {
-      if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
-        const user = link.user;
-        if (user) {
-          return {
-            id: user.id,
-            email: user.email,
-            isAdmin: user.isAdmin,
-            isPublicUser: true,
-            sharedLinkId: link.id,
-            isAllowUpload: link.allowUpload,
-            isAllowDownload: link.allowDownload,
-            isShowExif: link.showExif,
-          };
-        }
-      }
-    }
-    throw new UnauthorizedException('Invalid share key');
-  }
-}

+ 53 - 0
server/src/domain/shared-link/shared-link.dto.ts

@@ -0,0 +1,53 @@
+import { SharedLinkType } from '@app/infra/entities';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator';
+import { ValidateUUID } from '../../immich/decorators/validate-uuid.decorator';
+
+export class SharedLinkCreateDto {
+  @IsEnum(SharedLinkType)
+  @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })
+  type!: SharedLinkType;
+
+  @ValidateUUID({ each: true, optional: true })
+  assetIds?: string[];
+
+  @ValidateUUID({ optional: true })
+  albumId?: string;
+
+  @IsString()
+  @IsOptional()
+  description?: string;
+
+  @IsDate()
+  @IsOptional()
+  expiresAt?: Date | null = null;
+
+  @IsOptional()
+  @IsBoolean()
+  allowUpload?: boolean = false;
+
+  @IsOptional()
+  @IsBoolean()
+  allowDownload?: boolean = true;
+
+  @IsOptional()
+  @IsBoolean()
+  showExif?: boolean = true;
+}
+
+export class SharedLinkEditDto {
+  @IsOptional()
+  description?: string;
+
+  @IsOptional()
+  expiresAt?: Date | null;
+
+  @IsOptional()
+  allowUpload?: boolean;
+
+  @IsOptional()
+  allowDownload?: boolean;
+
+  @IsOptional()
+  showExif?: boolean;
+}

+ 1 - 1
server/src/domain/shared-link/shared-link.repository.ts

@@ -6,7 +6,7 @@ export interface ISharedLinkRepository {
   getAll(userId: string): Promise<SharedLinkEntity[]>;
   get(userId: string, id: string): Promise<SharedLinkEntity | null>;
   getByKey(key: Buffer): Promise<SharedLinkEntity | null>;
-  create(entity: Omit<SharedLinkEntity, 'id' | 'user'>): Promise<SharedLinkEntity>;
+  create(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
   update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
   remove(entity: SharedLinkEntity): Promise<void>;
 }

+ 149 - 2
server/src/domain/shared-link/shared-link.service.spec.ts

@@ -1,16 +1,33 @@
 import { BadRequestException, ForbiddenException } from '@nestjs/common';
-import { authStub, newSharedLinkRepositoryMock, sharedLinkResponseStub, sharedLinkStub } from '@test';
+import {
+  albumStub,
+  assetEntityStub,
+  authStub,
+  newAccessRepositoryMock,
+  newCryptoRepositoryMock,
+  newSharedLinkRepositoryMock,
+  sharedLinkResponseStub,
+  sharedLinkStub,
+} from '@test';
+import { when } from 'jest-when';
+import _ from 'lodash';
+import { SharedLinkType } from '../../infra/entities/shared-link.entity';
+import { AssetIdErrorReason, IAccessRepository, ICryptoRepository } from '../index';
 import { ISharedLinkRepository } from './shared-link.repository';
 import { SharedLinkService } from './shared-link.service';
 
 describe(SharedLinkService.name, () => {
   let sut: SharedLinkService;
+  let accessMock: jest.Mocked<IAccessRepository>;
+  let cryptoMock: jest.Mocked<ICryptoRepository>;
   let shareMock: jest.Mocked<ISharedLinkRepository>;
 
   beforeEach(async () => {
+    accessMock = newAccessRepositoryMock();
+    cryptoMock = newCryptoRepositoryMock();
     shareMock = newSharedLinkRepositoryMock();
 
-    sut = new SharedLinkService(shareMock);
+    sut = new SharedLinkService(accessMock, cryptoMock, shareMock);
   });
 
   it('should work', () => {
@@ -64,6 +81,82 @@ describe(SharedLinkService.name, () => {
     });
   });
 
+  describe('create', () => {
+    it('should not allow an album shared link without an albumId', async () => {
+      await expect(sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [] })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+    });
+
+    it('should not allow non-owners to create album shared links', async () => {
+      accessMock.hasAlbumOwnerAccess.mockResolvedValue(false);
+      await expect(
+        sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [], albumId: 'album-1' }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+    });
+
+    it('should not allow individual shared links with no assets', async () => {
+      await expect(
+        sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: [] }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+    });
+
+    it('should require asset ownership to make an individual shared link', async () => {
+      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
+      await expect(
+        sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: ['asset-1'] }),
+      ).rejects.toBeInstanceOf(BadRequestException);
+    });
+
+    it('should create an album shared link', async () => {
+      accessMock.hasAlbumOwnerAccess.mockResolvedValue(true);
+      shareMock.create.mockResolvedValue(sharedLinkStub.valid);
+
+      await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
+
+      expect(accessMock.hasAlbumOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id);
+      expect(shareMock.create).toHaveBeenCalledWith({
+        type: SharedLinkType.ALBUM,
+        userId: authStub.admin.id,
+        albumId: albumStub.oneAsset.id,
+        allowDownload: true,
+        allowUpload: true,
+        assets: [],
+        description: null,
+        expiresAt: null,
+        showExif: true,
+        key: Buffer.from('random-bytes', 'utf8'),
+      });
+    });
+
+    it('should create an individual shared link', async () => {
+      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
+      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+
+      await sut.create(authStub.admin, {
+        type: SharedLinkType.INDIVIDUAL,
+        assetIds: [assetEntityStub.image.id],
+        showExif: true,
+        allowDownload: true,
+        allowUpload: true,
+      });
+
+      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
+      expect(shareMock.create).toHaveBeenCalledWith({
+        type: SharedLinkType.INDIVIDUAL,
+        userId: authStub.admin.id,
+        albumId: null,
+        allowDownload: true,
+        allowUpload: true,
+        assets: [{ id: assetEntityStub.image.id }],
+        description: null,
+        expiresAt: null,
+        showExif: true,
+        key: Buffer.from('random-bytes', 'utf8'),
+      });
+    });
+  });
+
   describe('update', () => {
     it('should throw an error for an invalid shared link', async () => {
       shareMock.get.mockResolvedValue(null);
@@ -100,4 +193,58 @@ describe(SharedLinkService.name, () => {
       expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
     });
   });
+
+  describe('addAssets', () => {
+    it('should not work on album shared links', async () => {
+      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+    });
+
+    it('should add assets to a shared link', async () => {
+      shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
+      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+
+      when(accessMock.hasOwnerAssetAccess).calledWith(authStub.admin.id, 'asset-2').mockResolvedValue(false);
+      when(accessMock.hasOwnerAssetAccess).calledWith(authStub.admin.id, 'asset-3').mockResolvedValue(true);
+
+      await expect(
+        sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetEntityStub.image.id, 'asset-2', 'asset-3'] }),
+      ).resolves.toEqual([
+        { assetId: assetEntityStub.image.id, success: false, error: AssetIdErrorReason.DUPLICATE },
+        { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NO_PERMISSION },
+        { assetId: 'asset-3', success: true },
+      ]);
+
+      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledTimes(2);
+      expect(shareMock.update).toHaveBeenCalledWith({
+        ...sharedLinkStub.individual,
+        assets: [assetEntityStub.image, { id: 'asset-3' }],
+      });
+    });
+  });
+
+  describe('removeAssets', () => {
+    it('should not work on album shared links', async () => {
+      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
+        BadRequestException,
+      );
+    });
+
+    it('should remove assets from a shared link', async () => {
+      shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
+      shareMock.create.mockResolvedValue(sharedLinkStub.individual);
+
+      await expect(
+        sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetEntityStub.image.id, 'asset-2'] }),
+      ).resolves.toEqual([
+        { assetId: assetEntityStub.image.id, success: true },
+        { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND },
+      ]);
+
+      expect(shareMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] });
+    });
+  });
 });

+ 112 - 6
server/src/domain/shared-link/shared-link.service.ts

@@ -1,15 +1,22 @@
-import { SharedLinkEntity } from '@app/infra/entities';
+import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entities';
 import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
+import { IAccessRepository } from '../access';
+import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
 import { AuthUserDto } from '../auth';
-import { EditSharedLinkDto } from './dto';
-import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
+import { ICryptoRepository } from '../crypto';
+import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './shared-link-response.dto';
+import { SharedLinkCreateDto, SharedLinkEditDto } from './shared-link.dto';
 import { ISharedLinkRepository } from './shared-link.repository';
 
 @Injectable()
 export class SharedLinkService {
-  constructor(@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository) {}
+  constructor(
+    @Inject(IAccessRepository) private accessRepository: IAccessRepository,
+    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
+    @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
+  ) {}
 
-  async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
+  getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
     return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
   }
 
@@ -30,7 +37,52 @@ export class SharedLinkService {
     return this.map(sharedLink, { withExif: true });
   }
 
-  async update(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) {
+  async create(authUser: AuthUserDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
+    switch (dto.type) {
+      case SharedLinkType.ALBUM:
+        if (!dto.albumId) {
+          throw new BadRequestException('Invalid albumId');
+        }
+
+        const isAlbumOwner = await this.accessRepository.hasAlbumOwnerAccess(authUser.id, dto.albumId);
+        if (!isAlbumOwner) {
+          throw new BadRequestException('Invalid albumId');
+        }
+
+        break;
+
+      case SharedLinkType.INDIVIDUAL:
+        if (!dto.assetIds || dto.assetIds.length === 0) {
+          throw new BadRequestException('Invalid assetIds');
+        }
+
+        for (const assetId of dto.assetIds) {
+          const hasAccess = await this.accessRepository.hasOwnerAssetAccess(authUser.id, assetId);
+          if (!hasAccess) {
+            throw new BadRequestException(`No access to assetId: ${assetId}`);
+          }
+        }
+
+        break;
+    }
+
+    const sharedLink = await this.repository.create({
+      key: this.cryptoRepository.randomBytes(50),
+      userId: authUser.id,
+      type: dto.type,
+      albumId: dto.albumId || null,
+      assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)),
+      description: dto.description || null,
+      expiresAt: dto.expiresAt || null,
+      allowUpload: dto.allowUpload ?? true,
+      allowDownload: dto.allowDownload ?? true,
+      showExif: dto.showExif ?? true,
+    });
+
+    return this.map(sharedLink, { withExif: true });
+  }
+
+  async update(authUser: AuthUserDto, id: string, dto: SharedLinkEditDto) {
     await this.findOrFail(authUser, id);
     const sharedLink = await this.repository.update({
       id,
@@ -57,6 +109,60 @@ export class SharedLinkService {
     return sharedLink;
   }
 
+  async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
+    const sharedLink = await this.findOrFail(authUser, id);
+
+    if (sharedLink.type !== SharedLinkType.INDIVIDUAL) {
+      throw new BadRequestException('Invalid shared link type');
+    }
+
+    const results: AssetIdsResponseDto[] = [];
+    for (const assetId of dto.assetIds) {
+      const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId);
+      if (hasAsset) {
+        results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE });
+        continue;
+      }
+
+      const hasAccess = await this.accessRepository.hasOwnerAssetAccess(authUser.id, assetId);
+      if (!hasAccess) {
+        results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION });
+        continue;
+      }
+
+      results.push({ assetId, success: true });
+      sharedLink.assets.push({ id: assetId } as AssetEntity);
+    }
+
+    await this.repository.update(sharedLink);
+
+    return results;
+  }
+
+  async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
+    const sharedLink = await this.findOrFail(authUser, id);
+
+    if (sharedLink.type !== SharedLinkType.INDIVIDUAL) {
+      throw new BadRequestException('Invalid shared link type');
+    }
+
+    const results: AssetIdsResponseDto[] = [];
+    for (const assetId of dto.assetIds) {
+      const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId);
+      if (!hasAsset) {
+        results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
+        continue;
+      }
+
+      results.push({ assetId, success: true });
+      sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== assetId);
+    }
+
+    await this.repository.update(sharedLink);
+
+    return results;
+  }
+
   private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
     return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink);
   }

+ 1 - 7
server/src/immich/api-v1/album/album.controller.ts

@@ -1,5 +1,5 @@
 import { AlbumResponseDto } from '@app/domain';
-import { Body, Controller, Delete, Get, Param, Post, Put, Query, Response } from '@nestjs/common';
+import { Body, Controller, Delete, Get, Param, Put, Query, Response } from '@nestjs/common';
 import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
 import { Response as Res } from 'express';
 import { handleDownload } from '../../app.utils';
@@ -10,7 +10,6 @@ import { UseValidation } from '../../decorators/use-validation.decorator';
 import { DownloadDto } from '../asset/dto/download-library.dto';
 import { AlbumService } from './album.service';
 import { AddAssetsDto } from './dto/add-assets.dto';
-import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto';
 import { RemoveAssetsDto } from './dto/remove-assets.dto';
 import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 
@@ -59,9 +58,4 @@ export class AlbumController {
   ) {
     return this.service.downloadArchive(authUser, id, dto).then((download) => handleDownload(download, res));
   }
-
-  @Post('create-shared-link')
-  createAlbumSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumSharedLinkDto) {
-    return this.service.createSharedLink(authUser, dto);
-  }
 }

+ 3 - 14
server/src/immich/api-v1/album/album.service.spec.ts

@@ -1,7 +1,7 @@
-import { AlbumResponseDto, ICryptoRepository, ISharedLinkRepository, mapUser } from '@app/domain';
+import { AlbumResponseDto, mapUser } from '@app/domain';
 import { AlbumEntity, UserEntity } from '@app/infra/entities';
 import { ForbiddenException, NotFoundException } from '@nestjs/common';
-import { newCryptoRepositoryMock, newSharedLinkRepositoryMock, userEntityStub } from '@test';
+import { userEntityStub } from '@test';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { DownloadService } from '../../modules/download/download.service';
 import { IAlbumRepository } from './album-repository';
@@ -11,9 +11,7 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 describe('Album service', () => {
   let sut: AlbumService;
   let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
-  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
   let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
-  let cryptoMock: jest.Mocked<ICryptoRepository>;
 
   const authUser: AuthUserDto = Object.freeze({
     id: '1111',
@@ -99,20 +97,11 @@ describe('Album service', () => {
       updateThumbnails: jest.fn(),
     };
 
-    sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
-
     downloadServiceMock = {
       downloadArchive: jest.fn(),
     };
 
-    cryptoMock = newCryptoRepositoryMock();
-
-    sut = new AlbumService(
-      albumRepositoryMock,
-      sharedLinkRepositoryMock,
-      downloadServiceMock as DownloadService,
-      cryptoMock,
-    );
+    sut = new AlbumService(albumRepositoryMock, downloadServiceMock as DownloadService);
   });
 
   it('gets an owned album', async () => {

+ 9 - 34
server/src/immich/api-v1/album/album.service.ts

@@ -1,36 +1,22 @@
-import {
-  AlbumResponseDto,
-  ICryptoRepository,
-  ISharedLinkRepository,
-  mapAlbum,
-  mapSharedLink,
-  SharedLinkCore,
-  SharedLinkResponseDto,
-} from '@app/domain';
-import { AlbumEntity, SharedLinkType } from '@app/infra/entities';
+import { AlbumResponseDto, mapAlbum } from '@app/domain';
+import { AlbumEntity } from '@app/infra/entities';
 import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { DownloadService } from '../../modules/download/download.service';
 import { DownloadDto } from '../asset/dto/download-library.dto';
 import { IAlbumRepository } from './album-repository';
 import { AddAssetsDto } from './dto/add-assets.dto';
-import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
 import { RemoveAssetsDto } from './dto/remove-assets.dto';
 import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 
 @Injectable()
 export class AlbumService {
-  readonly logger = new Logger(AlbumService.name);
-  private shareCore: SharedLinkCore;
+  private logger = new Logger(AlbumService.name);
 
   constructor(
     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
-    @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
     private downloadService: DownloadService,
-    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
-  ) {
-    this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository);
-  }
+  ) {}
 
   private async _getAlbum({
     authUser,
@@ -91,7 +77,7 @@ export class AlbumService {
   }
 
   async downloadArchive(authUser: AuthUserDto, albumId: string, dto: DownloadDto) {
-    this.shareCore.checkDownloadAccess(authUser);
+    this.checkDownloadAccess(authUser);
 
     const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
     const assets = (album.assets || []).map((asset) => asset).slice(dto.skip || 0);
@@ -99,20 +85,9 @@ export class AlbumService {
     return this.downloadService.downloadArchive(album.albumName, assets);
   }
 
-  async createSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise<SharedLinkResponseDto> {
-    const album = await this._getAlbum({ authUser, albumId: dto.albumId });
-
-    const sharedLink = await this.shareCore.create(authUser.id, {
-      type: SharedLinkType.ALBUM,
-      expiresAt: dto.expiresAt,
-      allowUpload: dto.allowUpload,
-      album,
-      assets: [],
-      description: dto.description,
-      allowDownload: dto.allowDownload,
-      showExif: dto.showExif,
-    });
-
-    return mapSharedLink(sharedLink);
+  private checkDownloadAccess(authUser: AuthUserDto) {
+    if (authUser.isPublicUser && !authUser.isAllowDownload) {
+      throw new ForbiddenException();
+    }
   }
 }

+ 0 - 35
server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts

@@ -1,35 +0,0 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsBoolean, IsDate, IsOptional, IsString } from 'class-validator';
-
-export class CreateAlbumShareLinkDto {
-  @ValidateUUID()
-  albumId!: string;
-
-  @IsOptional()
-  @IsDate()
-  @Type(() => Date)
-  @ApiProperty()
-  expiresAt?: Date;
-
-  @IsBoolean()
-  @IsOptional()
-  @ApiProperty()
-  allowUpload?: boolean;
-
-  @IsBoolean()
-  @IsOptional()
-  @ApiProperty()
-  allowDownload?: boolean;
-
-  @IsBoolean()
-  @IsOptional()
-  @ApiProperty()
-  showExif?: boolean;
-
-  @IsString()
-  @IsOptional()
-  @ApiProperty()
-  description?: string;
-}

+ 1 - 31
server/src/immich/api-v1/asset/asset.controller.ts

@@ -1,4 +1,4 @@
-import { AssetResponseDto, ImmichReadStream, SharedLinkResponseDto } from '@app/domain';
+import { AssetResponseDto, ImmichReadStream } from '@app/domain';
 import {
   Body,
   Controller,
@@ -10,7 +10,6 @@ import {
   HttpStatus,
   Param,
   ParseFilePipe,
-  Patch,
   Post,
   Put,
   Query,
@@ -28,15 +27,12 @@ import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'
 import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
 import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
 import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
-import { AddAssetsDto } from '../album/dto/add-assets.dto';
-import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
 import FileNotEmptyValidator from '../validation/file-not-empty-validator';
 import { AssetService } from './asset.service';
 import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
 import { AssetSearchDto } from './dto/asset-search.dto';
 import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
-import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { DeviceIdDto } from './dto/device-id.dto';
@@ -319,30 +315,4 @@ export class AssetController {
   ): Promise<AssetBulkUploadCheckResponseDto> {
     return this.assetService.bulkUploadCheck(authUser, dto);
   }
-
-  @Post('/shared-link')
-  createAssetsSharedLink(
-    @AuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) dto: CreateAssetsShareLinkDto,
-  ): Promise<SharedLinkResponseDto> {
-    return this.assetService.createAssetsSharedLink(authUser, dto);
-  }
-
-  @SharedLinkRoute()
-  @Patch('/shared-link/add')
-  addAssetsToSharedLink(
-    @AuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) dto: AddAssetsDto,
-  ): Promise<SharedLinkResponseDto> {
-    return this.assetService.addAssetsToSharedLink(authUser, dto);
-  }
-
-  @SharedLinkRoute()
-  @Patch('/shared-link/remove')
-  removeAssetsFromSharedLink(
-    @AuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) dto: RemoveAssetsDto,
-  ): Promise<SharedLinkResponseDto> {
-    return this.assetService.removeAssetsFromSharedLink(authUser, dto);
-  }
 }

+ 2 - 91
server/src/immich/api-v1/asset/asset.service.spec.ts

@@ -1,31 +1,19 @@
-import {
-  IAccessRepository,
-  ICryptoRepository,
-  IJobRepository,
-  ISharedLinkRepository,
-  IStorageRepository,
-  JobName,
-} from '@app/domain';
+import { IAccessRepository, IJobRepository, IStorageRepository, JobName } from '@app/domain';
 import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
-import { BadRequestException, ForbiddenException } from '@nestjs/common';
+import { ForbiddenException } from '@nestjs/common';
 import {
   assetEntityStub,
   authStub,
   fileStub,
   newAccessRepositoryMock,
-  newCryptoRepositoryMock,
   newJobRepositoryMock,
-  newSharedLinkRepositoryMock,
   newStorageRepositoryMock,
-  sharedLinkResponseStub,
-  sharedLinkStub,
 } from '@test';
 import { when } from 'jest-when';
 import { QueryFailedError, Repository } from 'typeorm';
 import { DownloadService } from '../../modules/download/download.service';
 import { IAssetRepository } from './asset-repository';
 import { AssetService } from './asset.service';
-import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 import { CreateAssetDto } from './dto/create-asset.dto';
 import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
@@ -134,8 +122,6 @@ describe('AssetService', () => {
   let accessMock: jest.Mocked<IAccessRepository>;
   let assetRepositoryMock: jest.Mocked<IAssetRepository>;
   let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
-  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
-  let cryptoMock: jest.Mocked<ICryptoRepository>;
   let jobMock: jest.Mocked<IJobRepository>;
   let storageMock: jest.Mocked<IStorageRepository>;
 
@@ -165,9 +151,7 @@ describe('AssetService', () => {
     };
 
     accessMock = newAccessRepositoryMock();
-    sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
     jobMock = newJobRepositoryMock();
-    cryptoMock = newCryptoRepositoryMock();
     storageMock = newStorageRepositoryMock();
 
     sut = new AssetService(
@@ -175,9 +159,7 @@ describe('AssetService', () => {
       assetRepositoryMock,
       a,
       downloadServiceMock as DownloadService,
-      sharedLinkRepositoryMock,
       jobMock,
-      cryptoMock,
       storageMock,
     );
 
@@ -189,77 +171,6 @@ describe('AssetService', () => {
       .mockResolvedValue(assetEntityStub.livePhotoMotionAsset);
   });
 
-  describe('createAssetsSharedLink', () => {
-    it('should create an individual share link', async () => {
-      const asset1 = _getAsset_1();
-      const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] };
-
-      assetRepositoryMock.getById.mockResolvedValue(asset1);
-      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
-      sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
-
-      await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
-
-      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
-      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.user1.id, asset1.id);
-    });
-  });
-
-  describe('updateAssetsInSharedLink', () => {
-    it('should require a valid shared link', async () => {
-      const asset1 = _getAsset_1();
-
-      const authDto = authStub.adminSharedLink;
-      const dto = { assetIds: [asset1.id] };
-
-      assetRepositoryMock.getById.mockResolvedValue(asset1);
-      sharedLinkRepositoryMock.get.mockResolvedValue(null);
-      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true);
-
-      await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
-
-      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
-      expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
-      expect(sharedLinkRepositoryMock.update).not.toHaveBeenCalled();
-    });
-
-    it('should add assets to a shared link', async () => {
-      const asset1 = _getAsset_1();
-
-      const authDto = authStub.adminSharedLink;
-      const dto = { assetIds: [asset1.id] };
-
-      assetRepositoryMock.getById.mockResolvedValue(asset1);
-      sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
-      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true);
-      sharedLinkRepositoryMock.update.mockResolvedValue(sharedLinkStub.valid);
-
-      await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
-
-      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
-      expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
-      expect(sharedLinkRepositoryMock.update).toHaveBeenCalled();
-    });
-
-    it('should remove assets from a shared link', async () => {
-      const asset1 = _getAsset_1();
-
-      const authDto = authStub.adminSharedLink;
-      const dto = { assetIds: [asset1.id] };
-
-      assetRepositoryMock.getById.mockResolvedValue(asset1);
-      sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
-      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true);
-      sharedLinkRepositoryMock.update.mockResolvedValue(sharedLinkStub.valid);
-
-      await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
-
-      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
-      expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
-      expect(sharedLinkRepositoryMock.update).toHaveBeenCalled();
-    });
-  });
-
   describe('uploadFile', () => {
     it('should handle a file upload', async () => {
       const assetEntity = _getAsset_1();

+ 3 - 68
server/src/immich/api-v1/asset/asset.service.ts

@@ -2,19 +2,14 @@ import {
   AssetResponseDto,
   getLivePhotoMotionFilename,
   IAccessRepository,
-  ICryptoRepository,
   IJobRepository,
   ImmichReadStream,
-  ISharedLinkRepository,
   IStorageRepository,
   JobName,
   mapAsset,
   mapAssetWithoutExif,
-  mapSharedLink,
-  SharedLinkCore,
-  SharedLinkResponseDto,
 } from '@app/domain';
-import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities';
+import { AssetEntity, AssetType } from '@app/infra/entities';
 import {
   BadRequestException,
   ForbiddenException,
@@ -33,15 +28,12 @@ import { QueryFailedError, Repository } from 'typeorm';
 import { promisify } from 'util';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { DownloadService } from '../../modules/download/download.service';
-import { AddAssetsDto } from '../album/dto/add-assets.dto';
-import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
 import { IAssetRepository } from './asset-repository';
 import { AssetCore } from './asset.core';
 import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
 import { AssetSearchDto } from './dto/asset-search.dto';
 import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
-import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { DownloadFilesDto } from './dto/download-files.dto';
@@ -80,22 +72,17 @@ interface ServableFile {
 @Injectable()
 export class AssetService {
   readonly logger = new Logger(AssetService.name);
-  private shareCore: SharedLinkCore;
   private assetCore: AssetCore;
 
   constructor(
     @Inject(IAccessRepository) private accessRepository: IAccessRepository,
     @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
-    @InjectRepository(AssetEntity)
-    private assetRepository: Repository<AssetEntity>,
+    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
     private downloadService: DownloadService,
-    @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
   ) {
     this.assetCore = new AssetCore(_assetRepository, jobRepository);
-    this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository);
   }
 
   public async uploadFile(
@@ -608,61 +595,9 @@ export class AssetService {
   }
 
   private checkDownloadAccess(authUser: AuthUserDto) {
-    this.shareCore.checkDownloadAccess(authUser);
-  }
-
-  async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> {
-    const assets = [];
-
-    await this.checkAssetsAccess(authUser, dto.assetIds);
-    for (const assetId of dto.assetIds) {
-      const asset = await this._assetRepository.getById(assetId);
-      assets.push(asset);
-    }
-
-    const sharedLink = await this.shareCore.create(authUser.id, {
-      type: SharedLinkType.INDIVIDUAL,
-      expiresAt: dto.expiresAt,
-      allowUpload: dto.allowUpload,
-      assets,
-      description: dto.description,
-      allowDownload: dto.allowDownload,
-      showExif: dto.showExif,
-    });
-
-    return mapSharedLink(sharedLink);
-  }
-
-  async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> {
-    if (!authUser.sharedLinkId) {
+    if (authUser.isPublicUser && !authUser.isAllowDownload) {
       throw new ForbiddenException();
     }
-
-    const assets = [];
-
-    for (const assetId of dto.assetIds) {
-      const asset = await this._assetRepository.getById(assetId);
-      assets.push(asset);
-    }
-
-    const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets);
-    return mapSharedLink(updatedLink);
-  }
-
-  async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> {
-    if (!authUser.sharedLinkId) {
-      throw new ForbiddenException();
-    }
-
-    const assets = [];
-
-    for (const assetId of dto.assetIds) {
-      const asset = await this._assetRepository.getById(assetId);
-      assets.push(asset);
-    }
-
-    const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets);
-    return mapSharedLink(updatedLink);
   }
 
   getExifPermission(authUser: AuthUserDto) {

+ 0 - 41
server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts

@@ -1,41 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsOptional, IsString } from 'class-validator';
-
-export class CreateAssetsShareLinkDto {
-  @IsArray()
-  @IsString({ each: true })
-  @IsNotEmpty({ each: true })
-  @ApiProperty({
-    isArray: true,
-    type: String,
-    title: 'Array asset IDs to be shared',
-    example: [
-      'bf973405-3f2a-48d2-a687-2ed4167164be',
-      'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
-      'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
-    ],
-  })
-  assetIds!: string[];
-
-  @IsDate()
-  @Type(() => Date)
-  @IsOptional()
-  expiresAt?: Date;
-
-  @IsBoolean()
-  @IsOptional()
-  allowUpload?: boolean;
-
-  @IsBoolean()
-  @IsOptional()
-  allowDownload?: boolean;
-
-  @IsBoolean()
-  @IsOptional()
-  showExif?: boolean;
-
-  @IsString()
-  @IsOptional()
-  description?: string;
-}

+ 38 - 5
server/src/immich/controllers/shared-link.controller.ts

@@ -1,13 +1,21 @@
-import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, SharedLinkService } from '@app/domain';
-import { Body, Controller, Delete, Get, Param, Patch } from '@nestjs/common';
+import {
+  AssetIdsDto,
+  AssetIdsResponseDto,
+  AuthUserDto,
+  SharedLinkCreateDto,
+  SharedLinkEditDto,
+  SharedLinkResponseDto,
+  SharedLinkService,
+} from '@app/domain';
+import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { AuthUser } from '../decorators/auth-user.decorator';
 import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
 import { UseValidation } from '../decorators/use-validation.decorator';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
-@ApiTags('share')
-@Controller('share')
+@ApiTags('Shared Link')
+@Controller('shared-link')
 @Authenticated()
 @UseValidation()
 export class SharedLinkController {
@@ -29,11 +37,16 @@ export class SharedLinkController {
     return this.service.get(authUser, id);
   }
 
+  @Post()
+  createSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: SharedLinkCreateDto) {
+    return this.service.create(authUser, dto);
+  }
+
   @Patch(':id')
   updateSharedLink(
     @AuthUser() authUser: AuthUserDto,
     @Param() { id }: UUIDParamDto,
-    @Body() dto: EditSharedLinkDto,
+    @Body() dto: SharedLinkEditDto,
   ): Promise<SharedLinkResponseDto> {
     return this.service.update(authUser, id, dto);
   }
@@ -42,4 +55,24 @@ export class SharedLinkController {
   removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
     return this.service.remove(authUser, id);
   }
+
+  @SharedLinkRoute()
+  @Put(':id/assets')
+  addSharedLinkAssets(
+    @AuthUser() authUser: AuthUserDto,
+    @Param() { id }: UUIDParamDto,
+    @Body() dto: AssetIdsDto,
+  ): Promise<AssetIdsResponseDto[]> {
+    return this.service.addAssets(authUser, id, dto);
+  }
+
+  @SharedLinkRoute()
+  @Delete(':id/assets')
+  removeSharedLinkAssets(
+    @AuthUser() authUser: AuthUserDto,
+    @Param() { id }: UUIDParamDto,
+    @Body() dto: AssetIdsDto,
+  ): Promise<AssetIdsResponseDto[]> {
+    return this.service.removeAssets(authUser, id, dto);
+  }
 }

+ 5 - 2
server/src/infra/entities/shared-link.entity.ts

@@ -18,8 +18,8 @@ export class SharedLinkEntity {
   @PrimaryGeneratedColumn('uuid')
   id!: string;
 
-  @Column({ nullable: true })
-  description?: string;
+  @Column({ type: 'varchar', nullable: true })
+  description!: string | null;
 
   @Column()
   userId!: string;
@@ -55,6 +55,9 @@ export class SharedLinkEntity {
   @Index('IDX_sharedlink_albumId')
   @ManyToOne(() => AlbumEntity, (album) => album.sharedLinks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
   album?: AlbumEntity;
+
+  @Column({ type: 'varchar', nullable: true })
+  albumId!: string | null;
 }
 
 export enum SharedLinkType {

+ 9 - 0
server/src/infra/repositories/access.repository.ts

@@ -95,4 +95,13 @@ export class AccessRepository implements IAccessRepository {
       }))
     );
   }
+
+  hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean> {
+    return this.albumRepository.exist({
+      where: {
+        id: albumId,
+        ownerId: userId,
+      },
+    });
+  }
 }

+ 25 - 4
server/test/fixtures.ts

@@ -777,6 +777,21 @@ export const loginResponseStub = {
 };
 
 export const sharedLinkStub = {
+  individual: Object.freeze({
+    id: '123',
+    userId: authStub.admin.id,
+    user: userEntityStub.admin,
+    key: sharedLinkBytes,
+    type: SharedLinkType.INDIVIDUAL,
+    createdAt: today,
+    expiresAt: tomorrow,
+    allowUpload: true,
+    allowDownload: true,
+    showExif: true,
+    album: undefined,
+    description: null,
+    assets: [assetEntityStub.image],
+  } as SharedLinkEntity),
   valid: Object.freeze({
     id: '123',
     userId: authStub.admin.id,
@@ -789,6 +804,8 @@ export const sharedLinkStub = {
     allowDownload: true,
     showExif: true,
     album: undefined,
+    albumId: null,
+    description: null,
     assets: [],
   } as SharedLinkEntity),
   expired: Object.freeze({
@@ -802,6 +819,8 @@ export const sharedLinkStub = {
     allowUpload: true,
     allowDownload: true,
     showExif: true,
+    description: null,
+    albumId: null,
     assets: [],
   } as SharedLinkEntity),
   readonlyNoExif: Object.freeze<SharedLinkEntity>({
@@ -815,7 +834,9 @@ export const sharedLinkStub = {
     allowUpload: false,
     allowDownload: false,
     showExif: false,
+    description: null,
     assets: [],
+    albumId: 'album-123',
     album: {
       id: 'album-123',
       ownerId: authStub.admin.id,
@@ -903,7 +924,7 @@ export const sharedLinkResponseStub = {
     allowUpload: true,
     assets: [],
     createdAt: today,
-    description: undefined,
+    description: null,
     expiresAt: tomorrow,
     id: '123',
     key: sharedLinkBytes.toString('base64url'),
@@ -917,7 +938,7 @@ export const sharedLinkResponseStub = {
     allowUpload: true,
     assets: [],
     createdAt: today,
-    description: undefined,
+    description: null,
     expiresAt: yesterday,
     id: '123',
     key: sharedLinkBytes.toString('base64url'),
@@ -932,7 +953,7 @@ export const sharedLinkResponseStub = {
     type: SharedLinkType.ALBUM,
     createdAt: today,
     expiresAt: tomorrow,
-    description: undefined,
+    description: null,
     allowUpload: false,
     allowDownload: false,
     showExif: true,
@@ -946,7 +967,7 @@ export const sharedLinkResponseStub = {
     type: SharedLinkType.ALBUM,
     createdAt: today,
     expiresAt: tomorrow,
-    description: undefined,
+    description: null,
     allowUpload: false,
     allowDownload: false,
     showExif: false,

+ 3 - 0
server/test/repositories/access.repository.mock.ts

@@ -3,9 +3,12 @@ import { IAccessRepository } from '@app/domain';
 export const newAccessRepositoryMock = (): jest.Mocked<IAccessRepository> => {
   return {
     hasPartnerAccess: jest.fn(),
+
     hasAlbumAssetAccess: jest.fn(),
     hasOwnerAssetAccess: jest.fn(),
     hasPartnerAssetAccess: jest.fn(),
     hasSharedLinkAssetAccess: jest.fn(),
+
+    hasAlbumOwnerAccess: jest.fn(),
   };
 };

+ 6 - 6
web/src/api/api.ts

@@ -7,16 +7,16 @@ import {
 	Configuration,
 	ConfigurationParameters,
 	JobApi,
+	JobName,
 	OAuthApi,
-	PersonApi,
 	PartnerApi,
+	PersonApi,
 	SearchApi,
 	ServerInfoApi,
-	ShareApi,
+	SharedLinkApi,
 	SystemConfigApi,
 	UserApi,
-	UserApiFp,
-	JobName
+	UserApiFp
 } from './open-api';
 import { BASE_PATH } from './open-api/base';
 import { DUMMY_BASE_URL, toPathString } from './open-api/common';
@@ -32,7 +32,7 @@ export class ImmichApi {
 	public partnerApi: PartnerApi;
 	public searchApi: SearchApi;
 	public serverInfoApi: ServerInfoApi;
-	public shareApi: ShareApi;
+	public sharedLinkApi: SharedLinkApi;
 	public personApi: PersonApi;
 	public systemConfigApi: SystemConfigApi;
 	public userApi: UserApi;
@@ -51,7 +51,7 @@ export class ImmichApi {
 		this.partnerApi = new PartnerApi(this.config);
 		this.searchApi = new SearchApi(this.config);
 		this.serverInfoApi = new ServerInfoApi(this.config);
-		this.shareApi = new ShareApi(this.config);
+		this.sharedLinkApi = new SharedLinkApi(this.config);
 		this.personApi = new PersonApi(this.config);
 		this.systemConfigApi = new SystemConfigApi(this.config);
 		this.userApi = new UserApi(this.config);

File diff suppressed because it is too large
+ 258 - 517
web/src/api/open-api/api.ts


+ 1 - 1
web/src/lib/components/album-page/user-selection-modal.svelte

@@ -31,7 +31,7 @@
 	});
 
 	const getSharedLinks = async () => {
-		const { data } = await api.shareApi.getAllSharedLinks();
+		const { data } = await api.sharedLinkApi.getAllSharedLinks();
 
 		sharedLinks = data.filter((link) => link.album?.id === album.id);
 	};

+ 45 - 14
web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte

@@ -1,34 +1,65 @@
 <script lang="ts">
 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-	import { AssetResponseDto, SharedLinkResponseDto, api } from '@api';
+	import { SharedLinkResponseDto, api } from '@api';
 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
+	import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
 	import { getAssetControlContext } from '../asset-select-control-bar.svelte';
+	import {
+		NotificationType,
+		notificationController
+	} from '../../shared-components/notification/notification';
+	import { handleError } from '../../../utils/handle-error';
 
 	export let sharedLink: SharedLinkResponseDto;
-	export let allAssets: AssetResponseDto[];
+
+	let removing = false;
 
 	const { getAssets, clearSelect } = getAssetControlContext();
 
-	const handleRemoveAssetsFromSharedLink = async () => {
-		if (window.confirm('Do you want to remove selected assets from the shared link?')) {
-			// TODO: Rename API method or change functionality. The assetIds passed
-			// in are kept instead of removed.
-			const assetsToKeep = allAssets.filter((a) => !getAssets().has(a));
-			await api.assetApi.removeAssetsFromSharedLink({
-				removeAssetsDto: {
-					assetIds: assetsToKeep.map((a) => a.id)
+	const handleRemove = async () => {
+		try {
+			const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
+				id: sharedLink.id,
+				assetIdsDto: {
+					assetIds: Array.from(getAssets()).map((asset) => asset.id)
 				},
-				key: sharedLink?.key
+				key: sharedLink.key
+			});
+
+			for (const result of results) {
+				if (!result.success) {
+					continue;
+				}
+
+				sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== result.assetId);
+			}
+
+			const count = results.filter((item) => item.success).length;
+
+			notificationController.show({
+				type: NotificationType.Info,
+				message: `Removed ${count} assets`
 			});
 
-			sharedLink.assets = assetsToKeep;
 			clearSelect();
+		} catch (error) {
+			handleError(error, 'Unable to remove assets from shared link');
 		}
 	};
 </script>
 
 <CircleIconButton
-	title="Remove from album"
-	on:click={handleRemoveAssetsFromSharedLink}
+	title="Remove from shared link"
+	on:click={() => (removing = true)}
 	logo={DeleteOutline}
 />
+
+{#if removing}
+	<ConfirmDialogue
+		title="Remove Assets?"
+		prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
+		confirmText="Remove"
+		on:confirm={() => handleRemove()}
+		on:cancel={() => (removing = false)}
+	/>
+{/if}

+ 14 - 16
web/src/lib/components/share-page/individual-shared-viewer.svelte

@@ -17,6 +17,7 @@
 		notificationController,
 		NotificationType
 	} from '../shared-components/notification/notification';
+	import { handleError } from '../../utils/handle-error';
 
 	export let sharedLink: SharedLinkResponseDto;
 	export let isOwned: boolean;
@@ -26,43 +27,40 @@
 	$: assets = sharedLink.assets;
 	$: isMultiSelectionMode = selectedAssets.size > 0;
 
-	const clearMultiSelectAssetAssetHandler = () => {
-		selectedAssets = new Set();
-	};
-
 	const downloadAssets = async () => {
-		await bulkDownload('immich-shared', assets, undefined, sharedLink?.key);
+		await bulkDownload('immich-shared', assets, undefined, sharedLink.key);
 	};
 
 	const handleUploadAssets = async () => {
 		try {
-			const results = await openFileUploadDialog(undefined, sharedLink?.key);
+			const results = await openFileUploadDialog(undefined, sharedLink.key);
 
-			const assetIds = results.filter((id) => !!id) as string[];
-
-			await api.assetApi.addAssetsToSharedLink({
-				addAssetsDto: {
-					assetIds
+			const { data } = await api.sharedLinkApi.addSharedLinkAssets({
+				id: sharedLink.id,
+				assetIdsDto: {
+					assetIds: results.filter((id) => !!id) as string[]
 				},
-				key: sharedLink?.key
+				key: sharedLink.key
 			});
 
+			const added = data.filter((item) => item.success).length;
+
 			notificationController.show({
-				message: `Successfully add ${assetIds.length} to the shared link`,
+				message: `Added ${added} assets`,
 				type: NotificationType.Info
 			});
 		} catch (e) {
-			console.error('handleUploadAssets', e);
+			handleError(e, 'Unable to add assets to shared link');
 		}
 	};
 </script>
 
 <section class="bg-immich-bg dark:bg-immich-dark-bg">
 	{#if isMultiSelectionMode}
-		<AssetSelectControlBar assets={selectedAssets} clearSelect={clearMultiSelectAssetAssetHandler}>
+		<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
 			<DownloadAction filename="immich-shared" sharedLinkKey={sharedLink.key} />
 			{#if isOwned}
-				<RemoveFromSharedLink bind:sharedLink allAssets={assets} />
+				<RemoveFromSharedLink bind:sharedLink />
 			{/if}
 		</AssetSelectControlBar>
 	{:else}

+ 62 - 79
web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte

@@ -7,31 +7,31 @@
 	import { handleError } from '$lib/utils/handle-error';
 	import {
 		AlbumResponseDto,
+		api,
 		AssetResponseDto,
 		SharedLinkResponseDto,
-		SharedLinkType,
-		api
+		SharedLinkType
 	} from '@api';
 	import { createEventDispatcher, onMount } from 'svelte';
 	import Link from 'svelte-material-icons/Link.svelte';
 	import BaseModal from '../base-modal.svelte';
 	import type { ImmichDropDownOption } from '../dropdown-button.svelte';
 	import DropdownButton from '../dropdown-button.svelte';
-	import { NotificationType, notificationController } from '../notification/notification';
+	import { notificationController, NotificationType } from '../notification/notification';
 
 	export let shareType: SharedLinkType;
 	export let sharedAssets: AssetResponseDto[] = [];
 	export let album: AlbumResponseDto | undefined = undefined;
 	export let editingLink: SharedLinkResponseDto | undefined = undefined;
 
-	let isShowSharedLink = false;
-	let expirationTime = '';
-	let isAllowUpload = false;
-	let sharedLink = '';
+	let sharedLink: string | null = null;
 	let description = '';
+	let allowDownload = true;
+	let allowUpload = false;
+	let showExif = true;
+	let expirationTime = '';
 	let shouldChangeExpirationTime = false;
-	let isAllowDownload = true;
-	let shouldShowExif = true;
+
 	const dispatch = createEventDispatcher();
 
 	const expiredDateOption: ImmichDropDownOption = {
@@ -44,9 +44,9 @@
 			if (editingLink.description) {
 				description = editingLink.description;
 			}
-			isAllowUpload = editingLink.allowUpload;
-			isAllowDownload = editingLink.allowDownload;
-			shouldShowExif = editingLink.showExif;
+			allowUpload = editingLink.allowUpload;
+			allowDownload = editingLink.allowDownload;
+			showExif = editingLink.showExif;
 		}
 	});
 
@@ -58,49 +58,32 @@
 			: undefined;
 
 		try {
-			if (shareType === SharedLinkType.Album && album) {
-				const { data } = await api.albumApi.createAlbumSharedLink({
-					createAlbumShareLinkDto: {
-						albumId: album.id,
-						expiresAt: expirationDate,
-						allowUpload: isAllowUpload,
-						description: description,
-						allowDownload: isAllowDownload,
-						showExif: shouldShowExif
-					}
-				});
-				buildSharedLink(data);
-			} else {
-				const { data } = await api.assetApi.createAssetsSharedLink({
-					createAssetsShareLinkDto: {
-						assetIds: sharedAssets.map((a) => a.id),
-						expiresAt: expirationDate,
-						allowUpload: isAllowUpload,
-						description: description,
-						allowDownload: isAllowDownload,
-						showExif: shouldShowExif
-					}
-				});
-				buildSharedLink(data);
-			}
+			const { data } = await api.sharedLinkApi.createSharedLink({
+				sharedLinkCreateDto: {
+					type: shareType,
+					albumId: album ? album.id : undefined,
+					assetIds: sharedAssets.map((a) => a.id),
+					expiresAt: expirationDate,
+					allowUpload,
+					description,
+					allowDownload,
+					showExif
+				}
+			});
+			sharedLink = `${window.location.origin}/share/${data.key}`;
 		} catch (e) {
 			handleError(e, 'Failed to create shared link');
 		}
-
-		isShowSharedLink = true;
-	};
-
-	const buildSharedLink = (createdLink: SharedLinkResponseDto) => {
-		sharedLink = `${window.location.origin}/share/${createdLink.key}`;
 	};
 
 	const handleCopy = async () => {
+		if (!sharedLink) {
+			return;
+		}
+
 		try {
 			await navigator.clipboard.writeText(sharedLink);
-			notificationController.show({
-				message: 'Copied to clipboard!',
-				type: NotificationType.Info
-			});
+			notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info });
 		} catch (e) {
 			handleError(
 				e,
@@ -129,34 +112,36 @@
 	};
 
 	const handleEditLink = async () => {
-		if (editingLink) {
-			try {
-				const expirationTime = getExpirationTimeInMillisecond();
-				const currentTime = new Date().getTime();
-				const expirationDate: string | null = expirationTime
-					? new Date(currentTime + expirationTime).toISOString()
-					: null;
+		if (!editingLink) {
+			return;
+		}
 
-				await api.shareApi.updateSharedLink({
-					id: editingLink.id,
-					editSharedLinkDto: {
-						description,
-						expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
-						allowUpload: isAllowUpload,
-						allowDownload: isAllowDownload,
-						showExif: shouldShowExif
-					}
-				});
+		try {
+			const expirationTime = getExpirationTimeInMillisecond();
+			const currentTime = new Date().getTime();
+			const expirationDate: string | null = expirationTime
+				? new Date(currentTime + expirationTime).toISOString()
+				: null;
+
+			await api.sharedLinkApi.updateSharedLink({
+				id: editingLink.id,
+				sharedLinkEditDto: {
+					description,
+					expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
+					allowUpload: allowUpload,
+					allowDownload: allowDownload,
+					showExif: showExif
+				}
+			});
 
-				notificationController.show({
-					type: NotificationType.Info,
-					message: 'Edited'
-				});
+			notificationController.show({
+				type: NotificationType.Info,
+				message: 'Edited'
+			});
 
-				dispatch('close');
-			} catch (e) {
-				handleError(e, 'Failed to edit shared link');
-			}
+			dispatch('close');
+		} catch (e) {
+			handleError(e, 'Failed to edit shared link');
 		}
 	};
 </script>
@@ -212,15 +197,15 @@
 				</div>
 
 				<div class="my-3">
-					<SettingSwitch bind:checked={shouldShowExif} title={'Show metadata'} />
+					<SettingSwitch bind:checked={showExif} title={'Show metadata'} />
 				</div>
 
 				<div class="my-3">
-					<SettingSwitch bind:checked={isAllowDownload} title={'Allow public user to download'} />
+					<SettingSwitch bind:checked={allowDownload} title={'Allow public user to download'} />
 				</div>
 
 				<div class="my-3">
-					<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} />
+					<SettingSwitch bind:checked={allowUpload} title={'Allow public user to upload'} />
 				</div>
 
 				<div class="text-sm">
@@ -248,7 +233,7 @@
 	<hr />
 
 	<section class="m-6">
-		{#if !isShowSharedLink}
+		{#if !sharedLink}
 			{#if editingLink}
 				<div class="flex justify-end">
 					<Button size="sm" rounded="lg" on:click={handleEditLink}>Confirm</Button>
@@ -258,9 +243,7 @@
 					<Button size="sm" rounded="lg" on:click={handleCreateSharedLink}>Create link</Button>
 				</div>
 			{/if}
-		{/if}
-
-		{#if isShowSharedLink}
+		{:else}
 			<div class="flex w-full gap-4">
 				<input class="immich-form-input w-full" bind:value={sharedLink} disabled />
 

+ 1 - 1
web/src/routes/(user)/share/[key]/+page.server.ts

@@ -7,7 +7,7 @@ export const load = (async ({ params, locals: { api } }) => {
 	const { key } = params;
 
 	try {
-		const { data: sharedLink } = await api.shareApi.getMySharedLink({ key });
+		const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
 
 		const assetCount = sharedLink.assets.length;
 		const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;

+ 40 - 39
web/src/routes/(user)/sharing/sharedlinks/+page.svelte

@@ -1,7 +1,6 @@
 <script lang="ts">
 	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
-
 	import { api, SharedLinkResponseDto } from '@api';
 	import { goto } from '$app/navigation';
 	import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
@@ -11,53 +10,45 @@
 	} from '$lib/components/shared-components/notification/notification';
 	import { onMount } from 'svelte';
 	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
+	import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
+	import { handleError } from '$lib/utils/handle-error';
+	import { AppRoute } from '$lib/constants';
 
 	let sharedLinks: SharedLinkResponseDto[] = [];
-	let showEditForm = false;
-	let editSharedLink: SharedLinkResponseDto;
-
-	onMount(async () => {
-		sharedLinks = await getSharedLinks();
-	});
+	let editSharedLink: SharedLinkResponseDto | null = null;
 
-	const getSharedLinks = async () => {
-		const { data: sharedLinks } = await api.shareApi.getAllSharedLinks();
+	let deleteLinkId: string | null = null;
 
-		return sharedLinks;
+	const refresh = async () => {
+		const { data } = await api.sharedLinkApi.getAllSharedLinks();
+		sharedLinks = data;
 	};
 
-	const handleDeleteLink = async (linkId: string) => {
-		if (window.confirm('Do you want to delete the shared link? ')) {
-			try {
-				await api.shareApi.removeSharedLink({ id: linkId });
-				notificationController.show({
-					message: 'Shared link deleted',
-					type: NotificationType.Info
-				});
+	onMount(async () => {
+		await refresh();
+	});
 
-				sharedLinks = await getSharedLinks();
-			} catch (e) {
-				console.error(e);
-				notificationController.show({
-					message: 'Failed to delete shared link',
-					type: NotificationType.Error
-				});
-			}
+	const handleDeleteLink = async () => {
+		if (!deleteLinkId) {
+			return;
 		}
-	};
 
-	const handleEditLink = async (id: string) => {
-		const { data } = await api.shareApi.getSharedLinkById({ id });
-		editSharedLink = data;
-		showEditForm = true;
+		try {
+			await api.sharedLinkApi.removeSharedLink({ id: deleteLinkId });
+			notificationController.show({ message: 'Deleted shared link', type: NotificationType.Info });
+			deleteLinkId = null;
+			refresh();
+		} catch (error) {
+			handleError(error, 'Unable to delete shared link');
+		}
 	};
 
 	const handleEditDone = async () => {
-		sharedLinks = await getSharedLinks();
-		showEditForm = false;
+		refresh();
+		editSharedLink = null;
 	};
 
-	const handleCopy = async (key: string) => {
+	const handleCopyLink = async (key: string) => {
 		const link = `${window.location.origin}/share/${key}`;
 		await navigator.clipboard.writeText(link);
 		notificationController.show({
@@ -67,7 +58,7 @@
 	};
 </script>
 
-<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto('/sharing')}>
+<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
 	<svelte:fragment slot="leading">Shared links</svelte:fragment>
 </ControlAppBar>
 
@@ -86,16 +77,16 @@
 			{#each sharedLinks as link (link.id)}
 				<SharedLinkCard
 					{link}
-					on:delete={() => handleDeleteLink(link.id)}
-					on:edit={() => handleEditLink(link.id)}
-					on:copy={() => handleCopy(link.key)}
+					on:delete={() => (deleteLinkId = link.id)}
+					on:edit={() => (editSharedLink = link)}
+					on:copy={() => handleCopyLink(link.key)}
 				/>
 			{/each}
 		</div>
 	{/if}
 </section>
 
-{#if showEditForm}
+{#if editSharedLink}
 	<CreateSharedLinkModal
 		editingLink={editSharedLink}
 		shareType={editSharedLink.type}
@@ -103,3 +94,13 @@
 		on:close={handleEditDone}
 	/>
 {/if}
+
+{#if deleteLinkId}
+	<ConfirmDialogue
+		title="Delete Shared Link"
+		prompt="Are you want to delete this shared link?"
+		confirmText="Delete"
+		on:confirm={() => handleDeleteLink()}
+		on:cancel={() => (deleteLinkId = null)}
+	/>
+{/if}

Some files were not shown because too many files changed in this diff