diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index 00c455d6c..1d069d0f5 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -243,9 +243,21 @@ describe('/shared-links', () => { }); it('should get data for correct password protected link', async () => { + const response = await request(app) + .post('/shared-links/login') + .send({ password: 'foo' }) + .query({ key: linkWithPassword.key }); + + expect(response.status).toBe(201); + + const cookies = response.get('Set-Cookie') ?? []; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toContain('immich_shared_link_token'); + const { status, body } = await request(app) .get('/shared-links/me') - .query({ key: linkWithPassword.key, password: 'foo' }); + .query({ key: linkWithPassword.key }) + .set('Cookie', cookies); expect(status).toBe(200); expect(body).toEqual( diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e2db95b9e..000125ac7 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -27,11 +27,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, slug: slug, ); + Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto,) async { + final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -104,11 +89,7 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/assets'; @@ -119,13 +100,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -147,12 +121,8 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, key: key, slug: slug, ); + Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async { + final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 084662ace..475044228 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -27,11 +27,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class SharedLinksApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { - final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, ); + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -235,14 +220,8 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async { + Future getMySharedLinkWithHttpInfo({ String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -256,15 +235,9 @@ class SharedLinksApi { if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } - if (password != null) { - queryParams.addAll(_queryParams('', 'password', password)); - } if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (token != null) { - queryParams.addAll(_queryParams('', 'token', token)); - } const contentTypes = []; @@ -288,15 +261,9 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLink({ String? key, String? password, String? slug, String? token, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, ); + Future getMySharedLink({ String? key, String? slug, }) async { + final response = await getMySharedLinkWithHttpInfo( key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4ab8a1d58..bc855058c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1768,24 +1768,7 @@ "put": { "description": "Send a list of asset IDs and album IDs to add each asset to each album.", "operationId": "addAssetsToAlbums", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "requestBody": { "content": { "application/json": { @@ -2184,22 +2167,6 @@ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } } ], "requestBody": { @@ -11427,15 +11394,6 @@ "type": "string" } }, - { - "name": "password", - "required": false, - "in": "query", - "description": "Link password", - "schema": { - "type": "string" - } - }, { "name": "slug", "required": false, @@ -11443,15 +11401,6 @@ "schema": { "type": "string" } - }, - { - "name": "token", - "required": false, - "in": "query", - "description": "Access token", - "schema": { - "type": "string" - } } ], "responses": { @@ -11766,22 +11715,6 @@ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } } ], "requestBody": { @@ -11838,6 +11771,7 @@ "state": "Stable" } ], + "x-immich-permission": "sharedLink.update", "x-immich-state": "Stable" } }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 6bb17f683..520948b82 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -3684,18 +3684,13 @@ export function createAlbum({ createAlbumDto }: { /** * Add assets to albums */ -export function addAssetsToAlbums({ key, slug, albumsAddAssetsDto }: { - key?: string; - slug?: string; +export function addAssetsToAlbums({ albumsAddAssetsDto }: { albumsAddAssetsDto: AlbumsAddAssetsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumsAddAssetsResponseDto; - }>(`/albums/assets${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.json({ + }>("/albums/assets", oazapfts.json({ ...opts, method: "PUT", body: albumsAddAssetsDto @@ -3778,19 +3773,14 @@ export function removeAssetFromAlbum({ id, bulkIdsDto }: { /** * Add assets to an album */ -export function addAssetsToAlbum({ id, key, slug, bulkIdsDto }: { +export function addAssetsToAlbum({ id, bulkIdsDto }: { id: string; - key?: string; - slug?: string; bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: BulkIdResponseDto[]; - }>(`/albums/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.json({ + }>(`/albums/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "PUT", body: bulkIdsDto @@ -5906,20 +5896,16 @@ export function sharedLinkLogin({ key, slug, sharedLinkLoginDto }: { /** * Retrieve current shared link */ -export function getMySharedLink({ key, password, slug, token }: { +export function getMySharedLink({ key, slug }: { key?: string; - password?: string; slug?: string; - token?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: SharedLinkResponseDto; }>(`/shared-links/me${QS.query(QS.explode({ key, - password, - slug, - token + slug }))}`, { ...opts })); @@ -5983,19 +5969,14 @@ export function removeSharedLinkAssets({ id, assetIdsDto }: { /** * Add assets to a shared link */ -export function addSharedLinkAssets({ id, key, slug, assetIdsDto }: { +export function addSharedLinkAssets({ id, assetIdsDto }: { id: string; - key?: string; - slug?: string; assetIdsDto: AssetIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AssetIdsResponseDto[]; - }>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.json({ + }>(`/shared-links/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "PUT", body: assetIdsDto diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index dad70257a..df326a395 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -103,7 +103,7 @@ export class AlbumController { } @Put(':id/assets') - @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) + @Authenticated({ permission: Permission.AlbumAssetCreate }) @Endpoint({ summary: 'Add assets to an album', description: 'Add multiple assets to a specific album by its ID.', @@ -118,7 +118,7 @@ export class AlbumController { } @Put('assets') - @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) + @Authenticated({ permission: Permission.AlbumAssetCreate }) @Endpoint({ summary: 'Add assets to albums', description: 'Send a list of asset IDs and album IDs to add each asset to each album.', diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index c7ba589a9..b3a8dd9f5 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -23,7 +23,6 @@ import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkLoginDto, - SharedLinkPasswordDto, SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; @@ -96,21 +95,7 @@ export class SharedLinkController { description: 'Retrieve the current shared link associated with authentication method.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - async getMySharedLink( - @Auth() auth: AuthDto, - @Query() dto: SharedLinkPasswordDto, - @Req() req: Request, - @Res({ passthrough: true }) res: Response, - @GetLoginDetails() loginDetails: LoginDetails, - ): Promise { - if (dto.password) { - this.logger.deprecate( - 'Passing shared link password via query parameters is deprecated and will be removed in the next major release. Please use POST /shared-links/login instead.', - ); - - return this.sharedLinkLogin(auth, { password: dto.password }, req, res, loginDetails); - } - + getMySharedLink(@Auth() auth: AuthDto, @Req() req: Request): Promise { return this.service.getMine(auth, getAuthTokens(req.cookies)); } @@ -164,7 +149,7 @@ export class SharedLinkController { } @Put(':id/assets') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.SharedLinkUpdate }) @Endpoint({ summary: 'Add assets to a shared link', description: diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 7dcec034d..95f52dd24 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -57,13 +57,6 @@ const SharedLinkLoginSchema = z }) .meta({ id: 'SharedLinkLoginDto' }); -const SharedLinkPasswordSchema = z - .object({ - password: z.string().optional().describe('Link password'), - token: z.string().optional().describe('Access token'), - }) - .meta({ id: 'SharedLinkPasswordDto' }); - const SharedLinkResponseSchema = z .object({ id: z.string().describe('Shared link ID'), @@ -96,7 +89,6 @@ export class SharedLinkSearchDto extends createZodDto(SharedLinkSearchSchema) {} export class SharedLinkCreateDto extends createZodDto(SharedLinkCreateSchema) {} export class SharedLinkEditDto extends createZodDto(SharedLinkEditSchema) {} export class SharedLinkLoginDto extends createZodDto(SharedLinkLoginSchema) {} -export class SharedLinkPasswordDto extends createZodDto(SharedLinkPasswordSchema) {} export class SharedLinkResponseDto extends createZodDto(SharedLinkResponseSchema) {} export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 47646d0c6..d24786fbb 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -717,31 +717,6 @@ describe(AlbumService.name, () => { expect(mocks.album.update).not.toHaveBeenCalled(); }); - it('should allow a shared link user to add assets', async () => { - const album = AlbumFactory.create(); - const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; - const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build(); - mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); - mocks.album.getById.mockResolvedValue(getForAlbum(album)); - mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - - await expect(sut.addAssets(auth, album.id, { ids: [asset1.id, asset2.id, asset3.id] })).resolves.toEqual([ - { success: true, id: asset1.id }, - { success: true, id: asset2.id }, - { success: true, id: asset3.id }, - ]); - - expect(mocks.album.update).toHaveBeenCalledWith(album.id, { - id: album.id, - updatedAt: expect.any(Date), - albumThumbnailAssetId: asset1.id, - }); - expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]); - - expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink?.id, new Set([album.id])); - }); - it('should allow adding assets shared via partner sharing', async () => { const album = AlbumFactory.create(); const asset = AssetFactory.create(); @@ -964,40 +939,6 @@ describe(AlbumService.name, () => { expect(mocks.album.update).not.toHaveBeenCalled(); }); - it('should not allow a shared link user to add assets to multiple albums', async () => { - const album1 = AlbumFactory.create(); - const album2 = AlbumFactory.create(); - const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; - mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); - mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2)); - mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); - - const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build(); - await expect( - sut.addAssetsToAlbums(auth, { - albumIds: [album1.id, album2.id], - assetIds: [asset1.id, asset2.id, asset3.id], - }), - ).resolves.toEqual({ success: true, error: undefined }); - - expect(mocks.album.update).toHaveBeenCalledTimes(1); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { - id: album1.id, - updatedAt: expect.any(Date), - albumThumbnailAssetId: asset1.id, - }); - expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: album1.id, assetId: asset1.id }, - { albumId: album1.id, assetId: asset2.id }, - { albumId: album1.id, assetId: asset3.id }, - ]); - expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( - auth.sharedLink?.id, - new Set([album1.id, album2.id]), - ); - }); - it('should allow adding assets shared via partner sharing', async () => { const user = UserFactory.create(); const album1 = AlbumFactory.create(); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 547ec63bf..24b9b165c 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -165,12 +165,6 @@ export class AlbumService extends BaseService { } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - if (auth.sharedLink) { - this.logger.deprecate( - 'Assets uploaded to a shared link are automatically added and calling this endpoint is no longer necessary. It will be removed in the next major release.', - ); - } - const album = await this.findOrFail(id, { withAssets: false }); await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [id] }); @@ -201,12 +195,6 @@ export class AlbumService extends BaseService { } async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise { - if (auth.sharedLink) { - this.logger.deprecate( - 'Assets uploaded to a shared link are automatically added and calling this endpoint is no longer necessary. It will be removed in the next major release.', - ); - } - const results: AlbumsAddAssetsResponseDto = { success: false, error: BulkIdErrorReason.DUPLICATE, diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 26b15031e..31c50b7c2 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -150,14 +150,7 @@ export class SharedLinkService extends BaseService { } async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { - if (auth.sharedLink) { - this.logger.deprecate( - 'Assets uploaded using shared link authentication are now automatically added to the shared link during upload and in the next major release this endpoint will no longer accept shared link authentication', - ); - } - const sharedLink = await this.findOrFail(auth.user.id, id); - if (sharedLink.type !== SharedLinkType.Individual) { throw new BadRequestException('Invalid shared link type'); } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 21e8bdd66..b8e773377 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -79,11 +79,6 @@ const checkSharedLinkAccess = async ( return sharedLink.allowUpload ? ids : new Set(); } - case Permission.AssetShare: { - // TODO: fix this to not use sharedLink.userId for access control - return await access.asset.checkOwnerAccess(sharedLink.userId, ids, false); - } - case Permission.AlbumRead: { return await access.album.checkSharedLinkAccess(sharedLinkId, ids); }