diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts deleted file mode 100644 index d8d7b4e807ab9..0000000000000 --- a/server/src/interfaces/access.interface.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AlbumUserRole } from 'src/enum'; - -export const IAccessRepository = 'IAccessRepository'; - -export interface IAccessRepository { - activity: { - checkOwnerAccess(userId: string, activityIds: Set): Promise>; - checkAlbumOwnerAccess(userId: string, activityIds: Set): Promise>; - checkCreateAccess(userId: string, albumIds: Set): Promise>; - }; - - asset: { - checkOwnerAccess(userId: string, assetIds: Set): Promise>; - checkAlbumAccess(userId: string, assetIds: Set): Promise>; - checkPartnerAccess(userId: string, assetIds: Set): Promise>; - checkSharedLinkAccess(sharedLinkId: string, assetIds: Set): Promise>; - }; - - authDevice: { - checkOwnerAccess(userId: string, deviceIds: Set): Promise>; - }; - - album: { - checkOwnerAccess(userId: string, albumIds: Set): Promise>; - checkSharedAlbumAccess(userId: string, albumIds: Set, access: AlbumUserRole): Promise>; - checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise>; - }; - - timeline: { - checkPartnerAccess(userId: string, partnerIds: Set): Promise>; - }; - - memory: { - checkOwnerAccess(userId: string, memoryIds: Set): Promise>; - }; - - person: { - checkFaceOwnerAccess(userId: string, assetFaceId: Set): Promise>; - checkOwnerAccess(userId: string, personIds: Set): Promise>; - }; - - partner: { - checkUpdateAccess(userId: string, partnerIds: Set): Promise>; - }; - - stack: { - checkOwnerAccess(userId: string, stackIds: Set): Promise>; - }; - - tag: { - checkOwnerAccess(userId: string, tagIds: Set): Promise>; - }; -} diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 4d32950d85ebf..9fa8b6243c892 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -1,33 +1,18 @@ -import { Injectable } from '@nestjs/common'; import { Kysely, sql } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; - import { AlbumUserRole } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; import { asUuid } from 'src/utils/database'; -type IActivityAccess = IAccessRepository['activity']; -type IAlbumAccess = IAccessRepository['album']; -type IAssetAccess = IAccessRepository['asset']; -type IAuthDeviceAccess = IAccessRepository['authDevice']; -type IMemoryAccess = IAccessRepository['memory']; -type IPersonAccess = IAccessRepository['person']; -type IPartnerAccess = IAccessRepository['partner']; -type IStackAccess = IAccessRepository['stack']; -type ITagAccess = IAccessRepository['tag']; -type ITimelineAccess = IAccessRepository['timeline']; - -@Injectable() -class ActivityAccess implements IActivityAccess { +class ActivityAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, activityIds: Set): Promise> { + async checkOwnerAccess(userId: string, activityIds: Set) { if (activityIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -41,9 +26,9 @@ class ActivityAccess implements IActivityAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkAlbumOwnerAccess(userId: string, activityIds: Set): Promise> { + async checkAlbumOwnerAccess(userId: string, activityIds: Set) { if (activityIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -58,9 +43,9 @@ class ActivityAccess implements IActivityAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkCreateAccess(userId: string, albumIds: Set): Promise> { + async checkCreateAccess(userId: string, albumIds: Set) { if (albumIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -77,14 +62,14 @@ class ActivityAccess implements IActivityAccess { } } -class AlbumAccess implements IAlbumAccess { +class AlbumAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, albumIds: Set): Promise> { + async checkOwnerAccess(userId: string, albumIds: Set) { if (albumIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -99,9 +84,9 @@ class AlbumAccess implements IAlbumAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkSharedAlbumAccess(userId: string, albumIds: Set, access: AlbumUserRole): Promise> { + async checkSharedAlbumAccess(userId: string, albumIds: Set, access: AlbumUserRole) { if (albumIds.size === 0) { - return new Set(); + return new Set(); } const accessRole = @@ -122,9 +107,9 @@ class AlbumAccess implements IAlbumAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise> { + async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set) { if (albumIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -139,14 +124,14 @@ class AlbumAccess implements IAlbumAccess { } } -class AssetAccess implements IAssetAccess { +class AssetAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkAlbumAccess(userId: string, assetIds: Set): Promise> { + async checkAlbumAccess(userId: string, assetIds: Set) { if (assetIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -182,9 +167,9 @@ class AssetAccess implements IAssetAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, assetIds: Set): Promise> { + async checkOwnerAccess(userId: string, assetIds: Set) { if (assetIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -198,9 +183,9 @@ class AssetAccess implements IAssetAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkPartnerAccess(userId: string, assetIds: Set): Promise> { + async checkPartnerAccess(userId: string, assetIds: Set) { if (assetIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -221,9 +206,9 @@ class AssetAccess implements IAssetAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set): Promise> { + async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set) { if (assetIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -273,14 +258,14 @@ class AssetAccess implements IAssetAccess { } } -class AuthDeviceAccess implements IAuthDeviceAccess { +class AuthDeviceAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, deviceIds: Set): Promise> { + async checkOwnerAccess(userId: string, deviceIds: Set) { if (deviceIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -293,14 +278,14 @@ class AuthDeviceAccess implements IAuthDeviceAccess { } } -class StackAccess implements IStackAccess { +class StackAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, stackIds: Set): Promise> { + async checkOwnerAccess(userId: string, stackIds: Set) { if (stackIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -313,14 +298,14 @@ class StackAccess implements IStackAccess { } } -class TimelineAccess implements ITimelineAccess { +class TimelineAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { + async checkPartnerAccess(userId: string, partnerIds: Set) { if (partnerIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -333,14 +318,14 @@ class TimelineAccess implements ITimelineAccess { } } -class MemoryAccess implements IMemoryAccess { +class MemoryAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, memoryIds: Set): Promise> { + async checkOwnerAccess(userId: string, memoryIds: Set) { if (memoryIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -354,14 +339,14 @@ class MemoryAccess implements IMemoryAccess { } } -class PersonAccess implements IPersonAccess { +class PersonAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, personIds: Set): Promise> { + async checkOwnerAccess(userId: string, personIds: Set) { if (personIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -375,9 +360,9 @@ class PersonAccess implements IPersonAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkFaceOwnerAccess(userId: string, assetFaceIds: Set): Promise> { + async checkFaceOwnerAccess(userId: string, assetFaceIds: Set) { if (assetFaceIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -393,14 +378,14 @@ class PersonAccess implements IPersonAccess { } } -class PartnerAccess implements IPartnerAccess { +class PartnerAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkUpdateAccess(userId: string, partnerIds: Set): Promise> { + async checkUpdateAccess(userId: string, partnerIds: Set) { if (partnerIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -413,14 +398,14 @@ class PartnerAccess implements IPartnerAccess { } } -class TagAccess implements ITagAccess { +class TagAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, tagIds: Set): Promise> { + async checkOwnerAccess(userId: string, tagIds: Set) { if (tagIds.size === 0) { - return new Set(); + return new Set(); } return this.db @@ -433,17 +418,17 @@ class TagAccess implements ITagAccess { } } -export class AccessRepository implements IAccessRepository { - activity: IActivityAccess; - album: IAlbumAccess; - asset: IAssetAccess; - authDevice: IAuthDeviceAccess; - memory: IMemoryAccess; - person: IPersonAccess; - partner: IPartnerAccess; - stack: IStackAccess; - tag: ITagAccess; - timeline: ITimelineAccess; +export class AccessRepository { + activity: ActivityAccess; + album: AlbumAccess; + asset: AssetAccess; + authDevice: AuthDeviceAccess; + memory: MemoryAccess; + person: PersonAccess; + partner: PartnerAccess; + stack: StackAccess; + tag: TagAccess; + timeline: TimelineAccess; constructor(@InjectKysely() db: Kysely) { this.activity = new ActivityAccess(db); diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index c48233f08fd64..8f691ac9e706d 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -1,4 +1,3 @@ -import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; @@ -78,11 +77,11 @@ import { ViewRepository } from 'src/repositories/view-repository'; export const repositories = [ // + AccessRepository, ActivityRepository, ]; export const providers = [ - { provide: IAccessRepository, useClass: AccessRepository }, { provide: IAlbumRepository, useClass: AlbumRepository }, { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 9e024daacd578..fa77bcc38884b 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -6,7 +6,6 @@ import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; -import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; @@ -44,6 +43,7 @@ import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { IViewRepository } from 'src/interfaces/view.interface'; +import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; @@ -53,7 +53,7 @@ export class BaseService { constructor( @Inject(ILoggerRepository) protected logger: ILoggerRepository, - @Inject(IAccessRepository) protected accessRepository: IAccessRepository, + protected accessRepository: AccessRepository, protected activityRepository: ActivityRepository, @Inject(IAuditRepository) protected auditRepository: IAuditRepository, @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository, diff --git a/server/src/types.ts b/server/src/types.ts index 0d3b037f9e72a..a6a070dc637f7 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,5 +1,6 @@ import { UserEntity } from 'src/entities/user.entity'; import { Permission } from 'src/enum'; +import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; export type AuthApiKey = { @@ -12,6 +13,7 @@ export type AuthApiKey = { export type RepositoryInterface = Pick; export type IActivityRepository = RepositoryInterface; +export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface }; export type ActivityItem = | Awaited> diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index d3219a1a6c4b6..cb917373495d2 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -2,7 +2,7 @@ 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 { AccessRepository } from 'src/repositories/access.repository'; import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set'; export type GrantedRequest = { @@ -34,7 +34,7 @@ export const requireUploadAccess = (auth: AuthDto | null): AuthDto => { return auth; }; -export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => { +export const requireAccess = async (access: AccessRepository, 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`); @@ -42,7 +42,7 @@ export const requireAccess = async (access: IAccessRepository, request: AccessRe }; export const checkAccess = async ( - access: IAccessRepository, + access: AccessRepository, { ids, auth, permission }: AccessRequest, ): Promise> => { const idSet = Array.isArray(ids) ? new Set(ids) : ids; @@ -56,7 +56,7 @@ export const checkAccess = async ( }; const checkSharedLinkAccess = async ( - access: IAccessRepository, + access: AccessRepository, request: SharedLinkAccessRequest, ): Promise> => { const { sharedLink, permission, ids } = request; @@ -102,7 +102,7 @@ const checkSharedLinkAccess = async ( } }; -const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest): Promise> => { +const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRequest): Promise> => { const { auth, permission, ids } = request; switch (permission) { diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index f8bed5485f8b1..39593a77f3e23 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -5,12 +5,12 @@ import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, AssetType, Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { AuthRequest } from 'src/middleware/auth.guard'; import { ImmichFile } from 'src/middleware/file-upload.interceptor'; +import { AccessRepository } from 'src/repositories/access.repository'; import { UploadFile } from 'src/services/asset-media.service'; import { checkAccess } from 'src/utils/access'; @@ -31,7 +31,7 @@ export const getAssetFiles = (files?: AssetFileEntity[]) => ({ export const addAssets = async ( auth: AuthDto, - repositories: { access: IAccessRepository; bulk: IBulkAsset }, + repositories: { access: AccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[] }, ) => { const { access, bulk } = repositories; @@ -71,7 +71,7 @@ export const addAssets = async ( export const removeAssets = async ( auth: AuthDto, - repositories: { access: IAccessRepository; bulk: IBulkAsset }, + repositories: { access: AccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission }, ) => { const { access, bulk } = repositories; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 9e9bf5406bd6d..23886e049559d 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,18 +1,7 @@ -import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAccessRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; -export interface IAccessRepositoryMock { - activity: Mocked; - asset: Mocked; - album: Mocked; - authDevice: Mocked; - memory: Mocked; - person: Mocked; - partner: Mocked; - stack: Mocked; - timeline: Mocked; - tag: Mocked; -} +export type IAccessRepositoryMock = { [K in keyof IAccessRepository]: Mocked }; export const newAccessRepositoryMock = (): IAccessRepositoryMock => { return { diff --git a/server/test/utils.ts b/server/test/utils.ts index bc0ada3259e1d..d6d1cd71beb99 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -3,9 +3,10 @@ import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import { ImmichWorker } from 'src/enum'; import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { BaseService } from 'src/services/base.service'; -import { IActivityRepository } from 'src/types'; +import { IAccessRepository, IActivityRepository } from 'src/types'; import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; @@ -105,7 +106,7 @@ export const newTestService = ( const sut = new Service( loggerMock, - accessMock, + accessMock as IAccessRepository as AccessRepository, activityMock as IActivityRepository as ActivityRepository, auditMock, albumMock,