fix: prevent an offline asset from being used as a person feature photo (#21278)

This commit is contained in:
Jason Rasmussen 2025-08-25 22:40:56 -04:00 committed by GitHub
parent d04675fb41
commit 8f1b505ba0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 11 additions and 9 deletions

View file

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } from 'kysely'; import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
@ -68,12 +68,6 @@ const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
).as('person'); ).as('person');
}; };
const withAsset = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
return jsonObjectFrom(eb.selectFrom('asset').selectAll('asset').whereRef('asset.id', '=', 'asset_face.assetId')).as(
'asset',
);
};
const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => { const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
return jsonObjectFrom( return jsonObjectFrom(
eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_face.id'), eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_face.id'),
@ -481,7 +475,12 @@ export class PersonRepository {
return this.db return this.db
.selectFrom('asset_face') .selectFrom('asset_face')
.selectAll('asset_face') .selectAll('asset_face')
.select(withAsset) .select((eb) =>
jsonObjectFrom(eb.selectFrom('asset').selectAll('asset').whereRef('asset.id', '=', 'asset_face.assetId')).as(
'asset',
),
)
.$narrowType<{ asset: NotNull }>()
.select(withPerson) .select(withPerson)
.where('asset_face.assetId', 'in', assetIds) .where('asset_face.assetId', 'in', assetIds)
.where('asset_face.personId', 'in', personIds) .where('asset_face.personId', 'in', personIds)

View file

@ -42,7 +42,6 @@ describe(MediaService.name, () => {
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image])); mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image]));
mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail])); mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail]));
mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]);
await sut.handleQueueGenerateThumbnails({ force: true }); await sut.handleQueueGenerateThumbnails({ force: true });

View file

@ -197,6 +197,10 @@ export class PersonService extends BaseService {
throw new BadRequestException('Invalid assetId for feature face'); throw new BadRequestException('Invalid assetId for feature face');
} }
if (face.asset.isOffline) {
throw new BadRequestException('An offline asset cannot be used for feature face');
}
faceId = face.id; faceId = face.id;
} }