From b60fa7784688f4c0bb01ba915b40a13fa46f2190 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 05:33:43 -0400 Subject: [PATCH 1/6] fix: update renovate labels (#11931) --- renovate.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 6f5424023b31d..ccfb75b19c157 100644 --- a/renovate.json +++ b/renovate.json @@ -79,7 +79,11 @@ "schedule": "on tuesday" } ], - "ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"], + "ignorePaths": [ + "mobile/openapi/pubspec.yaml", + "mobile/ios", + "mobile/android" + ], "ignoreDeps": ["http", "intl"], - "labels": ["dependencies"] + "labels": ["dependencies", "changelog:skip"] } From c7801eae7e2b95c62dceeeabf8ad03c11af3efeb Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:49:35 -0400 Subject: [PATCH 2/6] fix: random e2e test (#11932) --- e2e/src/api/specs/asset.e2e-spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 5bd52b437ec83..8444aea2ba0fc 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -363,6 +363,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken), ]); + + await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); it('should require authentication', async () => { From 8285803c9560077556bf724854945ae82c7caaf9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:49:56 -0400 Subject: [PATCH 3/6] refactor: access core (#11930) --- server/src/cores/access.core.ts | 312 ------------------ server/src/services/activity.service.ts | 18 +- server/src/services/album.service.ts | 36 +- server/src/services/asset-media.service.ts | 29 +- server/src/services/asset.service.ts | 16 +- server/src/services/audit.service.ts | 9 +- server/src/services/download.service.ts | 15 +- server/src/services/memory.service.ts | 30 +- server/src/services/partner.service.ts | 11 +- server/src/services/person.service.ts | 37 ++- server/src/services/session.service.ts | 9 +- server/src/services/shared-link.service.ts | 16 +- server/src/services/stack.service.ts | 20 +- server/src/services/sync.service.ts | 14 +- server/src/services/timeline.service.ts | 16 +- server/src/services/trash.service.ts | 12 +- server/src/utils/access.ts | 273 ++++++++++++++- server/src/utils/asset.util.ts | 31 +- .../repositories/access.repository.mock.ts | 7 +- 19 files changed, 415 insertions(+), 496 deletions(-) delete mode 100644 server/src/cores/access.core.ts diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts deleted file mode 100644 index f0050b3947253..0000000000000 --- a/server/src/cores/access.core.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { AuthDto } from 'src/dtos/auth.dto'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { AlbumUserRole, Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; - -let instance: AccessCore | null; - -export class AccessCore { - private constructor(private repository: IAccessRepository) {} - - static create(repository: IAccessRepository) { - if (!instance) { - instance = new AccessCore(repository); - } - - return instance; - } - - static reset() { - instance = null; - } - - requireUploadAccess(auth: AuthDto | null): AuthDto { - if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { - throw new UnauthorizedException(); - } - return auth; - } - - /** - * Check if user has access to all ids, for the given permission. - * Throws error if user does not have access to any of the ids. - */ - async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) { - ids = Array.isArray(ids) ? ids : [ids]; - const allowedIds = await this.checkAccess(auth, permission, ids); - if (!setIsEqual(new Set(ids), allowedIds)) { - throw new BadRequestException(`Not found or no ${permission} access`); - } - } - - /** - * Return ids that user has access to, for the given permission. - * Check is done for each id, and only allowed ids are returned. - * - * @returns Set - */ - async checkAccess(auth: AuthDto, permission: Permission, ids: Set | string[]): Promise> { - const idSet = Array.isArray(ids) ? new Set(ids) : ids; - if (idSet.size === 0) { - return new Set(); - } - - if (auth.sharedLink) { - return this.checkAccessSharedLink(auth.sharedLink, permission, idSet); - } - - return this.checkAccessOther(auth, permission, idSet); - } - - private async checkAccessSharedLink( - sharedLink: SharedLinkEntity, - permission: Permission, - ids: Set, - ): Promise> { - const sharedLinkId = sharedLink.id; - - switch (permission) { - case Permission.ASSET_READ: { - return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ASSET_VIEW: { - return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ASSET_DOWNLOAD: { - return sharedLink.allowDownload - ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - case Permission.ASSET_UPLOAD: { - return sharedLink.allowUpload ? ids : new Set(); - } - - case Permission.ASSET_SHARE: { - // TODO: fix this to not use sharedLink.userId for access control - return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids); - } - - case Permission.ALBUM_READ: { - return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ALBUM_DOWNLOAD: { - return sharedLink.allowDownload - ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - case Permission.ALBUM_ADD_ASSET: { - return sharedLink.allowUpload - ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - default: { - return new Set(); - } - } - } - - private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set): Promise> { - switch (permission) { - // uses album id - case Permission.ACTIVITY_CREATE: { - return await this.repository.activity.checkCreateAccess(auth.user.id, ids); - } - - // uses activity id - case Permission.ACTIVITY_DELETE: { - const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids); - const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess( - auth.user.id, - setDifference(ids, isOwner), - ); - return setUnion(isOwner, isAlbumOwner); - } - - case Permission.ASSET_READ: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_SHARE: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } - - case Permission.ASSET_VIEW: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_DOWNLOAD: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_UPDATE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ASSET_DELETE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ASSET_RESTORE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_READ: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_ADD_ASSET: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_UPDATE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_DELETE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_SHARE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_DOWNLOAD: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_REMOVE_ASSET: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ASSET_UPLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.ARCHIVE_READ: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.AUTH_DEVICE_DELETE: { - return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.TIMELINE_READ: { - const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } - - case Permission.TIMELINE_DOWNLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.MEMORY_READ: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_UPDATE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_DELETE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_DELETE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_READ: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_UPDATE: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_MERGE: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_CREATE: { - return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_REASSIGN: { - return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); - } - - case Permission.PARTNER_UPDATE: { - return await this.repository.partner.checkUpdateAccess(auth.user.id, ids); - } - - case Permission.STACK_READ: { - return this.repository.stack.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.STACK_UPDATE: { - return this.repository.stack.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.STACK_DELETE: { - return this.repository.stack.checkOwnerAccess(auth.user.id, ids); - } - - default: { - return new Set(); - } - } - } -} diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index c1b2e1b4d0e51..1e4034de936fa 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { ActivityCreateDto, ActivityDto, @@ -16,20 +15,17 @@ import { ActivityEntity } from 'src/entities/activity.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class ActivityService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IActivityRepository) private repository: IActivityRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); const activities = await this.repository.search({ userId: dto.userId, albumId: dto.albumId, @@ -41,12 +37,12 @@ export class ActivityService { } async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { - await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); const common = { userId: auth.user.id, @@ -80,7 +76,7 @@ export class ActivityService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); await this.repository.delete(id); } } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 06f2a7a0fb02a..02dab1a74024a 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { AddUsersDto, AlbumCountResponseDto, @@ -24,21 +23,19 @@ import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfa import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class AlbumService { - private access: AccessCore; constructor( - @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getCount(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ @@ -102,7 +99,7 @@ export class AlbumService { } async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] }); await this.albumRepository.updateThumbnails(); const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); @@ -126,7 +123,11 @@ export class AlbumService { } } - const allowedAssetIdsSet = await this.access.checkAccess(auth, Permission.ASSET_SHARE, new Set(dto.assetIds)); + const allowedAssetIdsSet = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: dto.assetIds || [], + }); const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity); const album = await this.albumRepository.create({ @@ -146,7 +147,7 @@ export class AlbumService { } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: true }); @@ -169,17 +170,17 @@ export class AlbumService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] }); await this.albumRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.access.requirePermission(auth, Permission.ALBUM_ADD_ASSET, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); const results = await addAssets( auth, - { accessRepository: this.accessRepository, repository: this.albumRepository }, + { access: this.access, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids }, ); @@ -198,12 +199,12 @@ export class AlbumService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_REMOVE_ASSET, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); const results = await removeAssets( auth, - { accessRepository: this.accessRepository, repository: this.albumRepository }, + { access: this.access, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, ); @@ -219,7 +220,7 @@ export class AlbumService { } async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); @@ -263,15 +264,14 @@ export class AlbumService { // non-admin can remove themselves if (auth.user.id !== userId) { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); } await this.albumUserRepository.delete({ albumId: id, userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); - + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index b66b0607b390a..9ce2e58d28fad 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -7,7 +7,6 @@ import { } from '@nestjs/common'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AssetBulkUploadCheckResponseDto, @@ -36,6 +35,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess, requireUploadAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; @@ -57,10 +57,8 @@ export interface UploadFile { @Injectable() export class AssetMediaService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @@ -69,7 +67,6 @@ export class AssetMediaService { @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetMediaService.name); - this.access = AccessCore.create(accessRepository); } async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise { @@ -86,7 +83,7 @@ export class AssetMediaService { } canUploadFile({ auth, fieldName, file }: UploadRequest): true { - this.access.requireUploadAccess(auth); + requireUploadAccess(auth); const filename = file.originalName; @@ -118,7 +115,7 @@ export class AssetMediaService { } getUploadFilename({ auth, fieldName, file }: UploadRequest): string { - this.access.requireUploadAccess(auth); + requireUploadAccess(auth); const originalExtension = extname(file.originalName); @@ -132,7 +129,7 @@ export class AssetMediaService { } getUploadFolder({ auth, fieldName, file }: UploadRequest): string { - auth = this.access.requireUploadAccess(auth); + auth = requireUploadAccess(auth); let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid); if (fieldName === UploadFieldName.PROFILE_DATA) { @@ -151,12 +148,12 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await this.access.requirePermission( + await requireAccess(this.access, { auth, - Permission.ASSET_UPLOAD, + permission: Permission.ASSET_UPLOAD, // do not need an id here, but the interface requires it - auth.user.id, - ); + ids: [auth.user.id], + }); this.requireQuota(auth, file.size); @@ -195,7 +192,7 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const asset = (await this.assetRepository.getById(id)) as AssetEntity; this.requireQuota(auth, file.size); @@ -219,7 +216,7 @@ export class AssetMediaService { } async downloadOriginal(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { @@ -234,7 +231,7 @@ export class AssetMediaService { } async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; @@ -257,7 +254,7 @@ export class AssetMediaService { } async playbackVideo(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index e9aefce910839..bfd3a0c4d26b5 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetResponseDto, @@ -39,15 +38,15 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService { - private access: AccessCore; private configCore: SystemConfigCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @@ -58,7 +57,6 @@ export class AssetService { @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetService.name); - this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @@ -109,7 +107,7 @@ export class AssetService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_READ, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] }); const asset = await this.assetRepository.getById( id, @@ -158,7 +156,7 @@ export class AssetService { } async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); @@ -182,7 +180,7 @@ export class AssetService { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids }); for (const id of ids) { await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); @@ -278,7 +276,7 @@ export class AssetService { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { const { ids, force } = dto; - await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); if (force) { await this.jobRepository.queueAll( @@ -294,7 +292,7 @@ export class AssetService { } async run(auth: AuthDto, dto: AssetJobsDto) { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const jobs: JobItem[] = []; diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 734ed9b7c353d..72db2b6eb56ce 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AuditDeletesDto, @@ -24,15 +23,14 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, @@ -41,7 +39,6 @@ export class AuditService { @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(AuditService.name); } @@ -52,7 +49,7 @@ export class AuditService { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise { const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const audits = await this.repository.getAfter(dto.after, { userIds: [userId], diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 1ff9e51576ba0..988b859ff882f 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -11,21 +10,19 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; +import { requireAccess } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; import { usePagination } from 'src/utils/pagination'; import { getPreferences } from 'src/utils/preferences'; @Injectable() export class DownloadService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(DownloadService.name); } @@ -76,7 +73,7 @@ export class DownloadService { } async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); @@ -119,20 +116,20 @@ export class DownloadService { if (dto.assetIds) { const assetIds = dto.assetIds; - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true }); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); } if (dto.albumId) { const albumId = dto.albumId; - await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); } if (dto.userId) { const userId = dto.userId; - await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), ); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index c8c44d04b3793..fb1ff49f0b456 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; @@ -7,18 +6,15 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class MemoryService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IMemoryRepository) private repository: IMemoryRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async search(auth: AuthDto) { const memories = await this.repository.search(auth.user.id); @@ -26,7 +22,7 @@ export class MemoryService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id); return mapMemory(memory); } @@ -35,7 +31,11 @@ export class MemoryService { // TODO validate type/data combination const assetIds = dto.assetIds || []; - const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds); + const allowedAssetIds = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: assetIds, + }); const memory = await this.repository.create({ ownerId: auth.user.id, type: dto.type, @@ -50,7 +50,7 @@ export class MemoryService { } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); const memory = await this.repository.update({ id, @@ -63,14 +63,14 @@ export class MemoryService { } async remove(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] }); await this.repository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); - const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const repos = { access: this.access, bulk: this.repository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); const hasSuccess = results.find(({ success }) => success); @@ -82,9 +82,9 @@ export class MemoryService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const repos = { access: this.access, bulk: this.repository }; const results = await removeAssets(auth, repos, { parentId: id, assetIds: dto.ids, diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index c20d43db5d1ca..4b7cd4c516e42 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; @@ -7,16 +6,14 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class PartnerService { - private access: AccessCore; constructor( @Inject(IPartnerRepository) private repository: IPartnerRepository, - @Inject(IAccessRepository) accessRepository: IAccessRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + @Inject(IAccessRepository) private access: IAccessRepository, + ) {} async create(auth: AuthDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; @@ -49,7 +46,7 @@ export class PartnerService { } async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { - await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById); + await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 3fc34d8b1561a..6f2283b72c6e8 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ImageFormat } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; @@ -50,6 +49,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf import { ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; @@ -59,12 +59,11 @@ import { IsNull } from 'typeorm'; @Injectable() export class PersonService { - private access: AccessCore; private configCore: SystemConfigCore; private storageCore: StorageCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, @@ -77,7 +76,6 @@ export class PersonService { @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(PersonService.name); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( @@ -114,7 +112,7 @@ export class PersonService { } async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; @@ -122,7 +120,7 @@ export class PersonService { const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); for (const face of faces) { - await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] }); if (person.faceAssetId === null) { changeFeaturePhoto.push(person.id); } @@ -143,9 +141,8 @@ export class PersonService { } async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); - - await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); + await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); const face = await this.repository.getFaceById(dto.id); const person = await this.findOrFail(personId); @@ -161,7 +158,7 @@ export class PersonService { } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] }); const faces = await this.repository.getFaces(dto.id); return faces.map((asset) => mapFaces(asset, auth)); } @@ -188,17 +185,17 @@ export class PersonService { } async getById(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.findOrFail(id).then(mapPerson); } async getStatistics(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.repository.getStatistics(id); } async getThumbnail(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); const person = await this.repository.getById(id); if (!person || !person.thumbnailPath) { throw new NotFoundException(); @@ -212,7 +209,7 @@ export class PersonService { } async getAssets(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); const assets = await this.repository.getAssets(id); return assets.map((asset) => mapAsset(asset)); } @@ -227,13 +224,13 @@ export class PersonService { } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; // TODO: set by faceId directly let faceId: string | undefined = undefined; if (assetId) { - await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] }); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); if (!face) { throw new BadRequestException('Invalid assetId for feature face'); @@ -587,13 +584,17 @@ export class PersonService { throw new BadRequestException('Cannot merge a person into themselves'); } - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); let primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; const results: BulkIdResponseDto[] = []; - const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds); + const allowedIds = await checkAccess(this.access, { + auth, + permission: Permission.PERSON_MERGE, + ids: mergeIds, + }); for (const mergeId of mergeIds) { const hasAccess = allowedIds.has(mergeId); diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 01cf3a5c0906a..47abf3c380246 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { Permission } from 'src/enum'; @@ -8,18 +7,16 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class SessionService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ISessionRepository) private sessionRepository: ISessionRepository, ) { this.logger.setContext(SessionService.name); - this.access = AccessCore.create(accessRepository); } async handleCleanup() { @@ -47,7 +44,7 @@ export class SessionService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); await this.sessionRepository.delete(id); } diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 4b6768e02879b..54c7fdf25bed7 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; @@ -21,22 +20,21 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService { - private access: AccessCore; private configCore: SystemConfigCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, ) { this.logger.setContext(SharedLinkService.name); - this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @@ -69,7 +67,7 @@ export class SharedLinkService { if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] }); break; } @@ -78,7 +76,7 @@ export class SharedLinkService { throw new BadRequestException('Invalid assetIds'); } - await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds }); break; } @@ -139,7 +137,11 @@ export class SharedLinkService { const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); - const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: notPresentAssetIds, + }); const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index 70234dee567c7..bebc8517d6b7a 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; @@ -7,18 +6,15 @@ import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class StackService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IStackRepository) private stackRepository: IStackRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async search(auth: AuthDto, dto: StackSearchDto): Promise { const stacks = await this.stackRepository.search({ @@ -30,7 +26,7 @@ export class StackService { } async create(auth: AuthDto, dto: StackCreateDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); @@ -40,13 +36,13 @@ export class StackService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.STACK_READ, id); + await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] }); const stack = await this.findOrFail(id); return mapStack(stack, { auth }); } async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.STACK_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] }); const stack = await this.findOrFail(id); if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { throw new BadRequestException('Primary asset must be in the stack'); @@ -60,14 +56,14 @@ export class StackService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.STACK_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] }); await this.stackRepository.delete(id); this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); } async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.STACK_DELETE, dto.ids); + await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); await this.stackRepository.deleteAll(dto.ids); this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 6af43d6ebc41f..7da3fbd9be58d 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,7 +1,6 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; @@ -10,27 +9,24 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { setIsEqual } from 'src/utils/set'; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export class SyncService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IAuditRepository) private auditRepository: IAuditRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { // mobile implementation is faster if this is a single id const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const assets = await this.assetRepository.getAllForUserFullSync({ ownerId: userId, updatedUntil: dto.updatedUntil, @@ -54,7 +50,7 @@ export class SyncService { return FULL_SYNC; } - await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds }); const limit = 10_000; const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index 44f1136da100e..052565fca99f5 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; @@ -7,18 +6,15 @@ import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; export class TimelineService { - private accessCore: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private repository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - ) { - this.accessCore = AccessCore.create(accessRepository); - } + ) {} async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); @@ -60,15 +56,15 @@ export class TimelineService { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { if (dto.albumId) { - await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { dto.userId = dto.userId || auth.user.id; } if (dto.userId) { - await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); if (dto.isArchived !== false) { - await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); + await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); } } diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 7e2582fd24b34..f64aef0516a1e 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,6 +1,5 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore } from 'src/cores/access.core'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { Permission } from 'src/enum'; @@ -8,23 +7,20 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; +import { requireAccess } from 'src/utils/access'; import { usePagination } from 'src/utils/pagination'; export class TrashService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { const { ids } = dto; - await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_RESTORE, ids }); await this.restoreAndSend(auth, ids); } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index cd24087d9bd2b..9367b0987e39e 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -1,5 +1,9 @@ -import { Permission } from 'src/enum'; -import { setIsSuperset } from 'src/utils/set'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { AlbumUserRole, Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set'; export type GrantedRequest = { requested: Permission[]; @@ -13,3 +17,268 @@ export const isGranted = ({ requested, current }: GrantedRequest) => { return setIsSuperset(new Set(current), new Set(requested)); }; + +export type AccessRequest = { + auth: AuthDto; + permission: Permission; + ids: Set | string[]; +}; + +type SharedLinkAccessRequest = { sharedLink: SharedLinkEntity; permission: Permission; ids: Set }; +type OtherAccessRequest = { auth: AuthDto; permission: Permission; ids: Set }; + +export const requireUploadAccess = (auth: AuthDto | null): AuthDto => { + if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { + throw new UnauthorizedException(); + } + return auth; +}; + +export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => { + const allowedIds = await checkAccess(access, request); + if (!setIsEqual(new Set(request.ids), allowedIds)) { + throw new BadRequestException(`Not found or no ${request.permission} access`); + } +}; + +export const checkAccess = async (access: IAccessRepository, { ids, auth, permission }: AccessRequest) => { + const idSet = Array.isArray(ids) ? new Set(ids) : ids; + if (idSet.size === 0) { + return new Set(); + } + + return auth.sharedLink + ? checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet }) + : checkOtherAccess(access, { auth, permission, ids: idSet }); +}; + +const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedLinkAccessRequest) => { + const { sharedLink, permission, ids } = request; + const sharedLinkId = sharedLink.id; + + switch (permission) { + case Permission.ASSET_READ: { + return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ASSET_VIEW: { + return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ASSET_DOWNLOAD: { + return sharedLink.allowDownload ? await access.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + case Permission.ASSET_UPLOAD: { + return sharedLink.allowUpload ? ids : new Set(); + } + + case Permission.ASSET_SHARE: { + // TODO: fix this to not use sharedLink.userId for access control + return await access.asset.checkOwnerAccess(sharedLink.userId, ids); + } + + case Permission.ALBUM_READ: { + return await access.album.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ALBUM_DOWNLOAD: { + return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + case Permission.ALBUM_ADD_ASSET: { + return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + default: { + return new Set(); + } + } +}; + +const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest) => { + const { auth, permission, ids } = request; + + switch (permission) { + // uses album id + case Permission.ACTIVITY_CREATE: { + return await access.activity.checkCreateAccess(auth.user.id, ids); + } + + // uses activity id + case Permission.ACTIVITY_DELETE: { + const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids); + const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isAlbumOwner); + } + + case Permission.ASSET_READ: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_SHARE: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.ASSET_VIEW: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_DOWNLOAD: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_UPDATE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ASSET_DELETE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ASSET_RESTORE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_READ: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.VIEWER, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_ADD_ASSET: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.EDITOR, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_UPDATE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_DELETE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_SHARE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_DOWNLOAD: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.VIEWER, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_REMOVE_ASSET: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.EDITOR, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ASSET_UPLOAD: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.ARCHIVE_READ: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.AUTH_DEVICE_DELETE: { + return await access.authDevice.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.TIMELINE_READ: { + const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.TIMELINE_DOWNLOAD: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.MEMORY_READ: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_UPDATE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_READ: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_UPDATE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_MERGE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_CREATE: { + return access.person.checkFaceOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_REASSIGN: { + return access.person.checkFaceOwnerAccess(auth.user.id, ids); + } + + case Permission.PARTNER_UPDATE: { + return await access.partner.checkUpdateAccess(auth.user.id, ids); + } + + case Permission.STACK_READ: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_UPDATE: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_DELETE: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + default: { + return new Set(); + } + } +}; diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 31f708611ddb6..26d5f9292ebeb 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,10 +1,10 @@ -import { AccessCore } from 'src/cores/access.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { checkAccess } from 'src/utils/access'; export interface IBulkAsset { getAssetIds: (id: string, assetIds: string[]) => Promise>; @@ -23,15 +23,17 @@ export const getAssetFiles = (files?: AssetFileEntity[]) => ({ export const addAssets = async ( auth: AuthDto, - repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + repositories: { access: IAccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[] }, ) => { - const { accessRepository, repository } = repositories; - const access = AccessCore.create(accessRepository); - - const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); + const { access, bulk } = repositories; + const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); - const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await checkAccess(access, { + auth, + permission: Permission.ASSET_SHARE, + ids: notPresentAssetIds, + }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -53,7 +55,7 @@ export const addAssets = async ( const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); if (newAssetIds.length > 0) { - await repository.addAssetIds(dto.parentId, newAssetIds); + await bulk.addAssetIds(dto.parentId, newAssetIds); } return results; @@ -61,18 +63,17 @@ export const addAssets = async ( export const removeAssets = async ( auth: AuthDto, - repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + repositories: { access: IAccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission }, ) => { - const { accessRepository, repository } = repositories; - const access = AccessCore.create(accessRepository); + const { access, bulk } = repositories; // check if the user can always remove from the parent album, memory, etc. - const canAlwaysRemove = await access.checkAccess(auth, dto.canAlwaysRemove, [dto.parentId]); - const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); + const canAlwaysRemove = await checkAccess(access, { auth, permission: dto.canAlwaysRemove, ids: [dto.parentId] }); + const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const allowedAssetIds = canAlwaysRemove.has(dto.parentId) ? existingAssetIds - : await access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds); + : await checkAccess(access, { auth, permission: Permission.ASSET_SHARE, ids: existingAssetIds }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -94,7 +95,7 @@ export const removeAssets = async ( const removedIds = results.filter(({ success }) => success).map(({ id }) => id); if (removedIds.length > 0) { - await repository.removeAssetIds(dto.parentId, removedIds); + await bulk.removeAssetIds(dto.parentId, removedIds); } return results; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index befe9c77a8920..c9db8cd76a7b6 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,4 +1,3 @@ -import { AccessCore } from 'src/cores/access.core'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Mocked, vitest } from 'vitest'; @@ -14,11 +13,7 @@ export interface IAccessRepositoryMock { timeline: Mocked; } -export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { - if (reset) { - AccessCore.reset(); - } - +export const newAccessRepositoryMock = (): IAccessRepositoryMock => { return { activity: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), From cde0458dc858b0923638b503e1a0bc2759d2c72e Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:50:09 -0400 Subject: [PATCH 4/6] fix(server): coverage reports (#11925) --- server/vitest.config.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/vitest.config.mjs b/server/vitest.config.mjs index 8811dafaf81df..3c0ea00c84c7a 100644 --- a/server/vitest.config.mjs +++ b/server/vitest.config.mjs @@ -6,6 +6,16 @@ export default defineConfig({ test: { root: './', globals: true, + coverage: { + provider: 'v8', + include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**'], + thresholds: { + lines: 80, + statements: 80, + branches: 85, + functions: 85, + }, + }, server: { deps: { fallbackCJS: true, From ef9a06be5c6a06e5875bed6baed4a10920c2300a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:50:36 -0400 Subject: [PATCH 5/6] fix(server): album statistics endpoint (#11924) --- e2e/src/api/specs/album.e2e-spec.ts | 6 +- mobile/openapi/README.md | 4 +- mobile/openapi/lib/api.dart | 2 +- mobile/openapi/lib/api/albums_api.dart | 82 +++++++++---------- mobile/openapi/lib/api_client.dart | 4 +- ...art => album_statistics_response_dto.dart} | 36 ++++---- open-api/immich-openapi-specs.json | 44 +++++----- open-api/typescript-sdk/src/fetch-client.ts | 8 +- server/src/controllers/album.controller.ts | 14 ++-- server/src/dtos/album.dto.ts | 2 +- server/src/services/album.service.spec.ts | 4 +- server/src/services/album.service.ts | 4 +- .../side-bar/more-information-albums.svelte | 8 +- .../side-bar/side-bar.svelte | 4 +- 14 files changed, 111 insertions(+), 111 deletions(-) rename mobile/openapi/lib/model/{album_count_response_dto.dart => album_statistics_response_dto.dart} (63%) diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 2a35eb3c92d03..9e925c40210c1 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -344,16 +344,16 @@ describe('/albums', () => { }); }); - describe('GET /albums/count', () => { + describe('GET /albums/statistics', () => { it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums/count'); + const { status, body } = await request(app).get('/albums/statistics'); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); it('should return total count of albums the user has access to', async () => { const { status, body } = await request(app) - .get('/albums/count') + .get('/albums/statistics') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f2effe1c2060b..c49b5052d865b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -86,8 +86,8 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**addUsersToAlbum**](doc//AlbumsApi.md#adduserstoalbum) | **PUT** /albums/{id}/users | *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | -*AlbumsApi* | [**getAlbumCount**](doc//AlbumsApi.md#getalbumcount) | **GET** /albums/count | *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | +*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | @@ -265,8 +265,8 @@ Class | Method | HTTP request | Description - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) + - [AlbumStatisticsResponseDto](doc//AlbumStatisticsResponseDto.md) - [AlbumUserAddDto](doc//AlbumUserAddDto.md) - [AlbumUserCreateDto](doc//AlbumUserCreateDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 6ee06d53042bb..a6f860dda27e7 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -73,8 +73,8 @@ part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; part 'model/admin_onboarding_update_dto.dart'; -part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; +part 'model/album_statistics_response_dto.dart'; part 'model/album_user_add_dto.dart'; part 'model/album_user_create_dto.dart'; part 'model/album_user_response_dto.dart'; diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index fb81c04616742..eb2bb7c0bd9cb 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -218,47 +218,6 @@ class AlbumsApi { } } - /// Performs an HTTP 'GET /albums/count' operation and returns the [Response]. - Future getAlbumCountWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/albums/count'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future getAlbumCount() async { - final response = await getAlbumCountWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumCountResponseDto',) as AlbumCountResponseDto; - - } - return null; - } - /// Performs an HTTP 'GET /albums/{id}' operation and returns the [Response]. /// Parameters: /// @@ -322,6 +281,47 @@ class AlbumsApi { return null; } + /// Performs an HTTP 'GET /albums/statistics' operation and returns the [Response]. + Future getAlbumStatisticsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/albums/statistics'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAlbumStatistics() async { + final response = await getAlbumStatisticsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumStatisticsResponseDto',) as AlbumStatisticsResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /albums' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 935324272d7b5..c9ed2a508d78b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -201,10 +201,10 @@ class ApiClient { return AddUsersDto.fromJson(value); case 'AdminOnboardingUpdateDto': return AdminOnboardingUpdateDto.fromJson(value); - case 'AlbumCountResponseDto': - return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': return AlbumResponseDto.fromJson(value); + case 'AlbumStatisticsResponseDto': + return AlbumStatisticsResponseDto.fromJson(value); case 'AlbumUserAddDto': return AlbumUserAddDto.fromJson(value); case 'AlbumUserCreateDto': diff --git a/mobile/openapi/lib/model/album_count_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart similarity index 63% rename from mobile/openapi/lib/model/album_count_response_dto.dart rename to mobile/openapi/lib/model/album_statistics_response_dto.dart index 531a17a0838cc..90dbe520163bb 100644 --- a/mobile/openapi/lib/model/album_count_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class AlbumCountResponseDto { - /// Returns a new [AlbumCountResponseDto] instance. - AlbumCountResponseDto({ +class AlbumStatisticsResponseDto { + /// Returns a new [AlbumStatisticsResponseDto] instance. + AlbumStatisticsResponseDto({ required this.notShared, required this.owned, required this.shared, @@ -25,7 +25,7 @@ class AlbumCountResponseDto { int shared; @override - bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto && + bool operator ==(Object other) => identical(this, other) || other is AlbumStatisticsResponseDto && other.notShared == notShared && other.owned == owned && other.shared == shared; @@ -38,7 +38,7 @@ class AlbumCountResponseDto { (shared.hashCode); @override - String toString() => 'AlbumCountResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; + String toString() => 'AlbumStatisticsResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; Map toJson() { final json = {}; @@ -48,14 +48,14 @@ class AlbumCountResponseDto { return json; } - /// Returns a new [AlbumCountResponseDto] instance and imports its values from + /// Returns a new [AlbumStatisticsResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AlbumCountResponseDto? fromJson(dynamic value) { + static AlbumStatisticsResponseDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return AlbumCountResponseDto( + return AlbumStatisticsResponseDto( notShared: mapValueOfType(json, r'notShared')!, owned: mapValueOfType(json, r'owned')!, shared: mapValueOfType(json, r'shared')!, @@ -64,11 +64,11 @@ class AlbumCountResponseDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AlbumCountResponseDto.fromJson(row); + final value = AlbumStatisticsResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -77,12 +77,12 @@ class AlbumCountResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AlbumCountResponseDto.fromJson(entry.value); + final value = AlbumStatisticsResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -91,14 +91,14 @@ class AlbumCountResponseDto { return map; } - // maps a json object with a list of AlbumCountResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AlbumStatisticsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AlbumCountResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AlbumStatisticsResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a9b08fc400646..16c25562a6b4d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -660,16 +660,16 @@ ] } }, - "/albums/count": { + "/albums/statistics": { "get": { - "operationId": "getAlbumCount", + "operationId": "getAlbumStatistics", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AlbumCountResponseDto" + "$ref": "#/components/schemas/AlbumStatisticsResponseDto" } } }, @@ -7505,25 +7505,6 @@ ], "type": "object" }, - "AlbumCountResponseDto": { - "properties": { - "notShared": { - "type": "integer" - }, - "owned": { - "type": "integer" - }, - "shared": { - "type": "integer" - } - }, - "required": [ - "notShared", - "owned", - "shared" - ], - "type": "object" - }, "AlbumResponseDto": { "properties": { "albumName": { @@ -7611,6 +7592,25 @@ ], "type": "object" }, + "AlbumStatisticsResponseDto": { + "properties": { + "notShared": { + "type": "integer" + }, + "owned": { + "type": "integer" + }, + "shared": { + "type": "integer" + } + }, + "required": [ + "notShared", + "owned", + "shared" + ], + "type": "object" + }, "AlbumUserAddDto": { "properties": { "role": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 8b503821f7af1..c6d8d3e3ba149 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -268,7 +268,7 @@ export type CreateAlbumDto = { assetIds?: string[]; description?: string; }; -export type AlbumCountResponseDto = { +export type AlbumStatisticsResponseDto = { notShared: number; owned: number; shared: number; @@ -1369,11 +1369,11 @@ export function createAlbum({ createAlbumDto }: { body: createAlbumDto }))); } -export function getAlbumCount(opts?: Oazapfts.RequestOpts) { +export function getAlbumStatistics(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AlbumCountResponseDto; - }>("/albums/count", { + data: AlbumStatisticsResponseDto; + }>("/albums/statistics", { ...opts })); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 06f2066c29f85..49ec5a82ea44c 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -2,9 +2,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@ import { ApiTags } from '@nestjs/swagger'; import { AddUsersDto, - AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, + AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -22,12 +22,6 @@ import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; export class AlbumController { constructor(private service: AlbumService) {} - @Get('count') - @Authenticated({ permission: Permission.ALBUM_STATISTICS }) - getAlbumCount(@Auth() auth: AuthDto): Promise { - return this.service.getCount(auth); - } - @Get() @Authenticated({ permission: Permission.ALBUM_READ }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { @@ -40,6 +34,12 @@ export class AlbumController { return this.service.create(auth, dto); } + @Get('statistics') + @Authenticated({ permission: Permission.ALBUM_STATISTICS }) + getAlbumStatistics(@Auth() auth: AuthDto): Promise { + return this.service.getStatistics(auth); + } + @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) @Get(':id') getAlbumInfo( diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 8f5c996caee17..b12847ee62537 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -95,7 +95,7 @@ export class GetAlbumsDto { assetId?: string; } -export class AlbumCountResponseDto { +export class AlbumStatisticsResponseDto { @ApiProperty({ type: 'integer' }) owned!: number; diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 406302ece96d4..16b2d97fdd4f4 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -43,12 +43,12 @@ describe(AlbumService.name, () => { expect(sut).toBeDefined(); }); - describe('getCount', () => { + describe('getStatistics', () => { it('should get the album count', async () => { albumMock.getOwned.mockResolvedValue([]); albumMock.getShared.mockResolvedValue([]); albumMock.getNotShared.mockResolvedValue([]); - await expect(sut.getCount(authStub.admin)).resolves.toEqual({ + await expect(sut.getStatistics(authStub.admin)).resolves.toEqual({ owned: 0, shared: 0, notShared: 0, diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 02dab1a74024a..b2b5ea32a2c93 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AddUsersDto, - AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, + AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -37,7 +37,7 @@ export class AlbumService { @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, ) {} - async getCount(auth: AuthDto): Promise { + async getStatistics(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ this.albumRepository.getOwned(auth.user.id), this.albumRepository.getShared(auth.user.id), diff --git a/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte b/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte index e47daaf86b767..68c58ab155e6d 100644 --- a/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte +++ b/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte @@ -1,13 +1,13 @@