diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 0d0d732762..6ec8dd5643 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 695db07bfb..1ae93da9ae 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AlbumApi.md b/mobile/openapi/doc/AlbumApi.md index 172cd14162..b4eba7916d 100644 Binary files a/mobile/openapi/doc/AlbumApi.md and b/mobile/openapi/doc/AlbumApi.md differ diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index b40916e1f3..d691591813 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/CreateAssetsShareLinkDto.md b/mobile/openapi/doc/CreateAssetsShareLinkDto.md deleted file mode 100644 index 59a6f02f38..0000000000 Binary files a/mobile/openapi/doc/CreateAssetsShareLinkDto.md and /dev/null differ diff --git a/mobile/openapi/doc/ShareApi.md b/mobile/openapi/doc/SharedLinkApi.md similarity index 52% rename from mobile/openapi/doc/ShareApi.md rename to mobile/openapi/doc/SharedLinkApi.md index 90dde98cb6..34b8e1e719 100644 Binary files a/mobile/openapi/doc/ShareApi.md and b/mobile/openapi/doc/SharedLinkApi.md differ diff --git a/mobile/openapi/doc/CreateAlbumShareLinkDto.md b/mobile/openapi/doc/SharedLinkCreateDto.md similarity index 54% rename from mobile/openapi/doc/CreateAlbumShareLinkDto.md rename to mobile/openapi/doc/SharedLinkCreateDto.md index 5223149329..fbed9ef71c 100644 Binary files a/mobile/openapi/doc/CreateAlbumShareLinkDto.md and b/mobile/openapi/doc/SharedLinkCreateDto.md differ diff --git a/mobile/openapi/doc/EditSharedLinkDto.md b/mobile/openapi/doc/SharedLinkEditDto.md similarity index 94% rename from mobile/openapi/doc/EditSharedLinkDto.md rename to mobile/openapi/doc/SharedLinkEditDto.md index 8097bb1183..5105726cbe 100644 Binary files a/mobile/openapi/doc/EditSharedLinkDto.md and b/mobile/openapi/doc/SharedLinkEditDto.md differ diff --git a/mobile/openapi/doc/SharedLinkResponseDto.md b/mobile/openapi/doc/SharedLinkResponseDto.md index 8ffe0787a8..f649807f4c 100644 Binary files a/mobile/openapi/doc/SharedLinkResponseDto.md and b/mobile/openapi/doc/SharedLinkResponseDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 393854e745..613084b09f 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/album_api.dart b/mobile/openapi/lib/api/album_api.dart index b17c838f5b..37490881d5 100644 Binary files a/mobile/openapi/lib/api/album_api.dart and b/mobile/openapi/lib/api/album_api.dart differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 188d239b10..9155c55b34 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/lib/api/share_api.dart b/mobile/openapi/lib/api/share_api.dart deleted file mode 100644 index 59dd1f890d..0000000000 Binary files a/mobile/openapi/lib/api/share_api.dart and /dev/null differ diff --git a/mobile/openapi/lib/api/shared_link_api.dart b/mobile/openapi/lib/api/shared_link_api.dart new file mode 100644 index 0000000000..029f7bc8a7 Binary files /dev/null and b/mobile/openapi/lib/api/shared_link_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index bf0e60bc7c..65f3bb544f 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/create_album_share_link_dto.dart b/mobile/openapi/lib/model/create_album_share_link_dto.dart deleted file mode 100644 index f296d80473..0000000000 Binary files a/mobile/openapi/lib/model/create_album_share_link_dto.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/create_assets_share_link_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart similarity index 53% rename from mobile/openapi/lib/model/create_assets_share_link_dto.dart rename to mobile/openapi/lib/model/shared_link_create_dto.dart index c8391c1db6..c8d1c547a5 100644 Binary files a/mobile/openapi/lib/model/create_assets_share_link_dto.dart and b/mobile/openapi/lib/model/shared_link_create_dto.dart differ diff --git a/mobile/openapi/lib/model/edit_shared_link_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart similarity index 82% rename from mobile/openapi/lib/model/edit_shared_link_dto.dart rename to mobile/openapi/lib/model/shared_link_edit_dto.dart index e6a38994fb..5f031ca792 100644 Binary files a/mobile/openapi/lib/model/edit_shared_link_dto.dart and b/mobile/openapi/lib/model/shared_link_edit_dto.dart differ diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index e7f2f9bf57..77b4a5016c 100644 Binary files a/mobile/openapi/lib/model/shared_link_response_dto.dart and b/mobile/openapi/lib/model/shared_link_response_dto.dart differ diff --git a/mobile/openapi/test/album_api_test.dart b/mobile/openapi/test/album_api_test.dart index 77f92590ee..28c93deb41 100644 Binary files a/mobile/openapi/test/album_api_test.dart and b/mobile/openapi/test/album_api_test.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 9a77c292b6..e404fd846b 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ diff --git a/mobile/openapi/test/create_album_share_link_dto_test.dart b/mobile/openapi/test/create_album_share_link_dto_test.dart deleted file mode 100644 index c9dfc63762..0000000000 Binary files a/mobile/openapi/test/create_album_share_link_dto_test.dart and /dev/null differ diff --git a/mobile/openapi/test/share_api_test.dart b/mobile/openapi/test/shared_link_api_test.dart similarity index 59% rename from mobile/openapi/test/share_api_test.dart rename to mobile/openapi/test/shared_link_api_test.dart index 4c715826ba..05843bad7a 100644 Binary files a/mobile/openapi/test/share_api_test.dart and b/mobile/openapi/test/shared_link_api_test.dart differ diff --git a/mobile/openapi/test/create_assets_share_link_dto_test.dart b/mobile/openapi/test/shared_link_create_dto_test.dart similarity index 67% rename from mobile/openapi/test/create_assets_share_link_dto_test.dart rename to mobile/openapi/test/shared_link_create_dto_test.dart index 832e269047..0a2a6a7f99 100644 Binary files a/mobile/openapi/test/create_assets_share_link_dto_test.dart and b/mobile/openapi/test/shared_link_create_dto_test.dart differ diff --git a/mobile/openapi/test/edit_shared_link_dto_test.dart b/mobile/openapi/test/shared_link_edit_dto_test.dart similarity index 88% rename from mobile/openapi/test/edit_shared_link_dto_test.dart rename to mobile/openapi/test/shared_link_edit_dto_test.dart index 1308ee71d2..575892f1e2 100644 Binary files a/mobile/openapi/test/edit_shared_link_dto_test.dart and b/mobile/openapi/test/shared_link_edit_dto_test.dart differ diff --git a/server/e2e/album.e2e-spec.ts b/server/e2e/album.e2e-spec.ts index 7f2396b275..c1a877e4c1 100644 --- a/server/e2e/album.e2e-spec.ts +++ b/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) { + const res = await request(app.getHttpServer()) + .post('/shared-link') + .send({ ...data, type: SharedLinkType.ALBUM }); expect(res.status).toEqual(201); return res.body as SharedLinkResponseDto; } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index e3de261584..cdf814eec9 100644 --- a/server/immich-openapi-specs.json +++ b/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", diff --git a/server/src/domain/access/access.repository.ts b/server/src/domain/access/access.repository.ts index 628647c5e9..f9949e46c8 100644 --- a/server/src/domain/access/access.repository.ts +++ b/server/src/domain/access/access.repository.ts @@ -2,8 +2,11 @@ export const IAccessRepository = 'IAccessRepository'; export interface IAccessRepository { hasPartnerAccess(userId: string, partnerId: string): Promise; + hasAlbumAssetAccess(userId: string, assetId: string): Promise; hasOwnerAssetAccess(userId: string, assetId: string): Promise; hasPartnerAssetAccess(userId: string, assetId: string): Promise; hasSharedLinkAssetAccess(userId: string, assetId: string): Promise; + + hasAlbumOwnerAccess(userId: string, albumId: string): Promise; } diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index ebc5ddefbc..87df987d9e 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/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 { + 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'); + } } diff --git a/server/src/domain/shared-link/dto/create-shared-link.dto.ts b/server/src/domain/shared-link/dto/create-shared-link.dto.ts deleted file mode 100644 index db82021c6a..0000000000 --- a/server/src/domain/shared-link/dto/create-shared-link.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/domain/shared-link/dto/edit-shared-link.dto.ts b/server/src/domain/shared-link/dto/edit-shared-link.dto.ts deleted file mode 100644 index f35f5ed9ac..0000000000 --- a/server/src/domain/shared-link/dto/edit-shared-link.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/domain/shared-link/dto/index.ts b/server/src/domain/shared-link/dto/index.ts deleted file mode 100644 index 8f29f0ca78..0000000000 --- a/server/src/domain/shared-link/dto/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './create-shared-link.dto'; -export * from './edit-shared-link.dto'; diff --git a/server/src/domain/shared-link/index.ts b/server/src/domain/shared-link/index.ts index d97d103301..78f9033464 100644 --- a/server/src/domain/shared-link/index.ts +++ b/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'; diff --git a/server/src/domain/shared-link/response-dto/index.ts b/server/src/domain/shared-link/response-dto/index.ts deleted file mode 100644 index 008ee5c1a0..0000000000 --- a/server/src/domain/shared-link/response-dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './shared-link-response.dto'; diff --git a/server/src/domain/shared-link/response-dto/shared-link-response.dto.ts b/server/src/domain/shared-link/shared-link-response.dto.ts similarity index 95% rename from server/src/domain/shared-link/response-dto/shared-link-response.dto.ts rename to server/src/domain/shared-link/shared-link-response.dto.ts index f4ccabcfe7..6c9832e091 100644 --- a/server/src/domain/shared-link/response-dto/shared-link-response.dto.ts +++ b/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; diff --git a/server/src/domain/shared-link/shared-link.core.ts b/server/src/domain/shared-link/shared-link.core.ts deleted file mode 100644 index c64256d795..0000000000 --- a/server/src/domain/shared-link/shared-link.core.ts +++ /dev/null @@ -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 { - 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 { - 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'); - } -} diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts new file mode 100644 index 0000000000..0695ad8c59 --- /dev/null +++ b/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; +} diff --git a/server/src/domain/shared-link/shared-link.repository.ts b/server/src/domain/shared-link/shared-link.repository.ts index 7b6dcb6fef..0f0255d0a3 100644 --- a/server/src/domain/shared-link/shared-link.repository.ts +++ b/server/src/domain/shared-link/shared-link.repository.ts @@ -6,7 +6,7 @@ export interface ISharedLinkRepository { getAll(userId: string): Promise; get(userId: string, id: string): Promise; getByKey(key: Buffer): Promise; - create(entity: Omit): Promise; + create(entity: Partial): Promise; update(entity: Partial): Promise; remove(entity: SharedLinkEntity): Promise; } diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index 1c269784c6..74fe479bfa 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/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; + let cryptoMock: jest.Mocked; let shareMock: jest.Mocked; 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: [] }); + }); + }); }); diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index e82e16b22d..2e30b629e6 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/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 { + getAll(authUser: AuthUserDto): Promise { 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 { + 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 { + 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 { + 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); } diff --git a/server/src/immich/api-v1/album/album.controller.ts b/server/src/immich/api-v1/album/album.controller.ts index e15a89e33f..5349f5d654 100644 --- a/server/src/immich/api-v1/album/album.controller.ts +++ b/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); - } } diff --git a/server/src/immich/api-v1/album/album.service.spec.ts b/server/src/immich/api-v1/album/album.service.spec.ts index 69b38021b6..4b5a74b5eb 100644 --- a/server/src/immich/api-v1/album/album.service.spec.ts +++ b/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; - let sharedLinkRepositoryMock: jest.Mocked; let downloadServiceMock: jest.Mocked>; - let cryptoMock: jest.Mocked; 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 () => { diff --git a/server/src/immich/api-v1/album/album.service.ts b/server/src/immich/api-v1/album/album.service.ts index 75df03a13d..7e5e551e0a 100644 --- a/server/src/immich/api-v1/album/album.service.ts +++ b/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 { - 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(); + } } } diff --git a/server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts b/server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts deleted file mode 100644 index eedf7a2070..0000000000 --- a/server/src/immich/api-v1/album/dto/create-album-shared-link.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index d57ab0a9e4..1d4228c28b 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/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 { return this.assetService.bulkUploadCheck(authUser, dto); } - - @Post('/shared-link') - createAssetsSharedLink( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: CreateAssetsShareLinkDto, - ): Promise { - return this.assetService.createAssetsSharedLink(authUser, dto); - } - - @SharedLinkRoute() - @Patch('/shared-link/add') - addAssetsToSharedLink( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: AddAssetsDto, - ): Promise { - return this.assetService.addAssetsToSharedLink(authUser, dto); - } - - @SharedLinkRoute() - @Patch('/shared-link/remove') - removeAssetsFromSharedLink( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: RemoveAssetsDto, - ): Promise { - return this.assetService.removeAssetsFromSharedLink(authUser, dto); - } } diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index fcd739c9a9..ddfc74463a 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/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; let assetRepositoryMock: jest.Mocked; let downloadServiceMock: jest.Mocked>; - let sharedLinkRepositoryMock: jest.Mocked; - let cryptoMock: jest.Mocked; let jobMock: jest.Mocked; let storageMock: jest.Mocked; @@ -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(); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 53d8922c7b..0234f10efc 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/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, + @InjectRepository(AssetEntity) private assetRepository: Repository, 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 { - 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 { - 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 { - 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) { diff --git a/server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts deleted file mode 100644 index 258e7009a9..0000000000 --- a/server/src/immich/api-v1/asset/dto/create-asset-shared-link.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts index 1ab8f505fd..aa0e533975 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/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 { return this.service.update(authUser, id, dto); } @@ -42,4 +55,24 @@ export class SharedLinkController { removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(authUser, id); } + + @SharedLinkRoute() + @Put(':id/assets') + addSharedLinkAssets( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetIdsDto, + ): Promise { + return this.service.addAssets(authUser, id, dto); + } + + @SharedLinkRoute() + @Delete(':id/assets') + removeSharedLinkAssets( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetIdsDto, + ): Promise { + return this.service.removeAssets(authUser, id, dto); + } } diff --git a/server/src/infra/entities/shared-link.entity.ts b/server/src/infra/entities/shared-link.entity.ts index 7bca8dddf4..e06635d6a6 100644 --- a/server/src/infra/entities/shared-link.entity.ts +++ b/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 { diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index c0d54e2396..95dfdadfb3 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -95,4 +95,13 @@ export class AccessRepository implements IAccessRepository { })) ); } + + hasAlbumOwnerAccess(userId: string, albumId: string): Promise { + return this.albumRepository.exist({ + where: { + id: albumId, + ownerId: userId, + }, + }); + } } diff --git a/server/test/fixtures.ts b/server/test/fixtures.ts index f60a0743a4..5003c45d53 100644 --- a/server/test/fixtures.ts +++ b/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({ @@ -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, diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 6bbf047176..055cc226e7 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -3,9 +3,12 @@ import { IAccessRepository } from '@app/domain'; export const newAccessRepositoryMock = (): jest.Mocked => { return { hasPartnerAccess: jest.fn(), + hasAlbumAssetAccess: jest.fn(), hasOwnerAssetAccess: jest.fn(), hasPartnerAssetAccess: jest.fn(), hasSharedLinkAssetAccess: jest.fn(), + + hasAlbumOwnerAccess: jest.fn(), }; }; diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 3361f5e187..2924d717d1 100644 --- a/web/src/api/api.ts +++ b/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); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 97a52ec8ec..41264aafc4 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -903,92 +903,6 @@ export interface CreateAlbumDto { */ 'assetIds'?: Array; } -/** - * - * @export - * @interface CreateAlbumShareLinkDto - */ -export interface CreateAlbumShareLinkDto { - /** - * - * @type {string} - * @memberof CreateAlbumShareLinkDto - */ - 'albumId': string; - /** - * - * @type {string} - * @memberof CreateAlbumShareLinkDto - */ - 'expiresAt'?: string; - /** - * - * @type {boolean} - * @memberof CreateAlbumShareLinkDto - */ - 'allowUpload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAlbumShareLinkDto - */ - 'allowDownload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAlbumShareLinkDto - */ - 'showExif'?: boolean; - /** - * - * @type {string} - * @memberof CreateAlbumShareLinkDto - */ - 'description'?: string; -} -/** - * - * @export - * @interface CreateAssetsShareLinkDto - */ -export interface CreateAssetsShareLinkDto { - /** - * - * @type {Array} - * @memberof CreateAssetsShareLinkDto - */ - 'assetIds': Array; - /** - * - * @type {string} - * @memberof CreateAssetsShareLinkDto - */ - 'expiresAt'?: string; - /** - * - * @type {boolean} - * @memberof CreateAssetsShareLinkDto - */ - 'allowUpload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAssetsShareLinkDto - */ - 'allowDownload'?: boolean; - /** - * - * @type {boolean} - * @memberof CreateAssetsShareLinkDto - */ - 'showExif'?: boolean; - /** - * - * @type {string} - * @memberof CreateAssetsShareLinkDto - */ - 'description'?: string; -} /** * * @export @@ -1201,43 +1115,6 @@ export interface DownloadFilesDto { */ 'assetIds': Array; } -/** - * - * @export - * @interface EditSharedLinkDto - */ -export interface EditSharedLinkDto { - /** - * - * @type {string} - * @memberof EditSharedLinkDto - */ - 'description'?: string; - /** - * - * @type {string} - * @memberof EditSharedLinkDto - */ - 'expiresAt'?: string | null; - /** - * - * @type {boolean} - * @memberof EditSharedLinkDto - */ - 'allowUpload'?: boolean; - /** - * - * @type {boolean} - * @memberof EditSharedLinkDto - */ - 'allowDownload'?: boolean; - /** - * - * @type {boolean} - * @memberof EditSharedLinkDto - */ - 'showExif'?: boolean; -} /** * * @export @@ -2122,6 +1999,100 @@ export interface ServerVersionReponseDto { */ 'patch': number; } +/** + * + * @export + * @interface SharedLinkCreateDto + */ +export interface SharedLinkCreateDto { + /** + * + * @type {SharedLinkType} + * @memberof SharedLinkCreateDto + */ + 'type': SharedLinkType; + /** + * + * @type {Array} + * @memberof SharedLinkCreateDto + */ + 'assetIds'?: Array; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'albumId'?: string; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'description'?: string; + /** + * + * @type {string} + * @memberof SharedLinkCreateDto + */ + 'expiresAt'?: string | null; + /** + * + * @type {boolean} + * @memberof SharedLinkCreateDto + */ + 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkCreateDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkCreateDto + */ + 'showExif'?: boolean; +} + + +/** + * + * @export + * @interface SharedLinkEditDto + */ +export interface SharedLinkEditDto { + /** + * + * @type {string} + * @memberof SharedLinkEditDto + */ + 'description'?: string; + /** + * + * @type {string} + * @memberof SharedLinkEditDto + */ + 'expiresAt'?: string | null; + /** + * + * @type {boolean} + * @memberof SharedLinkEditDto + */ + 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkEditDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkEditDto + */ + 'showExif'?: boolean; +} /** * * @export @@ -2145,7 +2116,7 @@ export interface SharedLinkResponseDto { * @type {string} * @memberof SharedLinkResponseDto */ - 'description'?: string; + 'description': string | null; /** * * @type {string} @@ -3542,50 +3513,6 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAlbumSharedLink: async (createAlbumShareLinkDto: CreateAlbumShareLinkDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'createAlbumShareLinkDto' is not null or undefined - assertParamExists('createAlbumSharedLink', 'createAlbumShareLinkDto', createAlbumShareLinkDto) - const localVarPath = `/album/create-shared-link`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, 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(createAlbumShareLinkDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {string} id @@ -4003,16 +3930,6 @@ export const AlbumApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbum(createAlbumDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbumSharedLink(createAlbumShareLinkDto, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {string} id @@ -4140,15 +4057,6 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath createAlbum(createAlbumDto: CreateAlbumDto, options?: any): AxiosPromise { return localVarFp.createAlbum(createAlbumDto, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: any): AxiosPromise { - return localVarFp.createAlbumSharedLink(createAlbumShareLinkDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {string} id @@ -4294,20 +4202,6 @@ export interface AlbumApiCreateAlbumRequest { readonly createAlbumDto: CreateAlbumDto } -/** - * Request parameters for createAlbumSharedLink operation in AlbumApi. - * @export - * @interface AlbumApiCreateAlbumSharedLinkRequest - */ -export interface AlbumApiCreateAlbumSharedLinkRequest { - /** - * - * @type {CreateAlbumShareLinkDto} - * @memberof AlbumApiCreateAlbumSharedLink - */ - readonly createAlbumShareLinkDto: CreateAlbumShareLinkDto -} - /** * Request parameters for deleteAlbum operation in AlbumApi. * @export @@ -4502,17 +4396,6 @@ export class AlbumApi extends BaseAPI { return AlbumApiFp(this.configuration).createAlbum(requestParameters.createAlbumDto, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AlbumApiCreateAlbumSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AlbumApi - */ - public createAlbumSharedLink(requestParameters: AlbumApiCreateAlbumSharedLinkRequest, options?: AxiosRequestConfig) { - return AlbumApiFp(this.configuration).createAlbumSharedLink(requestParameters.createAlbumShareLinkDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AlbumApiDeleteAlbumRequest} requestParameters Request parameters. @@ -4608,55 +4491,6 @@ export class AlbumApi extends BaseAPI { */ export const AssetApiAxiosParamCreator = function (configuration?: Configuration) { return { - /** - * - * @param {AddAssetsDto} addAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - addAssetsToSharedLink: async (addAssetsDto: AddAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'addAssetsDto' is not null or undefined - assertParamExists('addAssetsToSharedLink', 'addAssetsDto', addAssetsDto) - const localVarPath = `/asset/shared-link/add`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * Checks if assets exist by checksums * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto @@ -4794,50 +4628,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAssetsSharedLink: async (createAssetsShareLinkDto: CreateAssetsShareLinkDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'createAssetsShareLinkDto' is not null or undefined - assertParamExists('createAssetsSharedLink', 'createAssetsShareLinkDto', createAssetsShareLinkDto) - const localVarPath = `/asset/shared-link`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, 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(createAssetsShareLinkDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {DeleteAssetDto} deleteAssetDto @@ -5622,55 +5412,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {RemoveAssetsDto} removeAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - removeAssetsFromSharedLink: async (removeAssetsDto: RemoveAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'removeAssetsDto' is not null or undefined - assertParamExists('removeAssetsFromSharedLink', 'removeAssetsDto', removeAssetsDto) - const localVarPath = `/asset/shared-link/remove`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {SearchAssetDto} searchAssetDto @@ -5958,17 +5699,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = AssetApiAxiosParamCreator(configuration) return { - /** - * - * @param {AddAssetsDto} addAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async addAssetsToSharedLink(addAssetsDto: AddAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, key, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * Checks if assets exist by checksums * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto @@ -6000,16 +5730,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.createAssetsSharedLink(createAssetsShareLinkDto, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {DeleteAssetDto} deleteAssetDto @@ -6189,17 +5909,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {RemoveAssetsDto} removeAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetsFromSharedLink(removeAssetsDto, key, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {SearchAssetDto} searchAssetDto @@ -6267,16 +5976,6 @@ export const AssetApiFp = function(configuration?: Configuration) { export const AssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = AssetApiFp(configuration) return { - /** - * - * @param {AddAssetsDto} addAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - addAssetsToSharedLink(addAssetsDto: AddAssetsDto, key?: string, options?: any): AxiosPromise { - return localVarFp.addAssetsToSharedLink(addAssetsDto, key, options).then((request) => request(axios, basePath)); - }, /** * Checks if assets exist by checksums * @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto @@ -6305,15 +6004,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise { return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: any): AxiosPromise { - return localVarFp.createAssetsSharedLink(createAssetsShareLinkDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {DeleteAssetDto} deleteAssetDto @@ -6476,16 +6166,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise> { return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {RemoveAssetsDto} removeAssetsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, key?: string, options?: any): AxiosPromise { - return localVarFp.removeAssetsFromSharedLink(removeAssetsDto, key, options).then((request) => request(axios, basePath)); - }, /** * * @param {SearchAssetDto} searchAssetDto @@ -6542,27 +6222,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath }; }; -/** - * Request parameters for addAssetsToSharedLink operation in AssetApi. - * @export - * @interface AssetApiAddAssetsToSharedLinkRequest - */ -export interface AssetApiAddAssetsToSharedLinkRequest { - /** - * - * @type {AddAssetsDto} - * @memberof AssetApiAddAssetsToSharedLink - */ - readonly addAssetsDto: AddAssetsDto - - /** - * - * @type {string} - * @memberof AssetApiAddAssetsToSharedLink - */ - readonly key?: string -} - /** * Request parameters for bulkUploadCheck operation in AssetApi. * @export @@ -6612,20 +6271,6 @@ export interface AssetApiCheckExistingAssetsRequest { readonly checkExistingAssetsDto: CheckExistingAssetsDto } -/** - * Request parameters for createAssetsSharedLink operation in AssetApi. - * @export - * @interface AssetApiCreateAssetsSharedLinkRequest - */ -export interface AssetApiCreateAssetsSharedLinkRequest { - /** - * - * @type {CreateAssetsShareLinkDto} - * @memberof AssetApiCreateAssetsSharedLink - */ - readonly createAssetsShareLinkDto: CreateAssetsShareLinkDto -} - /** * Request parameters for deleteAsset operation in AssetApi. * @export @@ -6892,27 +6537,6 @@ export interface AssetApiGetUserAssetsByDeviceIdRequest { readonly deviceId: string } -/** - * Request parameters for removeAssetsFromSharedLink operation in AssetApi. - * @export - * @interface AssetApiRemoveAssetsFromSharedLinkRequest - */ -export interface AssetApiRemoveAssetsFromSharedLinkRequest { - /** - * - * @type {RemoveAssetsDto} - * @memberof AssetApiRemoveAssetsFromSharedLink - */ - readonly removeAssetsDto: RemoveAssetsDto - - /** - * - * @type {string} - * @memberof AssetApiRemoveAssetsFromSharedLink - */ - readonly key?: string -} - /** * Request parameters for searchAsset operation in AssetApi. * @export @@ -7095,17 +6719,6 @@ export interface AssetApiUploadFileRequest { * @extends {BaseAPI} */ export class AssetApi extends BaseAPI { - /** - * - * @param {AssetApiAddAssetsToSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public addAssetsToSharedLink(requestParameters: AssetApiAddAssetsToSharedLinkRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).addAssetsToSharedLink(requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - /** * Checks if assets exist by checksums * @param {AssetApiBulkUploadCheckRequest} requestParameters Request parameters. @@ -7139,17 +6752,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).checkExistingAssets(requestParameters.checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiCreateAssetsSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public createAssetsSharedLink(requestParameters: AssetApiCreateAssetsSharedLinkRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).createAssetsSharedLink(requestParameters.createAssetsShareLinkDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiDeleteAssetRequest} requestParameters Request parameters. @@ -7332,17 +6934,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiRemoveAssetsFromSharedLinkRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public removeAssetsFromSharedLink(requestParameters: AssetApiRemoveAssetsFromSharedLinkRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).removeAssetsFromSharedLink(requestParameters.removeAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiSearchAssetRequest} requestParameters Request parameters. @@ -10121,18 +9712,115 @@ export class ServerInfoApi extends BaseAPI { /** - * ShareApi - axios parameter creator + * SharedLinkApi - axios parameter creator * @export */ -export const ShareApiAxiosParamCreator = function (configuration?: Configuration) { +export const SharedLinkApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + addSharedLinkAssets: async (id: string, assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('addSharedLinkAssets', 'id', id) + // verify required parameter 'assetIdsDto' is not null or undefined + assertParamExists('addSharedLinkAssets', 'assetIdsDto', assetIdsDto) + const localVarPath = `/shared-link/{id}/assets` + .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; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (key !== undefined) { + localVarQueryParameter['key'] = key; + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {SharedLinkCreateDto} sharedLinkCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createSharedLink: async (sharedLinkCreateDto: SharedLinkCreateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'sharedLinkCreateDto' is not null or undefined + assertParamExists('createSharedLink', 'sharedLinkCreateDto', sharedLinkCreateDto) + const localVarPath = `/shared-link`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, 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(sharedLinkCreateDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAllSharedLinks: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/share`; + const localVarPath = `/shared-link`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -10171,7 +9859,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration * @throws {RequiredError} */ getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/share/me`; + const localVarPath = `/shared-link/me`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -10216,7 +9904,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration getSharedLinkById: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('getSharedLinkById', 'id', id) - const localVarPath = `/share/{id}` + const localVarPath = `/shared-link/{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); @@ -10258,7 +9946,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration removeSharedLink: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('removeSharedLink', 'id', id) - const localVarPath = `/share/{id}` + const localVarPath = `/shared-link/{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); @@ -10294,16 +9982,69 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration /** * * @param {string} id - * @param {EditSharedLinkDto} editSharedLinkDto + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise => { + removeSharedLinkAssets: async (id: string, assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('removeSharedLinkAssets', 'id', id) + // verify required parameter 'assetIdsDto' is not null or undefined + assertParamExists('removeSharedLinkAssets', 'assetIdsDto', assetIdsDto) + const localVarPath = `/shared-link/{id}/assets` + .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; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (key !== undefined) { + localVarQueryParameter['key'] = key; + } + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {SharedLinkEditDto} sharedLinkEditDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSharedLink: async (id: string, sharedLinkEditDto: SharedLinkEditDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('updateSharedLink', 'id', id) - // verify required parameter 'editSharedLinkDto' is not null or undefined - assertParamExists('updateSharedLink', 'editSharedLinkDto', editSharedLinkDto) - const localVarPath = `/share/{id}` + // verify required parameter 'sharedLinkEditDto' is not null or undefined + assertParamExists('updateSharedLink', 'sharedLinkEditDto', sharedLinkEditDto) + const localVarPath = `/shared-link/{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); @@ -10332,7 +10073,7 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration) + localVarRequestOptions.data = serializeDataIfNeeded(sharedLinkEditDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -10343,12 +10084,34 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration }; /** - * ShareApi - functional programming interface + * SharedLinkApi - functional programming interface * @export */ -export const ShareApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = ShareApiAxiosParamCreator(configuration) +export const SharedLinkApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = SharedLinkApiAxiosParamCreator(configuration) return { + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async addSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.addSharedLinkAssets(id, assetIdsDto, key, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {SharedLinkCreateDto} sharedLinkCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createSharedLink(sharedLinkCreateDto: SharedLinkCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createSharedLink(sharedLinkCreateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -10391,24 +10154,56 @@ export const ShareApiFp = function(configuration?: Configuration) { /** * * @param {string} id - * @param {EditSharedLinkDto} editSharedLinkDto + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updateSharedLink(id, editSharedLinkDto, options); + async removeSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLinkAssets(id, assetIdsDto, key, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {SharedLinkEditDto} sharedLinkEditDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateSharedLink(id: string, sharedLinkEditDto: SharedLinkEditDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateSharedLink(id, sharedLinkEditDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } }; /** - * ShareApi - factory interface + * SharedLinkApi - factory interface * @export */ -export const ShareApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = ShareApiFp(configuration) +export const SharedLinkApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = SharedLinkApiFp(configuration) return { + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + addSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: any): AxiosPromise> { + return localVarFp.addSharedLinkAssets(id, assetIdsDto, key, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {SharedLinkCreateDto} sharedLinkCreateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createSharedLink(sharedLinkCreateDto: SharedLinkCreateDto, options?: any): AxiosPromise { + return localVarFp.createSharedLink(sharedLinkCreateDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -10447,138 +10242,252 @@ export const ShareApiFactory = function (configuration?: Configuration, basePath /** * * @param {string} id - * @param {EditSharedLinkDto} editSharedLinkDto + * @param {AssetIdsDto} assetIdsDto + * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise { - return localVarFp.updateSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath)); + removeSharedLinkAssets(id: string, assetIdsDto: AssetIdsDto, key?: string, options?: any): AxiosPromise> { + return localVarFp.removeSharedLinkAssets(id, assetIdsDto, key, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {SharedLinkEditDto} sharedLinkEditDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSharedLink(id: string, sharedLinkEditDto: SharedLinkEditDto, options?: any): AxiosPromise { + return localVarFp.updateSharedLink(id, sharedLinkEditDto, options).then((request) => request(axios, basePath)); }, }; }; /** - * Request parameters for getMySharedLink operation in ShareApi. + * Request parameters for addSharedLinkAssets operation in SharedLinkApi. * @export - * @interface ShareApiGetMySharedLinkRequest + * @interface SharedLinkApiAddSharedLinkAssetsRequest */ -export interface ShareApiGetMySharedLinkRequest { +export interface SharedLinkApiAddSharedLinkAssetsRequest { /** * * @type {string} - * @memberof ShareApiGetMySharedLink + * @memberof SharedLinkApiAddSharedLinkAssets + */ + readonly id: string + + /** + * + * @type {AssetIdsDto} + * @memberof SharedLinkApiAddSharedLinkAssets + */ + readonly assetIdsDto: AssetIdsDto + + /** + * + * @type {string} + * @memberof SharedLinkApiAddSharedLinkAssets */ readonly key?: string } /** - * Request parameters for getSharedLinkById operation in ShareApi. + * Request parameters for createSharedLink operation in SharedLinkApi. * @export - * @interface ShareApiGetSharedLinkByIdRequest + * @interface SharedLinkApiCreateSharedLinkRequest */ -export interface ShareApiGetSharedLinkByIdRequest { +export interface SharedLinkApiCreateSharedLinkRequest { + /** + * + * @type {SharedLinkCreateDto} + * @memberof SharedLinkApiCreateSharedLink + */ + readonly sharedLinkCreateDto: SharedLinkCreateDto +} + +/** + * Request parameters for getMySharedLink operation in SharedLinkApi. + * @export + * @interface SharedLinkApiGetMySharedLinkRequest + */ +export interface SharedLinkApiGetMySharedLinkRequest { /** * * @type {string} - * @memberof ShareApiGetSharedLinkById + * @memberof SharedLinkApiGetMySharedLink + */ + readonly key?: string +} + +/** + * Request parameters for getSharedLinkById operation in SharedLinkApi. + * @export + * @interface SharedLinkApiGetSharedLinkByIdRequest + */ +export interface SharedLinkApiGetSharedLinkByIdRequest { + /** + * + * @type {string} + * @memberof SharedLinkApiGetSharedLinkById */ readonly id: string } /** - * Request parameters for removeSharedLink operation in ShareApi. + * Request parameters for removeSharedLink operation in SharedLinkApi. * @export - * @interface ShareApiRemoveSharedLinkRequest + * @interface SharedLinkApiRemoveSharedLinkRequest */ -export interface ShareApiRemoveSharedLinkRequest { +export interface SharedLinkApiRemoveSharedLinkRequest { /** * * @type {string} - * @memberof ShareApiRemoveSharedLink + * @memberof SharedLinkApiRemoveSharedLink */ readonly id: string } /** - * Request parameters for updateSharedLink operation in ShareApi. + * Request parameters for removeSharedLinkAssets operation in SharedLinkApi. * @export - * @interface ShareApiUpdateSharedLinkRequest + * @interface SharedLinkApiRemoveSharedLinkAssetsRequest */ -export interface ShareApiUpdateSharedLinkRequest { +export interface SharedLinkApiRemoveSharedLinkAssetsRequest { /** * * @type {string} - * @memberof ShareApiUpdateSharedLink + * @memberof SharedLinkApiRemoveSharedLinkAssets */ readonly id: string /** * - * @type {EditSharedLinkDto} - * @memberof ShareApiUpdateSharedLink + * @type {AssetIdsDto} + * @memberof SharedLinkApiRemoveSharedLinkAssets */ - readonly editSharedLinkDto: EditSharedLinkDto + readonly assetIdsDto: AssetIdsDto + + /** + * + * @type {string} + * @memberof SharedLinkApiRemoveSharedLinkAssets + */ + readonly key?: string } /** - * ShareApi - object-oriented interface + * Request parameters for updateSharedLink operation in SharedLinkApi. * @export - * @class ShareApi + * @interface SharedLinkApiUpdateSharedLinkRequest + */ +export interface SharedLinkApiUpdateSharedLinkRequest { + /** + * + * @type {string} + * @memberof SharedLinkApiUpdateSharedLink + */ + readonly id: string + + /** + * + * @type {SharedLinkEditDto} + * @memberof SharedLinkApiUpdateSharedLink + */ + readonly sharedLinkEditDto: SharedLinkEditDto +} + +/** + * SharedLinkApi - object-oriented interface + * @export + * @class SharedLinkApi * @extends {BaseAPI} */ -export class ShareApi extends BaseAPI { +export class SharedLinkApi extends BaseAPI { + /** + * + * @param {SharedLinkApiAddSharedLinkAssetsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SharedLinkApi + */ + public addSharedLinkAssets(requestParameters: SharedLinkApiAddSharedLinkAssetsRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).addSharedLinkAssets(requestParameters.id, requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SharedLinkApiCreateSharedLinkRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SharedLinkApi + */ + public createSharedLink(requestParameters: SharedLinkApiCreateSharedLinkRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).createSharedLink(requestParameters.sharedLinkCreateDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ public getAllSharedLinks(options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).getAllSharedLinks(options).then((request) => request(this.axios, this.basePath)); + return SharedLinkApiFp(this.configuration).getAllSharedLinks(options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiGetMySharedLinkRequest} requestParameters Request parameters. + * @param {SharedLinkApiGetMySharedLinkRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public getMySharedLink(requestParameters: ShareApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiGetSharedLinkByIdRequest} requestParameters Request parameters. + * @param {SharedLinkApiGetSharedLinkByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public getSharedLinkById(requestParameters: ShareApiGetSharedLinkByIdRequest, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).getSharedLinkById(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + public getSharedLinkById(requestParameters: SharedLinkApiGetSharedLinkByIdRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).getSharedLinkById(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiRemoveSharedLinkRequest} requestParameters Request parameters. + * @param {SharedLinkApiRemoveSharedLinkRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public removeSharedLink(requestParameters: ShareApiRemoveSharedLinkRequest, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).removeSharedLink(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + public removeSharedLink(requestParameters: SharedLinkApiRemoveSharedLinkRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).removeSharedLink(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {ShareApiUpdateSharedLinkRequest} requestParameters Request parameters. + * @param {SharedLinkApiRemoveSharedLinkAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} - * @memberof ShareApi + * @memberof SharedLinkApi */ - public updateSharedLink(requestParameters: ShareApiUpdateSharedLinkRequest, options?: AxiosRequestConfig) { - return ShareApiFp(this.configuration).updateSharedLink(requestParameters.id, requestParameters.editSharedLinkDto, options).then((request) => request(this.axios, this.basePath)); + public removeSharedLinkAssets(requestParameters: SharedLinkApiRemoveSharedLinkAssetsRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).removeSharedLinkAssets(requestParameters.id, requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SharedLinkApiUpdateSharedLinkRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SharedLinkApi + */ + public updateSharedLink(requestParameters: SharedLinkApiUpdateSharedLinkRequest, options?: AxiosRequestConfig) { + return SharedLinkApiFp(this.configuration).updateSharedLink(requestParameters.id, requestParameters.sharedLinkEditDto, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 452443c1bf..207b9e440f 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/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); }; diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index b2088e1437..4dc9642393 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -1,34 +1,65 @@ (removing = true)} logo={DeleteOutline} /> + +{#if removing} + handleRemove()} + on:cancel={() => (removing = false)} + /> +{/if} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 5ba129705e..8657475842 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/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'); } };
{#if isMultiSelectionMode} - + (selectedAssets = new Set())}> {#if isOwned} - + {/if} {:else} diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 32fb22d083..236e7e40f6 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/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; - notificationController.show({ - type: NotificationType.Info, - message: 'Edited' - }); + await api.sharedLinkApi.updateSharedLink({ + id: editingLink.id, + sharedLinkEditDto: { + description, + expiresAt: shouldChangeExpirationTime ? expirationDate : undefined, + allowUpload: allowUpload, + allowDownload: allowDownload, + showExif: showExif + } + }); - dispatch('close'); - } catch (e) { - handleError(e, 'Failed to edit shared link'); - } + notificationController.show({ + type: NotificationType.Info, + message: 'Edited' + }); + + dispatch('close'); + } catch (e) { + handleError(e, 'Failed to edit shared link'); } }; @@ -212,15 +197,15 @@
- +
- +
- +
@@ -248,7 +233,7 @@
- {#if !isShowSharedLink} + {#if !sharedLink} {#if editingLink}
@@ -258,9 +243,7 @@
{/if} - {/if} - - {#if isShowSharedLink} + {:else}
diff --git a/web/src/routes/(user)/share/[key]/+page.server.ts b/web/src/routes/(user)/share/[key]/+page.server.ts index 080a6e3914..c2863f093f 100644 --- a/web/src/routes/(user)/share/[key]/+page.server.ts +++ b/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; diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index 7d2aab0577..aee6a98639 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -1,7 +1,6 @@ - goto('/sharing')}> + goto(AppRoute.SHARING)}> Shared links @@ -86,16 +77,16 @@ {#each sharedLinks as link (link.id)} 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}
{/if}
-{#if showEditForm} +{#if editSharedLink} {/if} + +{#if deleteLinkId} + handleDeleteLink()} + on:cancel={() => (deleteLinkId = null)} + /> +{/if}