fix: search metadata for album shared links (#29742)

This commit is contained in:
Daniel Dietzler
2026-07-08 20:32:20 +02:00
committed by GitHub
parent cfd95f0196
commit 655cd01d57
6 changed files with 73 additions and 7 deletions

View File

@@ -4,6 +4,7 @@ import {
AssetVisibility,
deleteAssets,
LoginResponseDto,
SharedLinkType,
updateAsset,
} from '@immich/sdk';
import { DateTime } from 'luxon';
@@ -357,6 +358,32 @@ describe('/search', () => {
expect(body.assets.items).toHaveLength(assets.length);
});
}
it('should reject shared link access without an album filter', async () => {
const album = await utils.createAlbum(admin.accessToken, { albumName: 'foo' });
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
const { status, body } = await request(app).post(`/search/metadata?key=${sharedLink.key}`).send({});
expect(status).toBe(400);
expect(body).toEqual({ message: 'Shared link access is only allowed in combination with an albumIds filter' });
});
it('should allow shared link access for albums', async () => {
const asset = await utils.createAsset(admin.accessToken);
const album = await utils.createAlbum(admin.accessToken, { albumName: 'foo', assetIds: [asset.id] });
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
const { status, body } = await request(app)
.post(`/search/metadata?key=${sharedLink.key}`)
.send({ albumIds: [album.id] });
expect(status).toBe(200);
expect(body.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
});
});
describe('POST /search/random', () => {

View File

@@ -302,7 +302,11 @@ class SearchApi {
/// Parameters:
///
/// * [MetadataSearchDto] metadataSearchDto (required):
Future<Response> searchAssetsWithHttpInfo(MetadataSearchDto metadataSearchDto, { Future<void>? abortTrigger, }) async {
///
/// * [String] key:
///
/// * [String] slug:
Future<Response> searchAssetsWithHttpInfo(MetadataSearchDto metadataSearchDto, { String? key, String? slug, Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/search/metadata';
@@ -313,6 +317,13 @@ class SearchApi {
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'];
@@ -335,8 +346,12 @@ class SearchApi {
/// Parameters:
///
/// * [MetadataSearchDto] metadataSearchDto (required):
Future<SearchResponseDto?> searchAssets(MetadataSearchDto metadataSearchDto, { Future<void>? abortTrigger, }) async {
final response = await searchAssetsWithHttpInfo(metadataSearchDto, abortTrigger: abortTrigger,);
///
/// * [String] key:
///
/// * [String] slug:
Future<SearchResponseDto?> searchAssets(MetadataSearchDto metadataSearchDto, { String? key, String? slug, Future<void>? abortTrigger, }) async {
final response = await searchAssetsWithHttpInfo(metadataSearchDto, key: key, slug: slug, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -10552,7 +10552,24 @@
"post": {
"description": "Search for assets based on various metadata criteria.",
"operationId": "searchAssets",
"parameters": [],
"parameters": [
{
"name": "key",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "slug",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {

View File

@@ -5764,13 +5764,18 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat
/**
* Search assets by metadata
*/
export function searchAssets({ metadataSearchDto }: {
export function searchAssets({ key, slug, metadataSearchDto }: {
key?: string;
slug?: string;
metadataSearchDto: MetadataSearchDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SearchResponseDto;
}>("/search/metadata", oazapfts.json({
}>(`/search/metadata${QS.query(QS.explode({
key,
slug
}))}`, oazapfts.json({
...opts,
method: "POST",
body: metadataSearchDto

View File

@@ -28,7 +28,7 @@ export class SearchController {
constructor(private service: SearchService) {}
@Post('metadata')
@Authenticated({ permission: Permission.AssetRead })
@Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@HttpCode(HttpStatus.OK)
@Endpoint({
summary: 'Search assets by metadata',

View File

@@ -77,6 +77,8 @@ export class SearchService extends BaseService {
if (dto.albumIds && dto.albumIds.length > 0) {
await this.requireAccess({ auth, ids: dto.albumIds, permission: Permission.AlbumRead });
} else if (auth.sharedLink) {
throw new BadRequestException('Shared link access is only allowed in combination with an albumIds filter');
} else {
userIds = await this.getUserIdsToSearch(auth, dto.visibility);
}