mirror of
https://github.com/immich-app/immich.git
synced 2026-07-15 21:32:32 +03:00
fix: search metadata for album shared links (#29742)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
21
mobile/openapi/lib/api/search_api.dart
generated
21
mobile/openapi/lib/api/search_api.dart
generated
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user