diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 54a4c0160cc5c..864fe47196fe9 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -214,7 +214,7 @@ describe('/asset', () => { id: user1Assets[0].id, isFavorite: false, people: { - faces: [ + visiblePeople: [ { birthDate: null, id: expect.any(String), @@ -484,7 +484,7 @@ describe('/asset', () => { id: user1Assets[0].id, isFavorite: true, people: { - faces: [ + visiblePeople: [ { birthDate: null, id: expect.any(String), diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index a9fd6678f8035..f85808dece0b2 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -72,7 +72,7 @@ class AssetService { final AssetResponseDto? dto = await _apiService.assetApi.getAssetInfo(remoteId); - return dto?.people?.faces; + return dto?.people?.visiblePeople; } catch (error, stack) { log.severe( 'Error while getting remote asset info: ${error.toString()}', diff --git a/mobile/openapi/doc/PeopleWithFacesResponseDto.md b/mobile/openapi/doc/PeopleWithFacesResponseDto.md index acaa413389d88..a8399ec40cf40 100644 --- a/mobile/openapi/doc/PeopleWithFacesResponseDto.md +++ b/mobile/openapi/doc/PeopleWithFacesResponseDto.md @@ -8,8 +8,8 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**faces** | [**List**](PersonWithFacesResponseDto.md) | | [default to const []] **numberOfFaces** | **int** | | +**visiblePeople** | [**List**](PersonWithFacesResponseDto.md) | | [default to const []] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/model/people_with_faces_response_dto.dart b/mobile/openapi/lib/model/people_with_faces_response_dto.dart index 4e0d23d204a12..e6a03b6df3548 100644 --- a/mobile/openapi/lib/model/people_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/people_with_faces_response_dto.dart @@ -13,32 +13,32 @@ part of openapi.api; class PeopleWithFacesResponseDto { /// Returns a new [PeopleWithFacesResponseDto] instance. PeopleWithFacesResponseDto({ - this.faces = const [], required this.numberOfFaces, + this.visiblePeople = const [], }); - List faces; - int numberOfFaces; + List visiblePeople; + @override bool operator ==(Object other) => identical(this, other) || other is PeopleWithFacesResponseDto && - _deepEquality.equals(other.faces, faces) && - other.numberOfFaces == numberOfFaces; + other.numberOfFaces == numberOfFaces && + _deepEquality.equals(other.visiblePeople, visiblePeople); @override int get hashCode => // ignore: unnecessary_parenthesis - (faces.hashCode) + - (numberOfFaces.hashCode); + (numberOfFaces.hashCode) + + (visiblePeople.hashCode); @override - String toString() => 'PeopleWithFacesResponseDto[faces=$faces, numberOfFaces=$numberOfFaces]'; + String toString() => 'PeopleWithFacesResponseDto[numberOfFaces=$numberOfFaces, visiblePeople=$visiblePeople]'; Map toJson() { final json = {}; - json[r'faces'] = this.faces; json[r'numberOfFaces'] = this.numberOfFaces; + json[r'visiblePeople'] = this.visiblePeople; return json; } @@ -50,8 +50,8 @@ class PeopleWithFacesResponseDto { final json = value.cast(); return PeopleWithFacesResponseDto( - faces: PersonWithFacesResponseDto.listFromJson(json[r'faces']), numberOfFaces: mapValueOfType(json, r'numberOfFaces')!, + visiblePeople: PersonWithFacesResponseDto.listFromJson(json[r'visiblePeople']), ); } return null; @@ -99,8 +99,8 @@ class PeopleWithFacesResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'faces', 'numberOfFaces', + 'visiblePeople', }; } diff --git a/mobile/openapi/test/people_with_faces_response_dto_test.dart b/mobile/openapi/test/people_with_faces_response_dto_test.dart index 1a260f645674f..ff8c3aea0d939 100644 --- a/mobile/openapi/test/people_with_faces_response_dto_test.dart +++ b/mobile/openapi/test/people_with_faces_response_dto_test.dart @@ -16,13 +16,13 @@ void main() { // final instance = PeopleWithFacesResponseDto(); group('test PeopleWithFacesResponseDto', () { - // List faces (default value: const []) - test('to test the property `faces`', () async { + // int numberOfFaces + test('to test the property `numberOfFaces`', () async { // TODO }); - // int numberOfFaces - test('to test the property `numberOfFaces`', () async { + // List visiblePeople (default value: const []) + test('to test the property `visiblePeople`', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index cc4888b82fe24..36602cbe2d42c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9014,19 +9014,19 @@ }, "PeopleWithFacesResponseDto": { "properties": { - "faces": { + "numberOfFaces": { + "type": "integer" + }, + "visiblePeople": { "items": { "$ref": "#/components/schemas/PersonWithFacesResponseDto" }, "type": "array" - }, - "numberOfFaces": { - "type": "integer" } }, "required": [ - "faces", - "numberOfFaces" + "numberOfFaces", + "visiblePeople" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9f23d7631e96d..fa53473f98d5c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -101,8 +101,8 @@ export type PersonWithFacesResponseDto = { thumbnailPath: string; }; export type PeopleWithFacesResponseDto = { - faces: PersonWithFacesResponseDto[]; numberOfFaces: number; + visiblePeople: PersonWithFacesResponseDto[]; }; export type SmartInfoResponseDto = { objects?: string[] | null; diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index fed3465406583..317a672ada821 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -78,7 +78,7 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PeopleWithFacesResponseDto = } } - return { faces: result, numberOfFaces: faces.length }; + return { visiblePeople: result, numberOfFaces: faces.length }; }; export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index a81eb2b6efd22..85dfd5e30717f 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -78,7 +78,7 @@ export class PersonWithFacesResponseDto extends PersonResponseDto { } export class PeopleWithFacesResponseDto { - faces!: PersonWithFacesResponseDto[]; + visiblePeople!: PersonWithFacesResponseDto[]; @ApiProperty({ type: 'integer' }) numberOfFaces!: number; } diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 924f99196e958..cc7340f8a958b 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -449,6 +449,18 @@ describe(PersonService.name, () => { await expect(sut.unassignFace(authStub.admin, faceStub.face1.id)).resolves.toStrictEqual( mapFaces(faceStub.unassignedFace, authStub.admin), ); + + expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); + }); + + it('should not unassign a face if user has no create access', async () => { + personMock.getFaceById.mockResolvedValueOnce(faceStub.face1); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); + personMock.reassignFace.mockResolvedValue(1); + personMock.getRandomFace.mockResolvedValue(null); + personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace); + + await expect(sut.unassignFace(authStub.admin, faceStub.face1.id)).rejects.toBeInstanceOf(BadRequestException); }); }); @@ -465,6 +477,18 @@ describe(PersonService.name, () => { sut.unassignFaces(authStub.admin, { data: [{ assetId: faceStub.face1.id, personId: 'person-1' }] }), ).resolves.toStrictEqual([{ id: 'assetFaceId1', success: true }]); }); + + it('should not unassign a face if the user has no create access', async () => { + personMock.getFacesByIds.mockResolvedValueOnce([faceStub.face1]); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); + personMock.reassignFace.mockResolvedValue(1); + personMock.getRandomFace.mockResolvedValue(null); + personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace); + + await expect( + sut.unassignFaces(authStub.admin, { data: [{ assetId: faceStub.face1.id, personId: 'person-1' }] }), + ).rejects.toBeInstanceOf(BadRequestException); + }); }); describe('handlePersonCleanup', () => { diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index f850c70c5554e..ed687e9aa9b05 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -218,7 +218,7 @@

PEOPLE

- {#if people.faces.some((person) => person.isHidden)} + {#if people.visiblePeople.some((person) => person.isHidden)}
- {#each people.faces as person (person.id)} + {#each people.visiblePeople as person (person.id)} {#if showingHiddenPeople || !person.isHidden} p.name).map((p) => p.name) ?? []; + const names = asset.people?.visiblePeople.filter((p) => p.name).map((p) => p.name) ?? []; if (names.length == 1) { altText += ` with ${names[0]}`; }