diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index 7a54b4de79..9642f1c5f3 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -9,9 +9,9 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use import { DownloadService } from '../../modules/download/download.service'; import { AlbumRepository, IAlbumRepository } from '../album/album-repository'; import { + IAccessRepository, ICryptoRepository, IJobRepository, - IPartnerRepository, ISharedLinkRepository, IStorageRepository, JobName, @@ -20,6 +20,7 @@ import { assetEntityStub, authStub, fileStub, + newAccessRepositoryMock, newCryptoRepositoryMock, newJobRepositoryMock, newSharedLinkRepositoryMock, @@ -131,10 +132,10 @@ const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => { describe('AssetService', () => { let sut: AssetService; let a: Repository; // TO BE DELETED AFTER FINISHED REFACTORING + let accessMock: jest.Mocked; let assetRepositoryMock: jest.Mocked; let albumRepositoryMock: jest.Mocked; let downloadServiceMock: jest.Mocked>; - let partnerRepositoryMock: jest.Mocked; let sharedLinkRepositoryMock: jest.Mocked; let cryptoMock: jest.Mocked; let jobMock: jest.Mocked; @@ -173,12 +174,14 @@ describe('AssetService', () => { downloadArchive: jest.fn(), }; + accessMock = newAccessRepositoryMock(); sharedLinkRepositoryMock = newSharedLinkRepositoryMock(); jobMock = newJobRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); storageMock = newStorageRepositoryMock(); sut = new AssetService( + accessMock, assetRepositoryMock, albumRepositoryMock, a, @@ -187,7 +190,6 @@ describe('AssetService', () => { jobMock, cryptoMock, storageMock, - partnerRepositoryMock, ); when(assetRepositoryMock.get) diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 57fbfcda17..6ec4fc7039 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -25,12 +25,12 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon import { AssetResponseDto, getLivePhotoMotionFilename, + IAccessRepository, ImmichReadStream, IStorageRepository, JobName, mapAsset, mapAssetWithoutExif, - PartnerCore, } from '@app/domain'; import { CreateAssetDto, UploadFile } from './dto/create-asset.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; @@ -55,7 +55,6 @@ import { DownloadService } from '../../modules/download/download.service'; import { DownloadDto } from './dto/download-library.dto'; import { IAlbumRepository } from '../album/album-repository'; import { SharedLinkCore } from '@app/domain'; -import { IPartnerRepository } from '@app/domain'; import { ISharedLinkRepository } from '@app/domain'; import { DownloadFilesDto } from './dto/download-files.dto'; import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; @@ -82,9 +81,9 @@ export class AssetService { readonly logger = new Logger(AssetService.name); private shareCore: SharedLinkCore; private assetCore: AssetCore; - private partnerCore: PartnerCore; constructor( + @Inject(IAccessRepository) private accessRepository: IAccessRepository, @Inject(IAssetRepository) private _assetRepository: IAssetRepository, @Inject(IAlbumRepository) private _albumRepository: IAlbumRepository, @InjectRepository(AssetEntity) @@ -94,11 +93,9 @@ export class AssetService { @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, ) { this.assetCore = new AssetCore(_assetRepository, jobRepository); this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository); - this.partnerCore = new PartnerCore(partnerRepository); } public async uploadFile( @@ -581,7 +578,7 @@ export class AssetService { } // Step 3: Check if any partner owns the asset - const canAccess = await this.partnerCore.hasAssetAccess(assetId, authUser.id); + const canAccess = await this.accessRepository.hasPartnerAssetAccess(authUser.id, assetId); if (canAccess) { continue; } @@ -601,7 +598,8 @@ export class AssetService { private async checkUserAccess(authUser: AuthUserDto, userId: string) { // Check if userId shares assets with authUser - if (!(await this.partnerCore.get({ sharedById: userId, sharedWithId: authUser.id }))) { + const canAccess = await this.accessRepository.hasPartnerAccess(authUser.id, userId); + if (!canAccess) { throw new ForbiddenException(); } } diff --git a/server/libs/domain/src/access/access.repository.ts b/server/libs/domain/src/access/access.repository.ts new file mode 100644 index 0000000000..7af2255d4d --- /dev/null +++ b/server/libs/domain/src/access/access.repository.ts @@ -0,0 +1,6 @@ +export const IAccessRepository = 'IAccessRepository'; + +export interface IAccessRepository { + hasPartnerAccess(userId: string, partnerId: string): Promise; + hasPartnerAssetAccess(userId: string, assetId: string): Promise; +} diff --git a/server/libs/domain/src/access/index.ts b/server/libs/domain/src/access/index.ts new file mode 100644 index 0000000000..d864cd8dae --- /dev/null +++ b/server/libs/domain/src/access/index.ts @@ -0,0 +1 @@ +export * from './access.repository'; diff --git a/server/libs/domain/src/index.ts b/server/libs/domain/src/index.ts index dfc6f7a41e..81120f6e2b 100644 --- a/server/libs/domain/src/index.ts +++ b/server/libs/domain/src/index.ts @@ -1,3 +1,4 @@ +export * from './access'; export * from './album'; export * from './api-key'; export * from './asset'; @@ -13,10 +14,10 @@ export * from './job'; export * from './media'; export * from './metadata'; export * from './oauth'; +export * from './partner'; export * from './person'; export * from './search'; export * from './server-info'; -export * from './partner'; export * from './shared-link'; export * from './smart-info'; export * from './storage'; diff --git a/server/libs/domain/src/partner/index.ts b/server/libs/domain/src/partner/index.ts index 86006f17fe..a1542198e1 100644 --- a/server/libs/domain/src/partner/index.ts +++ b/server/libs/domain/src/partner/index.ts @@ -1,3 +1,2 @@ -export * from './partner.core'; export * from './partner.repository'; export * from './partner.service'; diff --git a/server/libs/domain/src/partner/partner.core.ts b/server/libs/domain/src/partner/partner.core.ts deleted file mode 100644 index 7e51c6fede..0000000000 --- a/server/libs/domain/src/partner/partner.core.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PartnerEntity } from '@app/infra/entities'; -import { IPartnerRepository, PartnerIds } from './partner.repository'; - -export enum PartnerDirection { - SharedBy = 'shared-by', - SharedWith = 'shared-with', -} - -export class PartnerCore { - constructor(private repository: IPartnerRepository) {} - - async getAll(userId: string, direction: PartnerDirection): Promise { - const partners = await this.repository.getAll(userId); - const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; - return partners.filter((partner) => partner[key] === userId); - } - - get(ids: PartnerIds): Promise { - return this.repository.get(ids); - } - - async create(ids: PartnerIds): Promise { - return this.repository.create(ids); - } - - async remove(ids: PartnerIds): Promise { - await this.repository.remove(ids as PartnerEntity); - } - - hasAssetAccess(assetId: string, userId: string): Promise { - return this.repository.hasAssetAccess(assetId, userId); - } -} diff --git a/server/libs/domain/src/partner/partner.repository.ts b/server/libs/domain/src/partner/partner.repository.ts index 9e4f78ec12..fd436932cb 100644 --- a/server/libs/domain/src/partner/partner.repository.ts +++ b/server/libs/domain/src/partner/partner.repository.ts @@ -5,6 +5,11 @@ export interface PartnerIds { sharedWithId: string; } +export enum PartnerDirection { + SharedBy = 'shared-by', + SharedWith = 'shared-with', +} + export const IPartnerRepository = 'IPartnerRepository'; export interface IPartnerRepository { @@ -12,5 +17,4 @@ export interface IPartnerRepository { get(partner: PartnerIds): Promise; create(partner: PartnerIds): Promise; remove(entity: PartnerEntity): Promise; - hasAssetAccess(assetId: string, userId: string): Promise; } diff --git a/server/libs/domain/src/partner/partner.service.spec.ts b/server/libs/domain/src/partner/partner.service.spec.ts index 2598567cc6..7a3c0490f7 100644 --- a/server/libs/domain/src/partner/partner.service.spec.ts +++ b/server/libs/domain/src/partner/partner.service.spec.ts @@ -1,7 +1,6 @@ import { BadRequestException } from '@nestjs/common'; import { authStub, newPartnerRepositoryMock, partnerStub } from '../../test'; -import { PartnerDirection } from './partner.core'; -import { IPartnerRepository } from './partner.repository'; +import { IPartnerRepository, PartnerDirection } from './partner.repository'; import { PartnerService } from './partner.service'; const responseDto = { diff --git a/server/libs/domain/src/partner/partner.service.ts b/server/libs/domain/src/partner/partner.service.ts index 0b2c22d84a..2759f37cba 100644 --- a/server/libs/domain/src/partner/partner.service.ts +++ b/server/libs/domain/src/partner/partner.service.ts @@ -1,41 +1,38 @@ import { PartnerEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AuthUserDto } from '../auth'; -import { IPartnerRepository, PartnerCore, PartnerDirection, PartnerIds } from '../partner'; +import { IPartnerRepository, PartnerDirection, PartnerIds } from '../partner'; import { mapUser, UserResponseDto } from '../user'; @Injectable() export class PartnerService { - private partnerCore: PartnerCore; - - constructor(@Inject(IPartnerRepository) partnerRepository: IPartnerRepository) { - this.partnerCore = new PartnerCore(partnerRepository); - } + constructor(@Inject(IPartnerRepository) private repository: IPartnerRepository) {} async create(authUser: AuthUserDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; - const exists = await this.partnerCore.get(partnerId); + const exists = await this.repository.get(partnerId); if (exists) { throw new BadRequestException(`Partner already exists`); } - const partner = await this.partnerCore.create(partnerId); + const partner = await this.repository.create(partnerId); return this.map(partner, PartnerDirection.SharedBy); } async remove(authUser: AuthUserDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; - const partner = await this.partnerCore.get(partnerId); + const partner = await this.repository.get(partnerId); if (!partner) { throw new BadRequestException('Partner not found'); } - await this.partnerCore.remove(partner); + await this.repository.remove(partner); } async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise { - const partners = await this.partnerCore.getAll(authUser.id, direction); - return partners.map((partner) => this.map(partner, direction)); + const partners = await this.repository.getAll(authUser.id); + const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; + return partners.filter((partner) => partner[key] === authUser.id).map((partner) => this.map(partner, direction)); } private map(partner: PartnerEntity, direction: PartnerDirection): UserResponseDto { diff --git a/server/libs/domain/test/access.repository.mock.ts b/server/libs/domain/test/access.repository.mock.ts new file mode 100644 index 0000000000..5f3af78a64 --- /dev/null +++ b/server/libs/domain/test/access.repository.mock.ts @@ -0,0 +1,8 @@ +import { IAccessRepository } from '../src'; + +export const newAccessRepositoryMock = (): jest.Mocked => { + return { + hasPartnerAccess: jest.fn(), + hasPartnerAssetAccess: jest.fn(), + }; +}; diff --git a/server/libs/domain/test/index.ts b/server/libs/domain/test/index.ts index f984d366c2..a868009f49 100644 --- a/server/libs/domain/test/index.ts +++ b/server/libs/domain/test/index.ts @@ -1,3 +1,4 @@ +export * from './access.repository.mock'; export * from './album.repository.mock'; export * from './api-key.repository.mock'; export * from './asset.repository.mock'; diff --git a/server/libs/domain/test/partner.repository.mock.ts b/server/libs/domain/test/partner.repository.mock.ts index 390790275a..396b334785 100644 --- a/server/libs/domain/test/partner.repository.mock.ts +++ b/server/libs/domain/test/partner.repository.mock.ts @@ -6,6 +6,5 @@ export const newPartnerRepositoryMock = (): jest.Mocked => { remove: jest.fn(), getAll: jest.fn(), get: jest.fn(), - hasAssetAccess: jest.fn(), }; }; diff --git a/server/libs/infra/src/infra.module.ts b/server/libs/infra/src/infra.module.ts index 420ef583ef..20bac55d55 100644 --- a/server/libs/infra/src/infra.module.ts +++ b/server/libs/infra/src/infra.module.ts @@ -1,4 +1,5 @@ import { + IAccessRepository, IAlbumRepository, IAssetRepository, ICommunicationRepository, @@ -30,6 +31,7 @@ import { databaseConfig } from './database.config'; import { databaseEntities } from './entities'; import { bullConfig, bullQueues } from './infra.config'; import { + AccessRepository, AlbumRepository, APIKeyRepository, AssetRepository, @@ -53,6 +55,7 @@ import { } from './repositories'; const providers: Provider[] = [ + { provide: IAccessRepository, useClass: AccessRepository }, { provide: IAlbumRepository, useClass: AlbumRepository }, { provide: IAssetRepository, useClass: AssetRepository }, { provide: ICommunicationRepository, useClass: CommunicationRepository }, diff --git a/server/libs/infra/src/repositories/access.repository.ts b/server/libs/infra/src/repositories/access.repository.ts new file mode 100644 index 0000000000..a8ae28c07c --- /dev/null +++ b/server/libs/infra/src/repositories/access.repository.ts @@ -0,0 +1,38 @@ +import { IAccessRepository } from '@app/domain'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PartnerEntity } from '../entities'; + +export class AccessRepository implements IAccessRepository { + constructor(@InjectRepository(PartnerEntity) private partnerRepository: Repository) {} + + hasPartnerAccess(userId: string, partnerId: string): Promise { + return this.partnerRepository.exist({ + where: { + sharedWithId: userId, + sharedById: partnerId, + }, + }); + } + + hasPartnerAssetAccess(userId: string, assetId: string): Promise { + return this.partnerRepository.exist({ + where: { + sharedWith: { + id: userId, + }, + sharedBy: { + assets: { + id: assetId, + }, + }, + }, + relations: { + sharedWith: true, + sharedBy: { + assets: true, + }, + }, + }); + } +} diff --git a/server/libs/infra/src/repositories/index.ts b/server/libs/infra/src/repositories/index.ts index bb49e336e6..5c7261b2d4 100644 --- a/server/libs/infra/src/repositories/index.ts +++ b/server/libs/infra/src/repositories/index.ts @@ -1,3 +1,4 @@ +export * from './access.repository'; export * from './album.repository'; export * from './api-key.repository'; export * from './asset.repository'; diff --git a/server/libs/infra/src/repositories/partner.repository.ts b/server/libs/infra/src/repositories/partner.repository.ts index 56fdbc531b..c56d8b0757 100644 --- a/server/libs/infra/src/repositories/partner.repository.ts +++ b/server/libs/infra/src/repositories/partner.repository.ts @@ -24,27 +24,4 @@ export class PartnerRepository implements IPartnerRepository { async remove(entity: PartnerEntity): Promise { await this.repository.remove(entity); } - - async hasAssetAccess(assetId: string, userId: string): Promise { - const count = await this.repository.count({ - where: { - sharedWith: { - id: userId, - }, - sharedBy: { - assets: { - id: assetId, - }, - }, - }, - relations: { - sharedWith: true, - sharedBy: { - assets: true, - }, - }, - }); - - return count == 1; - } }