From fa22e865a48a088626a00c71abefcb8de7ae68a3 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:33:08 -0400 Subject: [PATCH] fix(server): tighten asset visibility (#18699) * tighten visibility * update sql * elevated access util function * fix potential sync issue * include in user stats * include hidden assets in size usage * filter visibility in search duplicates query * stack visibility --- server/src/queries/activity.repository.sql | 1 + server/src/queries/album.repository.sql | 4 +- server/src/queries/asset.job.repository.sql | 6 +- server/src/queries/asset.repository.sql | 27 +++------ server/src/queries/person.repository.sql | 37 ++++++++---- server/src/queries/search.repository.sql | 20 +++---- server/src/queries/stack.repository.sql | 2 + server/src/queries/user.repository.sql | 5 +- .../src/repositories/activity.repository.ts | 2 + server/src/repositories/album.repository.ts | 3 + .../src/repositories/asset-job.repository.ts | 5 +- server/src/repositories/asset.repository.ts | 9 +-- server/src/repositories/person.repository.ts | 59 ++++++++----------- server/src/repositories/search.repository.ts | 6 +- server/src/repositories/stack.repository.ts | 5 +- server/src/repositories/user.repository.ts | 5 +- server/src/services/asset.service.ts | 5 ++ server/src/services/search.service.ts | 18 +++++- server/src/services/timeline.service.ts | 5 ++ server/src/utils/access.ts | 6 ++ server/src/utils/database.ts | 9 +-- 21 files changed, 132 insertions(+), 107 deletions(-) diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3040de8e03..3db4ee6f5b 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -73,3 +73,4 @@ where and "activity"."albumId" = $2 and "activity"."isLiked" = $3 and "assets"."deletedAt" is null + and "assets"."visibility" != 'locked' diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 2b351368ef..26ddccbe17 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -80,6 +80,7 @@ select where "albums_assets_assets"."albumsId" = "albums"."id" and "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') order by "assets"."fileCreatedAt" desc ) as "asset" @@ -178,7 +179,8 @@ from "assets" inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" where - "album_assets"."albumsId" in ($1) + "assets"."visibility" in ('archive', 'timeline') + and "album_assets"."albumsId" in ($1) and "assets"."deletedAt" is null group by "album_assets"."albumsId" diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 3d47b7517e..5caf2e30cd 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -186,8 +186,8 @@ from inner join "smart_search" on "assets"."id" = "smart_search"."assetId" inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "assets"."id" where - "assets"."visibility" != $1 - and "assets"."deletedAt" is null + "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') and "job_status"."duplicatesDetectedAt" is null -- AssetJobRepository.streamForEncodeClip @@ -349,7 +349,7 @@ from "assets" as "stacked" where "stacked"."deletedAt" is not null - and "stacked"."visibility" != $1 + and "stacked"."visibility" = $1 and "stacked"."stackId" = "asset_stack"."id" group by "asset_stack"."id" diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 8efaa6a17b..d85ad341d0 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -130,7 +130,6 @@ select from "assets" left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" where "assets"."id" = any ($1::uuid[]) @@ -240,10 +239,7 @@ with "assets" where "assets"."deletedAt" is null - and ( - "assets"."visibility" = $1 - or "assets"."visibility" = $2 - ) + and "assets"."visibility" in ('archive', 'timeline') ) select "timeBucket", @@ -300,21 +296,14 @@ with where "stacked"."stackId" = "assets"."stackId" and "stacked"."deletedAt" is null - and "stacked"."visibility" != $1 + and "stacked"."visibility" = $1 group by "stacked"."stackId" ) as "stacked_assets" on true where "assets"."deletedAt" is null - and ( - "assets"."visibility" = $2 - or "assets"."visibility" = $3 - ) - and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4 - and ( - "assets"."visibility" = $5 - or "assets"."visibility" = $6 - ) + and "assets"."visibility" in ('archive', 'timeline') + and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $2 and not exists ( select from @@ -374,10 +363,10 @@ with "exif"."assetId" = "assets"."id" ) as "asset" on true where - "assets"."ownerId" = $1::uuid + "assets"."visibility" in ('archive', 'timeline') + and "assets"."ownerId" = $1::uuid and "assets"."duplicateId" is not null and "assets"."deletedAt" is null - and "assets"."visibility" != $2 and "assets"."stackId" is null group by "assets"."duplicateId" @@ -388,12 +377,12 @@ with from "duplicates" where - json_array_length("assets") = $3 + json_array_length("assets") = $2 ), "removed_unique" as ( update "assets" set - "duplicateId" = $4 + "duplicateId" = $3 from "unique" where diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 48854f4872..b8da3b5ae3 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -182,27 +182,42 @@ from "asset_faces" left join "assets" on "assets"."id" = "asset_faces"."assetId" and "asset_faces"."personId" = $1 - and "assets"."visibility" != $2 + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null where "asset_faces"."deletedAt" is null -- PersonRepository.getNumberOfPeople select - count(distinct ("person"."id")) as "total", - count(distinct ("person"."id")) filter ( - where - "person"."isHidden" = $1 + coalesce(count(*), 0) as "total", + coalesce( + count(*) filter ( + where + "isHidden" = $1 + ), + 0 ) as "hidden" from "person" - inner join "asset_faces" on "asset_faces"."personId" = "person"."id" - inner join "assets" on "assets"."id" = "asset_faces"."assetId" - and "assets"."deletedAt" is null - and "assets"."visibility" != $2 where - "person"."ownerId" = $3 - and "asset_faces"."deletedAt" is null + exists ( + select + from + "asset_faces" + where + "asset_faces"."personId" = "person"."id" + and "asset_faces"."deletedAt" is null + and exists ( + select + from + "assets" + where + "assets"."id" = "asset_faces"."assetId" + and "assets"."visibility" = 'timeline' + and "assets"."deletedAt" is null + ) + ) + and "person"."ownerId" = $2 -- PersonRepository.refreshFaces with diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index c100089179..806fdb1c70 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -102,23 +102,23 @@ with "assets" inner join "smart_search" on "assets"."id" = "smart_search"."assetId" where - "assets"."ownerId" = any ($2::uuid[]) + "assets"."visibility" in ('archive', 'timeline') + and "assets"."ownerId" = any ($2::uuid[]) and "assets"."deletedAt" is null - and "assets"."visibility" != $3 - and "assets"."type" = $4 - and "assets"."id" != $5::uuid + and "assets"."type" = $3 + and "assets"."id" != $4::uuid and "assets"."stackId" is null order by "distance" limit - $6 + $5 ) select * from "cte" where - "cte"."distance" <= $7 + "cte"."distance" <= $6 commit -- SearchRepository.searchFaces @@ -241,7 +241,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "state" is not null @@ -253,7 +253,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "city" is not null @@ -265,7 +265,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "make" is not null @@ -277,6 +277,6 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "model" is not null diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 1f0c940101..6d450cd435 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -52,6 +52,7 @@ select where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" + and "assets"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from @@ -135,6 +136,7 @@ select where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" + and "assets"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 33f2960266..0638b8c965 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -290,7 +290,7 @@ order by select "users"."id" as "userId", "users"."name" as "userName", - "users"."quotaSizeInBytes" as "quotaSizeInBytes", + "users"."quotaSizeInBytes", count(*) filter ( where ( @@ -335,9 +335,8 @@ select from "users" left join "assets" on "assets"."ownerId" = "users"."id" + and "assets"."deletedAt" is null left join "exif" on "exif"."assetId" = "assets"."id" -where - "assets"."deletedAt" is null group by "users"."id" order by diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index d030a99f4f..f8bbfdf8a6 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -5,6 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { Activity, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetVisibility } from 'src/enum'; import { asUuid } from 'src/utils/database'; export interface ActivitySearch { @@ -76,6 +77,7 @@ export class ActivityRepository { .where('activity.albumId', '=', albumId) .where('activity.isLiked', '=', false) .where('assets.deletedAt', 'is', null) + .where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED)) .executeTakeFirstOrThrow(); return count; diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index c8bdae6d31..7131a72f61 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -6,6 +6,7 @@ import { columns, Exif } from 'src/database'; import { Albums, DB } from 'src/db'; import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { AlbumUserCreateDto } from 'src/dtos/album.dto'; +import { withDefaultVisibility } from 'src/utils/database'; export interface AlbumAssetCount { albumId: string; @@ -58,6 +59,7 @@ const withAssets = (eb: ExpressionBuilder) => { .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') .where('assets.deletedAt', 'is', null) + .$call(withDefaultVisibility) .orderBy('assets.fileCreatedAt', 'desc') .as('asset'), ) @@ -121,6 +123,7 @@ export class AlbumRepository { return ( this.db .selectFrom('assets') + .$call(withDefaultVisibility) .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') .select('album_assets.albumsId as albumId') .select((eb) => eb.fn.min(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate')) diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 6f86edaaa1..d629202f04 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -11,6 +11,7 @@ import { anyUuid, asUuid, toJson, + withDefaultVisibility, withExif, withExifInner, withFaces, @@ -140,9 +141,9 @@ export class AssetJobRepository { return this.db .selectFrom('assets') .select(['assets.id']) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.deletedAt', 'is', null) .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .$call(withDefaultVisibility) .$if(!force, (qb) => qb .innerJoin('asset_job_status as job_status', 'job_status.assetId', 'assets.id') @@ -226,7 +227,7 @@ export class AssetJobRepository { .select(['asset_stack.id', 'asset_stack.primaryAssetId']) .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) .where('stacked.deletedAt', 'is not', null) - .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) + .where('stacked.visibility', '=', AssetVisibility.TIMELINE) .whereRef('stacked.stackId', '=', 'asset_stack.id') .groupBy('asset_stack.id') .as('stacked_assets'), diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 60744ddc5f..416cf4e5de 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -300,7 +300,6 @@ export class AssetRepository { .select(withFacesAndPeople) .select(withTags) .$call(withExif) - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') .where('assets.id', '=', anyUuid(ids)) .execute(); } @@ -523,8 +522,8 @@ export class AssetRepository { .selectFrom('assets') .selectAll('assets') .$call(withExif) + .$call(withDefaultVisibility) .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '!=', AssetVisibility.HIDDEN) .where('deletedAt', 'is', null) .orderBy((eb) => eb.fn('random')) .limit(take) @@ -634,8 +633,6 @@ export class AssetRepository { ) .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(options.visibility == undefined, withDefaultVisibility) - .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) .$if(!!options.withStacked, (qb) => qb @@ -656,7 +653,7 @@ export class AssetRepository { .select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack')) .whereRef('stacked.stackId', '=', 'assets.stackId') .where('stacked.deletedAt', 'is', null) - .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) + .where('stacked.visibility', '=', AssetVisibility.TIMELINE) .groupBy('stacked.stackId') .as('stacked_assets'), (join) => join.onTrue(), @@ -709,6 +706,7 @@ export class AssetRepository { .with('duplicates', (qb) => qb .selectFrom('assets') + .$call(withDefaultVisibility) .leftJoinLateral( (qb) => qb @@ -727,7 +725,6 @@ export class AssetRepository { .where('assets.duplicateId', 'is not', null) .$narrowType<{ duplicateId: NotNull }>() .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.stackId', 'is', null) .groupBy('assets.duplicateId'), ) diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 70a9980201..229a523c17 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -38,11 +38,6 @@ export interface PersonStatistics { assets: number; } -export interface PeopleStatistics { - total: number; - hidden: number; -} - export interface DeleteFacesOptions { sourceType: SourceType; } @@ -151,7 +146,7 @@ export class PersonRepository { .innerJoin('assets', (join) => join .onRef('asset_faces.assetId', '=', 'assets.id') - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) + .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('assets.deletedAt', 'is', null), ) .where('person.ownerId', '=', userId) @@ -341,7 +336,7 @@ export class PersonRepository { join .onRef('assets.id', '=', 'asset_faces.assetId') .on('asset_faces.personId', '=', personId) - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) + .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('assets.deletedAt', 'is', null), ) .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count')) @@ -354,35 +349,31 @@ export class PersonRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - async getNumberOfPeople(userId: string): Promise { - const items = await this.db + getNumberOfPeople(userId: string) { + const zero = sql.lit(0); + return this.db .selectFrom('person') - .innerJoin('asset_faces', 'asset_faces.personId', 'person.id') + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('asset_faces') + .whereRef('asset_faces.personId', '=', 'person.id') + .where('asset_faces.deletedAt', 'is', null) + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('assets') + .whereRef('assets.id', '=', 'asset_faces.assetId') + .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('assets.deletedAt', 'is', null), + ), + ), + ), + ) .where('person.ownerId', '=', userId) - .where('asset_faces.deletedAt', 'is', null) - .innerJoin('assets', (join) => - join - .onRef('assets.id', '=', 'asset_faces.assetId') - .on('assets.deletedAt', 'is', null) - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE), - ) - .select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total')) - .select((eb) => - eb.fn - .count(eb.fn('distinct', ['person.id'])) - .filterWhere('person.isHidden', '=', true) - .as('hidden'), - ) - .executeTakeFirst(); - - if (items == undefined) { - return { total: 0, hidden: 0 }; - } - - return { - total: Number(items.total), - hidden: Number(items.hidden), - }; + .select((eb) => eb.fn.coalesce(eb.fn.countAll(), zero).as('total')) + .select((eb) => eb.fn.coalesce(eb.fn.countAll().filterWhere('isHidden', '=', true), zero).as('hidden')) + .executeTakeFirstOrThrow(); } create(person: Insertable) { diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index a7b7027b7b..747a59c65b 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -7,7 +7,7 @@ import { DummyValue, GenerateSql } from 'src/decorators'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum'; import { probes } from 'src/repositories/database.repository'; -import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database'; +import { anyUuid, asUuid, searchAssetBuilder, withDefaultVisibility } from 'src/utils/database'; import { paginationHelper } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; @@ -268,6 +268,7 @@ export class SearchRepository { .with('cte', (qb) => qb .selectFrom('assets') + .$call(withDefaultVisibility) .select([ 'assets.id as assetId', 'assets.duplicateId', @@ -276,7 +277,6 @@ export class SearchRepository { .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') .where('assets.ownerId', '=', anyUuid(userIds)) .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.type', '=', type) .where('assets.id', '!=', asUuid(assetId)) .where('assets.stackId', 'is', null) @@ -472,7 +472,7 @@ export class SearchRepository { .distinctOn(field) .innerJoin('assets', 'assets.id', 'exif.assetId') .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '!=', AssetVisibility.HIDDEN) + .where('visibility', '=', AssetVisibility.TIMELINE) .where('deletedAt', 'is', null) .where(field, 'is not', null); } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index c9d69fb37f..78ff255579 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { AssetStack, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { asUuid } from 'src/utils/database'; +import { asUuid, withDefaultVisibility } from 'src/utils/database'; export interface StackSearch { ownerId: string; @@ -34,7 +34,8 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) ) .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) .where('assets.deletedAt', 'is', null) - .whereRef('assets.stackId', '=', 'asset_stack.id'), + .whereRef('assets.stackId', '=', 'asset_stack.id') + .$call(withDefaultVisibility), ).as('assets'); }; diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 6972479df6..06159041c5 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -210,9 +210,9 @@ export class UserRepository { getUserStats() { return this.db .selectFrom('users') - .leftJoin('assets', 'assets.ownerId', 'users.id') + .leftJoin('assets', (join) => join.onRef('assets.ownerId', '=', 'users.id').on('assets.deletedAt', 'is', null)) .leftJoin('exif', 'exif.assetId', 'assets.id') - .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes']) + .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes']) .select((eb) => [ eb.fn .countAll() @@ -256,7 +256,6 @@ export class UserRepository { ) .as('usageVideos'), ]) - .where('assets.deletedAt', 'is', null) .groupBy('users.id') .orderBy('users.createdAt', 'asc') .execute(); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index bc73ff6410..55906bf0a6 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -17,11 +17,16 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; +import { requireElevatedPermission } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; @Injectable() export class AssetService extends BaseService { async getStatistics(auth: AuthDto, dto: AssetStatsDto) { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const stats = await this.assetRepository.getStatistics(auth.user.id, dto); return mapStats(stats); } diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index df286d1809..3f122b5e74 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -14,8 +14,9 @@ import { SearchSuggestionType, SmartSearchDto, } from 'src/dtos/search.dto'; -import { AssetOrder } from 'src/enum'; +import { AssetOrder, AssetVisibility } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { isSmartSearchEnabled } from 'src/utils/misc'; @@ -40,9 +41,11 @@ export class SearchService extends BaseService { } async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { - let checksum: Buffer | undefined; - const userIds = await this.getUserIdsToSearch(auth); + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + let checksum: Buffer | undefined; if (dto.checksum) { const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; checksum = Buffer.from(dto.checksum, encoding); @@ -50,6 +53,7 @@ export class SearchService extends BaseService { const page = dto.page ?? 1; const size = dto.size || 250; + const userIds = await this.getUserIdsToSearch(auth); const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { @@ -64,12 +68,20 @@ export class SearchService extends BaseService { } async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const userIds = await this.getUserIdsToSearch(auth); const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds }); return items.map((item) => mapAsset(item, { auth })); } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { throw new BadRequestException('Smart search is not enabled'); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index f3ebcc2cd7..abd536a97e 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -4,6 +4,7 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketsResponseDto } from 'src/d import { AssetVisibility, Permission } from 'src/enum'; import { TimeBucketOptions } from 'src/repositories/asset.repository'; import { BaseService } from 'src/services/base.service'; +import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; @Injectable() @@ -44,6 +45,10 @@ export class TimelineService extends BaseService { } private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + if (dto.albumId) { await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 38697a654b..c1b162927d 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -304,3 +304,9 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe } } }; + +export const requireElevatedPermission = (auth: AuthDto) => { + if (!auth.session?.hasElevatedPermission) { + throw new UnauthorizedException('Elevated permission is required'); + } +}; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 40bf7503db..5e5c6c5fb4 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -153,17 +153,12 @@ export function toJson(qb: SelectQueryBuilder) { - return qb.where((qb) => - qb.or([ - qb('assets.visibility', '=', AssetVisibility.TIMELINE), - qb('assets.visibility', '=', AssetVisibility.ARCHIVE), - ]), - ); + return qb.where('assets.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]); } +// TODO come up with a better query that only selects the fields we need export function withExif(qb: SelectQueryBuilder) { return qb .leftJoin('exif', 'assets.id', 'exif.assetId')