refactor!: remove my shared link dto (#27023)

refactor!: remove deprecated shared link apis
This commit is contained in:
Jason Rasmussen 2026-04-14 20:58:02 -04:00 committed by GitHub
parent e1a84d3ab6
commit 6ba17bb86f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 39 additions and 281 deletions

View File

@ -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(

View File

@ -27,11 +27,7 @@ class AlbumsApi {
/// * [String] id (required):
///
/// * [BulkIdsDto] bulkIdsDto (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<Response> addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async {
Future<Response> 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 = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
if (slug != null) {
queryParams.addAll(_queryParams('', 'slug', slug));
}
const contentTypes = <String>['application/json'];
@ -73,12 +62,8 @@ class AlbumsApi {
/// * [String] id (required):
///
/// * [BulkIdsDto] bulkIdsDto (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<List<BulkIdResponseDto>?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async {
final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, slug: slug, );
Future<List<BulkIdResponseDto>?> 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<Response> addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async {
Future<Response> addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/assets';
@ -119,13 +100,6 @@ class AlbumsApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
if (slug != null) {
queryParams.addAll(_queryParams('', 'slug', slug));
}
const contentTypes = <String>['application/json'];
@ -147,12 +121,8 @@ class AlbumsApi {
/// Parameters:
///
/// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<AlbumsAddAssetsResponseDto?> addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async {
final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, key: key, slug: slug, );
Future<AlbumsAddAssetsResponseDto?> addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async {
final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@ -27,11 +27,7 @@ class SharedLinksApi {
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<Response> addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
Future<Response> 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 = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
if (slug != null) {
queryParams.addAll(_queryParams('', 'slug', slug));
}
const contentTypes = <String>['application/json'];
@ -73,12 +62,8 @@ class SharedLinksApi {
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
///
/// * [String] key:
///
/// * [String] slug:
Future<List<AssetIdsResponseDto>?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, );
Future<List<AssetIdsResponseDto>?> 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<Response> getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async {
Future<Response> 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 = <String>[];
@ -288,15 +261,9 @@ class SharedLinksApi {
///
/// * [String] key:
///
/// * [String] password:
/// Link password
///
/// * [String] slug:
///
/// * [String] token:
/// Access token
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, String? password, String? slug, String? token, }) async {
final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, );
Future<SharedLinkResponseDto?> 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));
}

View File

@ -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"
}
},

View File

@ -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

View File

@ -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.',

View File

@ -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<SharedLinkResponseDto> {
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<SharedLinkResponseDto> {
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:

View File

@ -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 {

View File

@ -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();

View File

@ -165,12 +165,6 @@ export class AlbumService extends BaseService {
}
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
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<AlbumsAddAssetsResponseDto> {
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,

View File

@ -150,14 +150,7 @@ export class SharedLinkService extends BaseService {
}
async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
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');
}

View File

@ -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);
}