Jelajahi Sumber

refactor(server): shared links (#2632)

* refactor: rename share => shared-link

* refactor: shared link crud methods

* chore: open api
Jason Rasmussen 2 tahun lalu
induk
melakukan
3ea2fe1c48
30 mengubah file dengan 456 tambahan dan 485 penghapusan
  1. 1 1
      mobile/openapi/README.md
  2. 40 40
      mobile/openapi/doc/ShareApi.md
  3. 52 52
      mobile/openapi/lib/api/share_api.dart
  4. 5 5
      mobile/openapi/test/share_api_test.dart
  5. 9 3
      server/apps/immich/src/api-v1/album/album.service.ts
  6. 5 5
      server/apps/immich/src/api-v1/asset/asset.service.spec.ts
  7. 3 3
      server/apps/immich/src/api-v1/asset/asset.service.ts
  8. 10 10
      server/apps/immich/src/controllers/shared-link.controller.ts
  9. 23 23
      server/immich-openapi-specs.json
  10. 1 1
      server/libs/domain/src/auth/auth.service.spec.ts
  11. 3 3
      server/libs/domain/src/auth/auth.service.ts
  12. 2 2
      server/libs/domain/src/domain.module.ts
  13. 1 1
      server/libs/domain/src/index.ts
  14. 0 127
      server/libs/domain/src/share/share.service.spec.ts
  15. 0 60
      server/libs/domain/src/share/share.service.ts
  16. 0 0
      server/libs/domain/src/shared-link/dto/create-shared-link.dto.ts
  17. 0 0
      server/libs/domain/src/shared-link/dto/edit-shared-link.dto.ts
  18. 0 0
      server/libs/domain/src/shared-link/dto/index.ts
  19. 2 2
      server/libs/domain/src/shared-link/index.ts
  20. 0 0
      server/libs/domain/src/shared-link/response-dto/index.ts
  21. 0 0
      server/libs/domain/src/shared-link/response-dto/shared-link-response.dto.ts
  22. 7 32
      server/libs/domain/src/shared-link/shared-link.core.ts
  23. 1 1
      server/libs/domain/src/shared-link/shared-link.repository.ts
  24. 103 0
      server/libs/domain/src/shared-link/shared-link.service.spec.ts
  25. 63 0
      server/libs/domain/src/shared-link/shared-link.service.ts
  26. 13 3
      server/libs/domain/test/fixtures.ts
  27. 1 1
      server/libs/domain/test/shared-link.repository.mock.ts
  28. 29 28
      server/libs/infra/src/repositories/shared-link.repository.ts
  29. 81 81
      web/src/api/open-api/api.ts
  30. 1 1
      web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte

+ 1 - 1
mobile/openapi/README.md

@@ -145,11 +145,11 @@ 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* | [**editSharedLink**](doc//ShareApi.md#editsharedlink) | **PATCH** /share/{id} | 
 *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} | 
 *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 | 

+ 40 - 40
mobile/openapi/doc/ShareApi.md

@@ -9,15 +9,15 @@ All URIs are relative to */api*
 
 Method | HTTP request | Description
 ------------- | ------------- | -------------
-[**editSharedLink**](ShareApi.md#editsharedlink) | **PATCH** /share/{id} | 
 [**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} | 
 
 
-# **editSharedLink**
-> SharedLinkResponseDto editSharedLink(id, editSharedLinkDto)
+# **getAllSharedLinks**
+> List<SharedLinkResponseDto> getAllSharedLinks()
 
 
 
@@ -40,27 +40,21 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = ShareApi();
-final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
-final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto | 
 
 try {
-    final result = api_instance.editSharedLink(id, editSharedLinkDto);
+    final result = api_instance.getAllSharedLinks();
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->editSharedLink: $e\n');
+    print('Exception when calling ShareApi->getAllSharedLinks: $e\n');
 }
 ```
 
 ### Parameters
-
-Name | Type | Description  | Notes
-------------- | ------------- | ------------- | -------------
- **id** | **String**|  | 
- **editSharedLinkDto** | [**EditSharedLinkDto**](EditSharedLinkDto.md)|  | 
+This endpoint does not need any parameter.
 
 ### Return type
 
-[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
+[**List<SharedLinkResponseDto>**](SharedLinkResponseDto.md)
 
 ### Authorization
 
@@ -68,13 +62,13 @@ Name | Type | Description  | Notes
 
 ### HTTP request headers
 
- - **Content-Type**: application/json
+ - **Content-Type**: Not defined
  - **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()
+# **getMySharedLink**
+> SharedLinkResponseDto getMySharedLink(key)
 
 
 
@@ -97,21 +91,25 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = ShareApi();
+final key = key_example; // String | 
 
 try {
-    final result = api_instance.getAllSharedLinks();
+    final result = api_instance.getMySharedLink(key);
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->getAllSharedLinks: $e\n');
+    print('Exception when calling ShareApi->getMySharedLink: $e\n');
 }
 ```
 
 ### Parameters
-This endpoint does not need any parameter.
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **key** | **String**|  | [optional] 
 
 ### Return type
 
-[**List<SharedLinkResponseDto>**](SharedLinkResponseDto.md)
+[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
 
 ### Authorization
 
@@ -124,8 +122,8 @@ This endpoint does not need any parameter.
 
 [[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)
 
-# **getMySharedLink**
-> SharedLinkResponseDto getMySharedLink(key)
+# **getSharedLinkById**
+> SharedLinkResponseDto getSharedLinkById(id)
 
 
 
@@ -148,13 +146,13 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = ShareApi();
-final key = key_example; // String | 
+final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
 
 try {
-    final result = api_instance.getMySharedLink(key);
+    final result = api_instance.getSharedLinkById(id);
     print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->getMySharedLink: $e\n');
+    print('Exception when calling ShareApi->getSharedLinkById: $e\n');
 }
 ```
 
@@ -162,7 +160,7 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **key** | **String**|  | [optional] 
+ **id** | **String**|  | 
 
 ### Return type
 
@@ -179,8 +177,8 @@ 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)
 
-# **getSharedLinkById**
-> SharedLinkResponseDto getSharedLinkById(id)
+# **removeSharedLink**
+> removeSharedLink(id)
 
 
 
@@ -206,10 +204,9 @@ final api_instance = ShareApi();
 final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
 
 try {
-    final result = api_instance.getSharedLinkById(id);
-    print(result);
+    api_instance.removeSharedLink(id);
 } catch (e) {
-    print('Exception when calling ShareApi->getSharedLinkById: $e\n');
+    print('Exception when calling ShareApi->removeSharedLink: $e\n');
 }
 ```
 
@@ -221,7 +218,7 @@ Name | Type | Description  | Notes
 
 ### Return type
 
-[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
+void (empty response body)
 
 ### Authorization
 
@@ -230,12 +227,12 @@ Name | Type | Description  | Notes
 ### HTTP request headers
 
  - **Content-Type**: Not defined
- - **Accept**: application/json
+ - **Accept**: Not defined
 
 [[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)
 
-# **removeSharedLink**
-> removeSharedLink(id)
+# **updateSharedLink**
+> SharedLinkResponseDto updateSharedLink(id, editSharedLinkDto)
 
 
 
@@ -259,11 +256,13 @@ import 'package:openapi/api.dart';
 
 final api_instance = ShareApi();
 final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto | 
 
 try {
-    api_instance.removeSharedLink(id);
+    final result = api_instance.updateSharedLink(id, editSharedLinkDto);
+    print(result);
 } catch (e) {
-    print('Exception when calling ShareApi->removeSharedLink: $e\n');
+    print('Exception when calling ShareApi->updateSharedLink: $e\n');
 }
 ```
 
@@ -272,10 +271,11 @@ try {
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
  **id** | **String**|  | 
+ **editSharedLinkDto** | [**EditSharedLinkDto**](EditSharedLinkDto.md)|  | 
 
 ### Return type
 
-void (empty response body)
+[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
 
 ### Authorization
 
@@ -283,8 +283,8 @@ void (empty response body)
 
 ### HTTP request headers
 
- - **Content-Type**: Not defined
- - **Accept**: Not defined
+ - **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)
 

+ 52 - 52
mobile/openapi/lib/api/share_api.dart

@@ -16,58 +16,6 @@ class ShareApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'PATCH /share/{id}' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [String] id (required):
-  ///
-  /// * [EditSharedLinkDto] editSharedLinkDto (required):
-  Future<Response> editSharedLinkWithHttpInfo(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?> editSharedLink(String id, EditSharedLinkDto editSharedLinkDto,) async {
-    final response = await editSharedLinkWithHttpInfo(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;
-  }
-
   /// Performs an HTTP 'GET /share' operation and returns the [Response].
   Future<Response> getAllSharedLinksWithHttpInfo() async {
     // ignore: prefer_const_declarations
@@ -250,4 +198,56 @@ class ShareApi {
       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;
+  }
 }

+ 5 - 5
mobile/openapi/test/share_api_test.dart

@@ -17,11 +17,6 @@ void main() {
   // final instance = ShareApi();
 
   group('tests for ShareApi', () {
-    //Future<SharedLinkResponseDto> editSharedLink(String id, EditSharedLinkDto editSharedLinkDto) async
-    test('test editSharedLink', () async {
-      // TODO
-    });
-
     //Future<List<SharedLinkResponseDto>> getAllSharedLinks() async
     test('test getAllSharedLinks', () async {
       // TODO
@@ -42,5 +37,10 @@ void main() {
       // TODO
     });
 
+    //Future<SharedLinkResponseDto> updateSharedLink(String id, EditSharedLinkDto editSharedLinkDto) async
+    test('test updateSharedLink', () async {
+      // TODO
+    });
+
   });
 }

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

@@ -10,13 +10,19 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 import { AddAssetsDto } from './dto/add-assets.dto';
 import { DownloadService } from '../../modules/download/download.service';
 import { DownloadDto } from '../asset/dto/download-library.dto';
-import { ShareCore, ISharedLinkRepository, mapSharedLink, SharedLinkResponseDto, ICryptoRepository } from '@app/domain';
+import {
+  SharedLinkCore,
+  ISharedLinkRepository,
+  mapSharedLink,
+  SharedLinkResponseDto,
+  ICryptoRepository,
+} from '@app/domain';
 import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
 
 @Injectable()
 export class AlbumService {
   readonly logger = new Logger(AlbumService.name);
-  private shareCore: ShareCore;
+  private shareCore: SharedLinkCore;
 
   constructor(
     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@@ -25,7 +31,7 @@ export class AlbumService {
     @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
   ) {
-    this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
+    this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository);
   }
 
   private async _getAlbum({

+ 5 - 5
server/apps/immich/src/api-v1/asset/asset.service.spec.ts

@@ -229,7 +229,7 @@ describe('AssetService', () => {
 
       expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
       expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
-      expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
+      expect(sharedLinkRepositoryMock.update).not.toHaveBeenCalled();
     });
 
     it('should add assets to a shared link', async () => {
@@ -241,13 +241,13 @@ describe('AssetService', () => {
       assetRepositoryMock.getById.mockResolvedValue(asset1);
       sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
       sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
-      sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
+      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.save).toHaveBeenCalled();
+      expect(sharedLinkRepositoryMock.update).toHaveBeenCalled();
     });
 
     it('should remove assets from a shared link', async () => {
@@ -259,13 +259,13 @@ describe('AssetService', () => {
       assetRepositoryMock.getById.mockResolvedValue(asset1);
       sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
       sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
-      sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
+      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.save).toHaveBeenCalled();
+      expect(sharedLinkRepositoryMock.update).toHaveBeenCalled();
     });
   });
 

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

@@ -54,7 +54,7 @@ import { ICryptoRepository, IJobRepository } from '@app/domain';
 import { DownloadService } from '../../modules/download/download.service';
 import { DownloadDto } from './dto/download-library.dto';
 import { IAlbumRepository } from '../album/album-repository';
-import { ShareCore } from '@app/domain';
+import { SharedLinkCore } from '@app/domain';
 import { IPartnerRepository } from '@app/domain';
 import { ISharedLinkRepository } from '@app/domain';
 import { DownloadFilesDto } from './dto/download-files.dto';
@@ -80,7 +80,7 @@ interface ServableFile {
 @Injectable()
 export class AssetService {
   readonly logger = new Logger(AssetService.name);
-  private shareCore: ShareCore;
+  private shareCore: SharedLinkCore;
   private assetCore: AssetCore;
   private partnerCore: PartnerCore;
 
@@ -97,7 +97,7 @@ export class AssetService {
     @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
   ) {
     this.assetCore = new AssetCore(_assetRepository, jobRepository);
-    this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
+    this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository);
     this.partnerCore = new PartnerCore(partnerRepository);
   }
 

+ 10 - 10
server/apps/immich/src/controllers/shared-link.controller.ts

@@ -1,4 +1,4 @@
-import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, ShareService } from '@app/domain';
+import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, SharedLinkService } from '@app/domain';
 import { Body, Controller, Delete, Get, Param, Patch } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { GetAuthUser } from '../decorators/auth-user.decorator';
@@ -11,7 +11,7 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
 @Authenticated()
 @UseValidation()
 export class SharedLinkController {
-  constructor(private readonly service: ShareService) {}
+  constructor(private readonly service: SharedLinkService) {}
 
   @Get()
   getAllSharedLinks(@GetAuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
@@ -29,20 +29,20 @@ export class SharedLinkController {
     @GetAuthUser() authUser: AuthUserDto,
     @Param() { id }: UUIDParamDto,
   ): Promise<SharedLinkResponseDto> {
-    return this.service.getById(authUser, id, true);
-  }
-
-  @Delete(':id')
-  removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
-    return this.service.remove(authUser, id);
+    return this.service.get(authUser, id);
   }
 
   @Patch(':id')
-  editSharedLink(
+  updateSharedLink(
     @GetAuthUser() authUser: AuthUserDto,
     @Param() { id }: UUIDParamDto,
     @Body() dto: EditSharedLinkDto,
   ): Promise<SharedLinkResponseDto> {
-    return this.service.edit(authUser, id, dto);
+    return this.service.update(authUser, id, dto);
+  }
+
+  @Delete(':id')
+  removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
+    return this.service.remove(authUser, id);
   }
 }

+ 23 - 23
server/immich-openapi-specs.json

@@ -1565,8 +1565,8 @@
           }
         ]
       },
-      "delete": {
-        "operationId": "removeSharedLink",
+      "patch": {
+        "operationId": "updateSharedLink",
         "parameters": [
           {
             "name": "id",
@@ -1578,9 +1578,26 @@
             }
           }
         ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/EditSharedLinkDto"
+              }
+            }
+          }
+        },
         "responses": {
           "200": {
-            "description": ""
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SharedLinkResponseDto"
+                }
+              }
+            }
           }
         },
         "tags": [
@@ -1598,8 +1615,8 @@
           }
         ]
       },
-      "patch": {
-        "operationId": "editSharedLink",
+      "delete": {
+        "operationId": "removeSharedLink",
         "parameters": [
           {
             "name": "id",
@@ -1611,26 +1628,9 @@
             }
           }
         ],
-        "requestBody": {
-          "required": true,
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/EditSharedLinkDto"
-              }
-            }
-          }
-        },
         "responses": {
           "200": {
-            "description": "",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/SharedLinkResponseDto"
-                }
-              }
-            }
+            "description": ""
           }
         },
         "tags": [

+ 1 - 1
server/libs/domain/src/auth/auth.service.spec.ts

@@ -20,7 +20,7 @@ import {
 } from '../../test';
 import { IKeyRepository } from '../api-key';
 import { ICryptoRepository } from '../crypto/crypto.repository';
-import { ISharedLinkRepository } from '../share';
+import { ISharedLinkRepository } from '../shared-link';
 import { ISystemConfigRepository } from '../system-config';
 import { IUserRepository } from '../user';
 import { IUserTokenRepository } from '../user-token';

+ 3 - 3
server/libs/domain/src/auth/auth.service.ts

@@ -18,7 +18,7 @@ import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from '.
 import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
 import { IUserTokenRepository, UserTokenCore } from '../user-token';
 import cookieParser from 'cookie';
-import { ISharedLinkRepository, ShareCore } from '../share';
+import { ISharedLinkRepository, SharedLinkCore } from '../shared-link';
 import { APIKeyCore } from '../api-key/api-key.core';
 import { IKeyRepository } from '../api-key';
 import { AuthDeviceResponseDto, mapUserToken } from './response-dto';
@@ -29,7 +29,7 @@ export class AuthService {
   private authCore: AuthCore;
   private oauthCore: OAuthCore;
   private userCore: UserCore;
-  private shareCore: ShareCore;
+  private shareCore: SharedLinkCore;
   private keyCore: APIKeyCore;
 
   private logger = new Logger(AuthService.name);
@@ -48,7 +48,7 @@ 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 ShareCore(shareRepository, cryptoRepository);
+    this.shareCore = new SharedLinkCore(shareRepository, cryptoRepository);
     this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
   }
 

+ 2 - 2
server/libs/domain/src/domain.module.ts

@@ -12,7 +12,7 @@ import { PartnerService } from './partner';
 import { PersonService } from './person';
 import { SearchService } from './search';
 import { ServerInfoService } from './server-info';
-import { ShareService } from './share';
+import { SharedLinkService } from './shared-link';
 import { SmartInfoService } from './smart-info';
 import { StorageService } from './storage';
 import { StorageTemplateService } from './storage-template';
@@ -34,7 +34,7 @@ const providers: Provider[] = [
   PartnerService,
   SearchService,
   ServerInfoService,
-  ShareService,
+  SharedLinkService,
   SmartInfoService,
   StorageService,
   StorageTemplateService,

+ 1 - 1
server/libs/domain/src/index.ts

@@ -17,7 +17,7 @@ export * from './person';
 export * from './search';
 export * from './server-info';
 export * from './partner';
-export * from './share';
+export * from './shared-link';
 export * from './smart-info';
 export * from './storage';
 export * from './storage-template';

+ 0 - 127
server/libs/domain/src/share/share.service.spec.ts

@@ -1,127 +0,0 @@
-import { BadRequestException, ForbiddenException } from '@nestjs/common';
-import {
-  authStub,
-  newCryptoRepositoryMock,
-  newSharedLinkRepositoryMock,
-  sharedLinkResponseStub,
-  sharedLinkStub,
-} from '../../test';
-import { ICryptoRepository } from '../crypto';
-import { ShareService } from './share.service';
-import { ISharedLinkRepository } from './shared-link.repository';
-
-describe(ShareService.name, () => {
-  let sut: ShareService;
-  let cryptoMock: jest.Mocked<ICryptoRepository>;
-  let shareMock: jest.Mocked<ISharedLinkRepository>;
-
-  beforeEach(async () => {
-    cryptoMock = newCryptoRepositoryMock();
-    shareMock = newSharedLinkRepositoryMock();
-
-    sut = new ShareService(cryptoMock, shareMock);
-  });
-
-  it('should work', () => {
-    expect(sut).toBeDefined();
-  });
-
-  describe('getAll', () => {
-    it('should return all keys for a user', async () => {
-      shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
-      await expect(sut.getAll(authStub.user1)).resolves.toEqual([
-        sharedLinkResponseStub.expired,
-        sharedLinkResponseStub.valid,
-      ]);
-      expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
-    });
-  });
-
-  describe('getMine', () => {
-    it('should only work for a public user', async () => {
-      await expect(sut.getMine(authStub.admin)).rejects.toBeInstanceOf(ForbiddenException);
-      expect(shareMock.get).not.toHaveBeenCalled();
-    });
-
-    it('should return the key for the public user (auth dto)', async () => {
-      const authDto = authStub.adminSharedLink;
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
-      await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.valid);
-      expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
-    });
-  });
-
-  describe('get', () => {
-    it('should not work on a missing key', async () => {
-      shareMock.get.mockResolvedValue(null);
-      await expect(sut.getById(authStub.user1, sharedLinkStub.valid.id, true)).rejects.toBeInstanceOf(
-        BadRequestException,
-      );
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
-      expect(shareMock.remove).not.toHaveBeenCalled();
-    });
-
-    it('should get a key by id', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
-      await expect(sut.getById(authStub.user1, sharedLinkStub.valid.id, false)).resolves.toEqual(
-        sharedLinkResponseStub.valid,
-      );
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
-    });
-
-    it('should include exif', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.readonly);
-      await expect(sut.getById(authStub.user1, sharedLinkStub.readonly.id, true)).resolves.toEqual(
-        sharedLinkResponseStub.readonly,
-      );
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.readonly.id);
-    });
-
-    it('should exclude exif', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.readonly);
-      await expect(sut.getById(authStub.user1, sharedLinkStub.readonly.id, false)).resolves.toEqual(
-        sharedLinkResponseStub.readonlyNoExif,
-      );
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.readonly.id);
-    });
-  });
-
-  describe('remove', () => {
-    it('should not work on a missing key', async () => {
-      shareMock.get.mockResolvedValue(null);
-      await expect(sut.remove(authStub.user1, sharedLinkStub.valid.id)).rejects.toBeInstanceOf(BadRequestException);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
-      expect(shareMock.remove).not.toHaveBeenCalled();
-    });
-
-    it('should remove a key', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
-      await sut.remove(authStub.user1, sharedLinkStub.valid.id);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
-      expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
-    });
-  });
-
-  describe('edit', () => {
-    it('should not work on a missing key', async () => {
-      shareMock.get.mockResolvedValue(null);
-      await expect(sut.edit(authStub.user1, sharedLinkStub.valid.id, {})).rejects.toBeInstanceOf(BadRequestException);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
-      expect(shareMock.save).not.toHaveBeenCalled();
-    });
-
-    it('should edit a key', async () => {
-      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
-      shareMock.save.mockResolvedValue(sharedLinkStub.valid);
-      const dto = { allowDownload: false };
-      await sut.edit(authStub.user1, sharedLinkStub.valid.id, dto);
-      // await expect(sut.edit(authStub.user1, sharedLinkStub.valid.id, dto)).rejects.toBeInstanceOf(BadRequestException);
-      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
-      expect(shareMock.save).toHaveBeenCalledWith({
-        id: sharedLinkStub.valid.id,
-        userId: authStub.user1.id,
-        allowDownload: false,
-      });
-    });
-  });
-});

+ 0 - 60
server/libs/domain/src/share/share.service.ts

@@ -1,60 +0,0 @@
-import { BadRequestException, ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
-import { AuthUserDto } from '../auth';
-import { ICryptoRepository } from '../crypto';
-import { EditSharedLinkDto } from './dto';
-import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
-import { ShareCore } from './share.core';
-import { ISharedLinkRepository } from './shared-link.repository';
-
-@Injectable()
-export class ShareService {
-  readonly logger = new Logger(ShareService.name);
-  private shareCore: ShareCore;
-
-  constructor(
-    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
-    @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
-  ) {
-    this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
-  }
-
-  async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
-    const links = await this.shareCore.getAll(authUser.id);
-    return links.map(mapSharedLink);
-  }
-
-  async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
-    if (!authUser.isPublicUser || !authUser.sharedLinkId) {
-      throw new ForbiddenException();
-    }
-
-    let allowExif = true;
-    if (authUser.isShowExif != undefined) {
-      allowExif = authUser.isShowExif;
-    }
-
-    return this.getById(authUser, authUser.sharedLinkId, allowExif);
-  }
-
-  async getById(authUser: AuthUserDto, id: string, allowExif: boolean): Promise<SharedLinkResponseDto> {
-    const link = await this.shareCore.get(authUser.id, id);
-    if (!link) {
-      throw new BadRequestException('Shared link not found');
-    }
-
-    if (allowExif) {
-      return mapSharedLink(link);
-    } else {
-      return mapSharedLinkWithNoExif(link);
-    }
-  }
-
-  async remove(authUser: AuthUserDto, id: string): Promise<void> {
-    await this.shareCore.remove(authUser.id, id);
-  }
-
-  async edit(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) {
-    const link = await this.shareCore.save(authUser.id, id, dto);
-    return mapSharedLink(link);
-  }
-}

+ 0 - 0
server/libs/domain/src/share/dto/create-shared-link.dto.ts → server/libs/domain/src/shared-link/dto/create-shared-link.dto.ts


+ 0 - 0
server/libs/domain/src/share/dto/edit-shared-link.dto.ts → server/libs/domain/src/shared-link/dto/edit-shared-link.dto.ts


+ 0 - 0
server/libs/domain/src/share/dto/index.ts → server/libs/domain/src/shared-link/dto/index.ts


+ 2 - 2
server/libs/domain/src/share/index.ts → server/libs/domain/src/shared-link/index.ts

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

+ 0 - 0
server/libs/domain/src/share/response-dto/index.ts → server/libs/domain/src/shared-link/response-dto/index.ts


+ 0 - 0
server/libs/domain/src/share/response-dto/shared-link-response.dto.ts → server/libs/domain/src/shared-link/response-dto/shared-link-response.dto.ts


+ 7 - 32
server/libs/domain/src/share/share.core.ts → server/libs/domain/src/shared-link/shared-link.core.ts

@@ -5,19 +5,12 @@ import { ICryptoRepository } from '../crypto';
 import { CreateSharedLinkDto } from './dto';
 import { ISharedLinkRepository } from './shared-link.repository';
 
-export class ShareCore {
-  readonly logger = new Logger(ShareCore.name);
+export class SharedLinkCore {
+  readonly logger = new Logger(SharedLinkCore.name);
 
   constructor(private repository: ISharedLinkRepository, private cryptoRepository: ICryptoRepository) {}
 
-  getAll(userId: string): Promise<SharedLinkEntity[]> {
-    return this.repository.getAll(userId);
-  }
-
-  get(userId: string, id: string): Promise<SharedLinkEntity | null> {
-    return this.repository.get(userId, id);
-  }
-
+  // TODO: move to SharedLinkController/SharedLinkService
   create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
     return this.repository.create({
       key: Buffer.from(this.cryptoRepository.randomBytes(50)),
@@ -34,42 +27,24 @@ export class ShareCore {
     });
   }
 
-  async save(userId: string, id: string, entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
-    const link = await this.get(userId, id);
-    if (!link) {
-      throw new BadRequestException('Shared link not found');
-    }
-
-    return this.repository.save({ ...entity, userId, id });
-  }
-
-  async remove(userId: string, id: string): Promise<void> {
-    const link = await this.get(userId, id);
-    if (!link) {
-      throw new BadRequestException('Shared link not found');
-    }
-
-    await this.repository.remove(link);
-  }
-
   async addAssets(userId: string, id: string, assets: AssetEntity[]) {
-    const link = await this.get(userId, id);
+    const link = await this.repository.get(userId, id);
     if (!link) {
       throw new BadRequestException('Shared link not found');
     }
 
-    return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
+    return this.repository.update({ ...link, assets: [...link.assets, ...assets] });
   }
 
   async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
-    const link = await this.get(userId, id);
+    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.save({ ...link, assets: newAssets });
+    return this.repository.update({ ...link, assets: newAssets });
   }
 
   async hasAssetAccess(id: string, assetId: string): Promise<boolean> {

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

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

+ 103 - 0
server/libs/domain/src/shared-link/shared-link.service.spec.ts

@@ -0,0 +1,103 @@
+import { BadRequestException, ForbiddenException } from '@nestjs/common';
+import { authStub, newSharedLinkRepositoryMock, sharedLinkResponseStub, sharedLinkStub } from '../../test';
+import { SharedLinkService } from './shared-link.service';
+import { ISharedLinkRepository } from './shared-link.repository';
+
+describe(SharedLinkService.name, () => {
+  let sut: SharedLinkService;
+  let shareMock: jest.Mocked<ISharedLinkRepository>;
+
+  beforeEach(async () => {
+    shareMock = newSharedLinkRepositoryMock();
+
+    sut = new SharedLinkService(shareMock);
+  });
+
+  it('should work', () => {
+    expect(sut).toBeDefined();
+  });
+
+  describe('getAll', () => {
+    it('should return all shared links for a user', async () => {
+      shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
+      await expect(sut.getAll(authStub.user1)).resolves.toEqual([
+        sharedLinkResponseStub.expired,
+        sharedLinkResponseStub.valid,
+      ]);
+      expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
+    });
+  });
+
+  describe('getMine', () => {
+    it('should only work for a public user', async () => {
+      await expect(sut.getMine(authStub.admin)).rejects.toBeInstanceOf(ForbiddenException);
+      expect(shareMock.get).not.toHaveBeenCalled();
+    });
+
+    it('should return the shared link for the public user', async () => {
+      const authDto = authStub.adminSharedLink;
+      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.valid);
+      expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
+    });
+
+    it('should return not return exif', async () => {
+      const authDto = authStub.adminSharedLinkNoExif;
+      shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
+      await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoExif);
+      expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
+    });
+  });
+
+  describe('get', () => {
+    it('should throw an error for an invalid shared link', async () => {
+      shareMock.get.mockResolvedValue(null);
+      await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
+      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
+      expect(shareMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should get a shared link by id', async () => {
+      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
+      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
+    });
+  });
+
+  describe('update', () => {
+    it('should throw an error for an invalid shared link', async () => {
+      shareMock.get.mockResolvedValue(null);
+      await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
+      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
+      expect(shareMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should update a shared link', async () => {
+      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      shareMock.update.mockResolvedValue(sharedLinkStub.valid);
+      await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
+      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
+      expect(shareMock.update).toHaveBeenCalledWith({
+        id: sharedLinkStub.valid.id,
+        userId: authStub.user1.id,
+        allowDownload: false,
+      });
+    });
+  });
+
+  describe('remove', () => {
+    it('should throw an error for an invalid shared link', async () => {
+      shareMock.get.mockResolvedValue(null);
+      await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
+      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
+      expect(shareMock.update).not.toHaveBeenCalled();
+    });
+
+    it('should remove a key', async () => {
+      shareMock.get.mockResolvedValue(sharedLinkStub.valid);
+      await sut.remove(authStub.user1, sharedLinkStub.valid.id);
+      expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
+      expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
+    });
+  });
+});

+ 63 - 0
server/libs/domain/src/shared-link/shared-link.service.ts

@@ -0,0 +1,63 @@
+import { SharedLinkEntity } from '@app/infra/entities';
+import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
+import { AuthUserDto } from '../auth';
+import { EditSharedLinkDto } from './dto';
+import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
+import { ISharedLinkRepository } from './shared-link.repository';
+
+@Injectable()
+export class SharedLinkService {
+  constructor(@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository) {}
+
+  async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
+    return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
+  }
+
+  async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
+    const { sharedLinkId: id, isPublicUser, isShowExif } = authUser;
+
+    if (!isPublicUser || !id) {
+      throw new ForbiddenException();
+    }
+
+    const sharedLink = await this.findOrFail(authUser, id);
+
+    return this.map(sharedLink, { withExif: isShowExif ?? true });
+  }
+
+  async get(authUser: AuthUserDto, id: string): Promise<SharedLinkResponseDto> {
+    const sharedLink = await this.findOrFail(authUser, id);
+    return this.map(sharedLink, { withExif: true });
+  }
+
+  async update(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) {
+    await this.findOrFail(authUser, id);
+    const sharedLink = await this.repository.update({
+      id,
+      userId: authUser.id,
+      description: dto.description,
+      expiresAt: dto.expiresAt,
+      allowUpload: dto.allowUpload,
+      allowDownload: dto.allowDownload,
+      showExif: dto.showExif,
+    });
+    return this.map(sharedLink, { withExif: true });
+  }
+
+  async remove(authUser: AuthUserDto, id: string): Promise<void> {
+    const sharedLink = await this.findOrFail(authUser, id);
+    await this.repository.remove(sharedLink);
+  }
+
+  private async findOrFail(authUser: AuthUserDto, id: string) {
+    const sharedLink = await this.repository.get(authUser.id, id);
+    if (!sharedLink) {
+      throw new BadRequestException('Shared link not found');
+    }
+    return sharedLink;
+  }
+
+  private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
+    return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink);
+  }
+}

+ 13 - 3
server/libs/domain/test/fixtures.ts

@@ -71,6 +71,16 @@ export const authStub = {
     isShowExif: true,
     sharedLinkId: '123',
   }),
+  adminSharedLinkNoExif: Object.freeze<AuthUserDto>({
+    id: 'admin_id',
+    email: 'admin@test.com',
+    isAdmin: true,
+    isAllowUpload: true,
+    isAllowDownload: true,
+    isPublicUser: true,
+    isShowExif: false,
+    sharedLinkId: '123',
+  }),
   readonlySharedLink: Object.freeze<AuthUserDto>({
     id: 'admin_id',
     email: 'admin@test.com',
@@ -690,7 +700,7 @@ export const sharedLinkStub = {
     showExif: true,
     assets: [],
   } as SharedLinkEntity),
-  readonly: Object.freeze<SharedLinkEntity>({
+  readonlyNoExif: Object.freeze<SharedLinkEntity>({
     id: '123',
     userId: authStub.admin.id,
     user: userEntityStub.admin,
@@ -700,7 +710,7 @@ export const sharedLinkStub = {
     expiresAt: tomorrow,
     allowUpload: false,
     allowDownload: false,
-    showExif: true,
+    showExif: false,
     assets: [],
     album: {
       id: 'album-123',
@@ -834,7 +844,7 @@ export const sharedLinkResponseStub = {
     description: undefined,
     allowUpload: false,
     allowDownload: false,
-    showExif: true,
+    showExif: false,
     album: albumResponse,
     assets: [{ ...assetResponse, exifInfo: undefined }],
   }),

+ 1 - 1
server/libs/domain/test/shared-link.repository.mock.ts

@@ -7,7 +7,7 @@ export const newSharedLinkRepositoryMock = (): jest.Mocked<ISharedLinkRepository
     getByKey: jest.fn(),
     create: jest.fn(),
     remove: jest.fn(),
-    save: jest.fn(),
+    update: jest.fn(),
     hasAssetAccess: jest.fn(),
   };
 };

+ 29 - 28
server/libs/infra/src/repositories/shared-link.repository.ts

@@ -1,16 +1,12 @@
 import { ISharedLinkRepository } from '@app/domain';
-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Repository } from 'typeorm';
 import { SharedLinkEntity } from '../entities';
 
 @Injectable()
 export class SharedLinkRepository implements ISharedLinkRepository {
-  readonly logger = new Logger(SharedLinkRepository.name);
-  constructor(
-    @InjectRepository(SharedLinkEntity)
-    private readonly repository: Repository<SharedLinkEntity>,
-  ) {}
+  constructor(@InjectRepository(SharedLinkEntity) private repository: Repository<SharedLinkEntity>) {}
 
   get(userId: string, id: string): Promise<SharedLinkEntity | null> {
     return this.repository.findOne({
@@ -78,40 +74,45 @@ export class SharedLinkRepository implements ISharedLinkRepository {
     });
   }
 
-  create(entity: Omit<SharedLinkEntity, 'id'>): Promise<SharedLinkEntity> {
-    return this.repository.save(entity);
+  create(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
+    return this.save(entity);
   }
 
-  async remove(entity: SharedLinkEntity): Promise<void> {
-    await this.repository.remove(entity);
+  update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
+    return this.save(entity);
   }
 
-  async save(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
-    await this.repository.save(entity);
-    return this.repository.findOneOrFail({ where: { id: entity.id } });
+  async remove(entity: SharedLinkEntity): Promise<void> {
+    await this.repository.remove(entity);
   }
 
   async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
-    const count1 = await this.repository.count({
-      where: {
-        id,
-        assets: {
-          id: assetId,
+    return (
+      // album asset
+      (await this.repository.exist({
+        where: {
+          id,
+          album: {
+            assets: {
+              id: assetId,
+            },
+          },
         },
-      },
-    });
-
-    const count2 = await this.repository.count({
-      where: {
-        id,
-        album: {
+      })) ||
+      // individual asset
+      (await this.repository.exist({
+        where: {
+          id,
           assets: {
             id: assetId,
           },
         },
-      },
-    });
+      }))
+    );
+  }
 
-    return Boolean(count1 + count2);
+  private async save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
+    await this.repository.save(entity);
+    return this.repository.findOneOrFail({ where: { id: entity.id } });
   }
 }

+ 81 - 81
web/src/api/open-api/api.ts

@@ -9986,18 +9986,11 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
     return {
         /**
          * 
-         * @param {string} id 
-         * @param {EditSharedLinkDto} editSharedLinkDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        editSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'id' is not null or undefined
-            assertParamExists('editSharedLink', 'id', id)
-            // verify required parameter 'editSharedLinkDto' is not null or undefined
-            assertParamExists('editSharedLink', 'editSharedLinkDto', editSharedLinkDto)
-            const localVarPath = `/share/{id}`
-                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+        getAllSharedLinks: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/share`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -10005,7 +9998,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -10020,12 +10013,9 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
-            localVarHeaderParameter['Content-Type'] = 'application/json';
-
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration)
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -10034,11 +10024,12 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAllSharedLinks: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/share`;
+        getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/share/me`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -10059,6 +10050,10 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
+            if (key !== undefined) {
+                localVarQueryParameter['key'] = key;
+            }
+
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -10072,12 +10067,15 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
-         * @param {string} [key] 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/share/me`;
+        getSharedLinkById: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'id' is not null or undefined
+            assertParamExists('getSharedLinkById', 'id', id)
+            const localVarPath = `/share/{id}`
+                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -10098,10 +10096,6 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
-            if (key !== undefined) {
-                localVarQueryParameter['key'] = key;
-            }
-
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -10119,9 +10113,9 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getSharedLinkById: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        removeSharedLink: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'id' is not null or undefined
-            assertParamExists('getSharedLinkById', 'id', id)
+            assertParamExists('removeSharedLink', 'id', id)
             const localVarPath = `/share/{id}`
                 .replace(`{${"id"}}`, encodeURIComponent(String(id)));
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
@@ -10131,7 +10125,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -10158,12 +10152,15 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
         /**
          * 
          * @param {string} id 
+         * @param {EditSharedLinkDto} editSharedLinkDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        removeSharedLink: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        updateSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'id' is not null or undefined
-            assertParamExists('removeSharedLink', 'id', id)
+            assertParamExists('updateSharedLink', 'id', id)
+            // verify required parameter 'editSharedLinkDto' is not null or undefined
+            assertParamExists('updateSharedLink', 'editSharedLinkDto', editSharedLinkDto)
             const localVarPath = `/share/{id}`
                 .replace(`{${"id"}}`, encodeURIComponent(String(id)));
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
@@ -10173,7 +10170,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -10188,9 +10185,12 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration)
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -10207,17 +10207,6 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
 export const ShareApiFp = function(configuration?: Configuration) {
     const localVarAxiosParamCreator = ShareApiAxiosParamCreator(configuration)
     return {
-        /**
-         * 
-         * @param {string} id 
-         * @param {EditSharedLinkDto} editSharedLinkDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        async editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.editSharedLink(id, editSharedLinkDto, options);
-            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
-        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -10257,6 +10246,17 @@ export const ShareApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLink(id, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {string} id 
+         * @param {EditSharedLinkDto} editSharedLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSharedLink(id, editSharedLinkDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
     }
 };
 
@@ -10267,16 +10267,6 @@ export const ShareApiFp = function(configuration?: Configuration) {
 export const ShareApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     const localVarFp = ShareApiFp(configuration)
     return {
-        /**
-         * 
-         * @param {string} id 
-         * @param {EditSharedLinkDto} editSharedLinkDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
-            return localVarFp.editSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath));
-        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -10312,30 +10302,19 @@ export const ShareApiFactory = function (configuration?: Configuration, basePath
         removeSharedLink(id: string, options?: any): AxiosPromise<void> {
             return localVarFp.removeSharedLink(id, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {string} id 
+         * @param {EditSharedLinkDto} editSharedLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
+            return localVarFp.updateSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath));
+        },
     };
 };
 
-/**
- * Request parameters for editSharedLink operation in ShareApi.
- * @export
- * @interface ShareApiEditSharedLinkRequest
- */
-export interface ShareApiEditSharedLinkRequest {
-    /**
-     * 
-     * @type {string}
-     * @memberof ShareApiEditSharedLink
-     */
-    readonly id: string
-
-    /**
-     * 
-     * @type {EditSharedLinkDto}
-     * @memberof ShareApiEditSharedLink
-     */
-    readonly editSharedLinkDto: EditSharedLinkDto
-}
-
 /**
  * Request parameters for getMySharedLink operation in ShareApi.
  * @export
@@ -10379,23 +10358,33 @@ export interface ShareApiRemoveSharedLinkRequest {
 }
 
 /**
- * ShareApi - object-oriented interface
+ * Request parameters for updateSharedLink operation in ShareApi.
  * @export
- * @class ShareApi
- * @extends {BaseAPI}
+ * @interface ShareApiUpdateSharedLinkRequest
  */
-export class ShareApi extends BaseAPI {
+export interface ShareApiUpdateSharedLinkRequest {
     /**
      * 
-     * @param {ShareApiEditSharedLinkRequest} requestParameters Request parameters.
-     * @param {*} [options] Override http request option.
-     * @throws {RequiredError}
-     * @memberof ShareApi
+     * @type {string}
+     * @memberof ShareApiUpdateSharedLink
      */
-    public editSharedLink(requestParameters: ShareApiEditSharedLinkRequest, options?: AxiosRequestConfig) {
-        return ShareApiFp(this.configuration).editSharedLink(requestParameters.id, requestParameters.editSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
-    }
+    readonly id: string
 
+    /**
+     * 
+     * @type {EditSharedLinkDto}
+     * @memberof ShareApiUpdateSharedLink
+     */
+    readonly editSharedLinkDto: EditSharedLinkDto
+}
+
+/**
+ * ShareApi - object-oriented interface
+ * @export
+ * @class ShareApi
+ * @extends {BaseAPI}
+ */
+export class ShareApi extends BaseAPI {
     /**
      * 
      * @param {*} [options] Override http request option.
@@ -10438,6 +10427,17 @@ export class ShareApi extends BaseAPI {
     public removeSharedLink(requestParameters: ShareApiRemoveSharedLinkRequest, options?: AxiosRequestConfig) {
         return ShareApiFp(this.configuration).removeSharedLink(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
     }
+
+    /**
+     * 
+     * @param {ShareApiUpdateSharedLinkRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof ShareApi
+     */
+    public updateSharedLink(requestParameters: ShareApiUpdateSharedLinkRequest, options?: AxiosRequestConfig) {
+        return ShareApiFp(this.configuration).updateSharedLink(requestParameters.id, requestParameters.editSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
+    }
 }
 
 

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

@@ -137,7 +137,7 @@
 					? new Date(currentTime + expirationTime).toISOString()
 					: null;
 
-				await api.shareApi.editSharedLink({
+				await api.shareApi.updateSharedLink({
 					id: editingLink.id,
 					editSharedLinkDto: {
 						description,