mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	refactor(server): access checks (#2776)
* refactor(server): access checks * chore: simply asset module
This commit is contained in:
		
							parent
							
								
									61d74263d9
								
							
						
					
					
						commit
						f04e47803c
					
				@ -2,6 +2,8 @@ export const IAccessRepository = 'IAccessRepository';
 | 
			
		||||
 | 
			
		||||
export interface IAccessRepository {
 | 
			
		||||
  hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
 | 
			
		||||
  hasAlbumAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
			
		||||
  hasOwnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
			
		||||
  hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
			
		||||
  hasSharedLinkAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
			
		||||
import { dataSource } from '@app/infra/database.config';
 | 
			
		||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
@ -12,7 +12,6 @@ export interface IAlbumRepository {
 | 
			
		||||
  removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
 | 
			
		||||
  addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
 | 
			
		||||
  updateThumbnails(): Promise<number | undefined>;
 | 
			
		||||
  getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const IAlbumRepository = 'IAlbumRepository';
 | 
			
		||||
@ -130,25 +129,4 @@ export class AlbumRepository implements IAlbumRepository {
 | 
			
		||||
 | 
			
		||||
    return result.affected;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number> {
 | 
			
		||||
    return this.albumRepository.count({
 | 
			
		||||
      where: [
 | 
			
		||||
        {
 | 
			
		||||
          ownerId: userId,
 | 
			
		||||
          assets: {
 | 
			
		||||
            id: assetId,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          sharedUsers: {
 | 
			
		||||
            id: userId,
 | 
			
		||||
          },
 | 
			
		||||
          assets: {
 | 
			
		||||
            id: assetId,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,15 +6,9 @@ import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
			
		||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
 | 
			
		||||
import { DownloadModule } from '../../modules/download/download.module';
 | 
			
		||||
 | 
			
		||||
const ALBUM_REPOSITORY_PROVIDER = {
 | 
			
		||||
  provide: IAlbumRepository,
 | 
			
		||||
  useClass: AlbumRepository,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity]), DownloadModule],
 | 
			
		||||
  controllers: [AlbumController],
 | 
			
		||||
  providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
 | 
			
		||||
  exports: [ALBUM_REPOSITORY_PROVIDER],
 | 
			
		||||
  providers: [AlbumService, { provide: IAlbumRepository, useClass: AlbumRepository }],
 | 
			
		||||
})
 | 
			
		||||
export class AlbumModule {}
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,12 @@
 | 
			
		||||
import { AlbumService } from './album.service';
 | 
			
		||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
			
		||||
import { NotFoundException, ForbiddenException } from '@nestjs/common';
 | 
			
		||||
import { AlbumResponseDto, ICryptoRepository, ISharedLinkRepository, mapUser } from '@app/domain';
 | 
			
		||||
import { AlbumEntity, UserEntity } from '@app/infra/entities';
 | 
			
		||||
import { AlbumResponseDto, ICryptoRepository, mapUser } from '@app/domain';
 | 
			
		||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
			
		||||
import { IAlbumRepository } from './album-repository';
 | 
			
		||||
import { DownloadService } from '../../modules/download/download.service';
 | 
			
		||||
import { ISharedLinkRepository } from '@app/domain';
 | 
			
		||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
 | 
			
		||||
import { newCryptoRepositoryMock, newSharedLinkRepositoryMock, userEntityStub } from '@test';
 | 
			
		||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
			
		||||
import { DownloadService } from '../../modules/download/download.service';
 | 
			
		||||
import { IAlbumRepository } from './album-repository';
 | 
			
		||||
import { AlbumService } from './album.service';
 | 
			
		||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
			
		||||
 | 
			
		||||
describe('Album service', () => {
 | 
			
		||||
  let sut: AlbumService;
 | 
			
		||||
@ -98,7 +97,6 @@ describe('Album service', () => {
 | 
			
		||||
      get: jest.fn(),
 | 
			
		||||
      removeAssets: jest.fn(),
 | 
			
		||||
      updateThumbnails: jest.fn(),
 | 
			
		||||
      getSharedWithUserAlbumCount: jest.fn(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,6 @@ export interface IAssetRepository {
 | 
			
		||||
  getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
 | 
			
		||||
  getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
 | 
			
		||||
  getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
 | 
			
		||||
  countByIdAndUser(assetId: string, userId: string): Promise<number>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const IAssetRepository = 'IAssetRepository';
 | 
			
		||||
@ -329,15 +328,6 @@ export class AssetRepository implements IAssetRepository {
 | 
			
		||||
    return assets.map((asset) => asset.deviceAssetId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  countByIdAndUser(assetId: string, ownerId: string): Promise<number> {
 | 
			
		||||
    return this.assetRepository.count({
 | 
			
		||||
      where: {
 | 
			
		||||
        id: assetId,
 | 
			
		||||
        ownerId,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getAssetCount(items: any): AssetCountByUserIdResponseDto {
 | 
			
		||||
    const assetCountByUserId = new AssetCountByUserIdResponseDto();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,22 +5,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
			
		||||
import { AssetRepository, IAssetRepository } from './asset-repository';
 | 
			
		||||
import { DownloadModule } from '../../modules/download/download.module';
 | 
			
		||||
import { AlbumModule } from '../album/album.module';
 | 
			
		||||
 | 
			
		||||
const ASSET_REPOSITORY_PROVIDER = {
 | 
			
		||||
  provide: IAssetRepository,
 | 
			
		||||
  useClass: AssetRepository,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    //
 | 
			
		||||
    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
			
		||||
    DownloadModule,
 | 
			
		||||
    AlbumModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [AssetController],
 | 
			
		||||
  providers: [AssetService, ASSET_REPOSITORY_PROVIDER],
 | 
			
		||||
  exports: [ASSET_REPOSITORY_PROVIDER],
 | 
			
		||||
  providers: [AssetService, { provide: IAssetRepository, useClass: AssetRepository }],
 | 
			
		||||
})
 | 
			
		||||
export class AssetModule {}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group
 | 
			
		||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 | 
			
		||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
 | 
			
		||||
import { DownloadService } from '../../modules/download/download.service';
 | 
			
		||||
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
 | 
			
		||||
import {
 | 
			
		||||
  IAccessRepository,
 | 
			
		||||
  ICryptoRepository,
 | 
			
		||||
@ -29,7 +28,7 @@ import {
 | 
			
		||||
  sharedLinkStub,
 | 
			
		||||
} from '@test';
 | 
			
		||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 | 
			
		||||
import { BadRequestException } from '@nestjs/common';
 | 
			
		||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
 | 
			
		||||
import { when } from 'jest-when';
 | 
			
		||||
import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
 | 
			
		||||
 | 
			
		||||
@ -134,7 +133,6 @@ describe('AssetService', () => {
 | 
			
		||||
  let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
 | 
			
		||||
  let accessMock: jest.Mocked<IAccessRepository>;
 | 
			
		||||
  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
			
		||||
  let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
 | 
			
		||||
  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
			
		||||
  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
 | 
			
		||||
  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
			
		||||
@ -160,13 +158,8 @@ describe('AssetService', () => {
 | 
			
		||||
      getAssetCountByUserId: jest.fn(),
 | 
			
		||||
      getArchivedAssetCountByUserId: jest.fn(),
 | 
			
		||||
      getExistingAssets: jest.fn(),
 | 
			
		||||
      countByIdAndUser: jest.fn(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    albumRepositoryMock = {
 | 
			
		||||
      getSharedWithUserAlbumCount: jest.fn(),
 | 
			
		||||
    } as unknown as jest.Mocked<AlbumRepository>;
 | 
			
		||||
 | 
			
		||||
    downloadServiceMock = {
 | 
			
		||||
      downloadArchive: jest.fn(),
 | 
			
		||||
    };
 | 
			
		||||
@ -180,7 +173,6 @@ describe('AssetService', () => {
 | 
			
		||||
    sut = new AssetService(
 | 
			
		||||
      accessMock,
 | 
			
		||||
      assetRepositoryMock,
 | 
			
		||||
      albumRepositoryMock,
 | 
			
		||||
      a,
 | 
			
		||||
      downloadServiceMock as DownloadService,
 | 
			
		||||
      sharedLinkRepositoryMock,
 | 
			
		||||
@ -203,13 +195,13 @@ describe('AssetService', () => {
 | 
			
		||||
      const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] };
 | 
			
		||||
 | 
			
		||||
      assetRepositoryMock.getById.mockResolvedValue(asset1);
 | 
			
		||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
 | 
			
		||||
 | 
			
		||||
      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
 | 
			
		||||
      expect(assetRepositoryMock.countByIdAndUser).toHaveBeenCalledWith(asset1.id, authStub.user1.id);
 | 
			
		||||
      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.user1.id, asset1.id);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -383,7 +375,7 @@ describe('AssetService', () => {
 | 
			
		||||
  describe('deleteAll', () => {
 | 
			
		||||
    it('should return failed status when an asset is missing', async () => {
 | 
			
		||||
      assetRepositoryMock.get.mockResolvedValue(null);
 | 
			
		||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
 | 
			
		||||
        { id: 'asset1', status: 'FAILED' },
 | 
			
		||||
@ -395,7 +387,7 @@ describe('AssetService', () => {
 | 
			
		||||
    it('should return failed status a delete fails', async () => {
 | 
			
		||||
      assetRepositoryMock.get.mockResolvedValue({ id: 'asset1' } as AssetEntity);
 | 
			
		||||
      assetRepositoryMock.remove.mockRejectedValue('delete failed');
 | 
			
		||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
 | 
			
		||||
        { id: 'asset1', status: 'FAILED' },
 | 
			
		||||
@ -405,7 +397,7 @@ describe('AssetService', () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should delete a live photo', async () => {
 | 
			
		||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.deleteAll(authStub.user1, { ids: [assetEntityStub.livePhotoStillAsset.id] })).resolves.toEqual([
 | 
			
		||||
        { id: assetEntityStub.livePhotoStillAsset.id, status: 'SUCCESS' },
 | 
			
		||||
@ -454,7 +446,7 @@ describe('AssetService', () => {
 | 
			
		||||
        .calledWith(asset2.id)
 | 
			
		||||
        .mockResolvedValue(asset2 as AssetEntity);
 | 
			
		||||
 | 
			
		||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'] })).resolves.toEqual([
 | 
			
		||||
        { id: 'asset1', status: 'SUCCESS' },
 | 
			
		||||
@ -499,7 +491,7 @@ describe('AssetService', () => {
 | 
			
		||||
 | 
			
		||||
  describe('downloadFile', () => {
 | 
			
		||||
    it('should download a single file', async () => {
 | 
			
		||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
 | 
			
		||||
 | 
			
		||||
      await sut.downloadFile(authStub.admin, 'id_1');
 | 
			
		||||
@ -535,4 +527,60 @@ describe('AssetService', () => {
 | 
			
		||||
      expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.id, [file1, file2]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('getAssetById', () => {
 | 
			
		||||
    it('should allow owner access', async () => {
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
			
		||||
      await sut.getAssetById(authStub.admin, assetEntityStub.image.id);
 | 
			
		||||
      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should allow shared link access', async () => {
 | 
			
		||||
      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
			
		||||
      await sut.getAssetById(authStub.adminSharedLink, assetEntityStub.image.id);
 | 
			
		||||
      expect(accessMock.hasSharedLinkAssetAccess).toHaveBeenCalledWith(
 | 
			
		||||
        authStub.adminSharedLink.sharedLinkId,
 | 
			
		||||
        assetEntityStub.image.id,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should allow partner sharing access', async () => {
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      accessMock.hasPartnerAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
			
		||||
      await sut.getAssetById(authStub.admin, assetEntityStub.image.id);
 | 
			
		||||
      expect(accessMock.hasPartnerAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should allow shared album access', async () => {
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      accessMock.hasPartnerAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      accessMock.hasAlbumAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
			
		||||
      await sut.getAssetById(authStub.admin, assetEntityStub.image.id);
 | 
			
		||||
      expect(accessMock.hasAlbumAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error for no access', async () => {
 | 
			
		||||
      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      accessMock.hasPartnerAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      accessMock.hasAlbumAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      await expect(sut.getAssetById(authStub.admin, assetEntityStub.image.id)).rejects.toBeInstanceOf(
 | 
			
		||||
        ForbiddenException,
 | 
			
		||||
      );
 | 
			
		||||
      expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error for an invalid shared link', async () => {
 | 
			
		||||
      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(false);
 | 
			
		||||
      await expect(sut.getAssetById(authStub.adminSharedLink, assetEntityStub.image.id)).rejects.toBeInstanceOf(
 | 
			
		||||
        ForbiddenException,
 | 
			
		||||
      );
 | 
			
		||||
      expect(accessMock.hasOwnerAssetAccess).not.toHaveBeenCalled();
 | 
			
		||||
      expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,6 @@ import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-res
 | 
			
		||||
import { ICryptoRepository, IJobRepository } from '@app/domain';
 | 
			
		||||
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 { ISharedLinkRepository } from '@app/domain';
 | 
			
		||||
import { DownloadFilesDto } from './dto/download-files.dto';
 | 
			
		||||
@ -85,7 +84,6 @@ export class AssetService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(IAccessRepository) private accessRepository: IAccessRepository,
 | 
			
		||||
    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
			
		||||
    @Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
 | 
			
		||||
    @InjectRepository(AssetEntity)
 | 
			
		||||
    private assetRepository: Repository<AssetEntity>,
 | 
			
		||||
    private downloadService: DownloadService,
 | 
			
		||||
@ -567,32 +565,33 @@ export class AssetService {
 | 
			
		||||
    const sharedLinkId = authUser.sharedLinkId;
 | 
			
		||||
 | 
			
		||||
    for (const assetId of assetIds) {
 | 
			
		||||
      // Step 1: Check if asset is part of a public shared
 | 
			
		||||
      if (sharedLinkId) {
 | 
			
		||||
        const canAccess = await this.accessRepository.hasSharedLinkAssetAccess(sharedLinkId, assetId);
 | 
			
		||||
        if (canAccess) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // Step 2: Check if user owns asset
 | 
			
		||||
        if ((await this._assetRepository.countByIdAndUser(assetId, authUser.id)) == 1) {
 | 
			
		||||
 | 
			
		||||
        throw new ForbiddenException();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const isOwner = await this.accessRepository.hasOwnerAssetAccess(authUser.id, assetId);
 | 
			
		||||
      if (isOwner) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        // Step 3: Check if any partner owns the asset
 | 
			
		||||
        const canAccess = await this.accessRepository.hasPartnerAssetAccess(authUser.id, assetId);
 | 
			
		||||
        if (canAccess) {
 | 
			
		||||
      if (mustBeOwner) {
 | 
			
		||||
        throw new ForbiddenException();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const isPartnerShared = await this.accessRepository.hasPartnerAssetAccess(authUser.id, assetId);
 | 
			
		||||
      if (isPartnerShared) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        // Avoid additional checks if ownership is required
 | 
			
		||||
        if (!mustBeOwner) {
 | 
			
		||||
          // Step 2: Check if asset is part of an album shared with me
 | 
			
		||||
          if ((await this._albumRepository.getSharedWithUserAlbumCount(authUser.id, assetId)) > 0) {
 | 
			
		||||
      const isAlbumShared = await this.accessRepository.hasAlbumAssetAccess(authUser.id, assetId);
 | 
			
		||||
      if (isAlbumShared) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      throw new ForbiddenException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
import { IAccessRepository } from '@app/domain';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
import { PartnerEntity, SharedLinkEntity } from '../entities';
 | 
			
		||||
import { AlbumEntity, AssetEntity, PartnerEntity, SharedLinkEntity } from '../entities';
 | 
			
		||||
 | 
			
		||||
export class AccessRepository implements IAccessRepository {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
			
		||||
    @InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
 | 
			
		||||
    @InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
 | 
			
		||||
    @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
 | 
			
		||||
  ) {}
 | 
			
		||||
@ -18,6 +20,36 @@ export class AccessRepository implements IAccessRepository {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hasAlbumAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
			
		||||
    return this.albumRepository.exist({
 | 
			
		||||
      where: [
 | 
			
		||||
        {
 | 
			
		||||
          ownerId: userId,
 | 
			
		||||
          assets: {
 | 
			
		||||
            id: assetId,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          sharedUsers: {
 | 
			
		||||
            id: userId,
 | 
			
		||||
          },
 | 
			
		||||
          assets: {
 | 
			
		||||
            id: assetId,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hasOwnerAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
			
		||||
    return this.assetRepository.exist({
 | 
			
		||||
      where: {
 | 
			
		||||
        id: assetId,
 | 
			
		||||
        ownerId: userId,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
			
		||||
    return this.partnerRepository.exist({
 | 
			
		||||
      where: {
 | 
			
		||||
 | 
			
		||||
@ -124,8 +124,8 @@ export class AlbumRepository implements IAlbumRepository {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async hasAsset(id: string, assetId: string): Promise<boolean> {
 | 
			
		||||
    const count = await this.repository.count({
 | 
			
		||||
  hasAsset(id: string, assetId: string): Promise<boolean> {
 | 
			
		||||
    return this.repository.exist({
 | 
			
		||||
      where: {
 | 
			
		||||
        id,
 | 
			
		||||
        assets: {
 | 
			
		||||
@ -136,8 +136,6 @@ export class AlbumRepository implements IAlbumRepository {
 | 
			
		||||
        assets: true,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return Boolean(count);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ import { IAccessRepository } from '@app/domain';
 | 
			
		||||
export const newAccessRepositoryMock = (): jest.Mocked<IAccessRepository> => {
 | 
			
		||||
  return {
 | 
			
		||||
    hasPartnerAccess: jest.fn(),
 | 
			
		||||
    hasAlbumAssetAccess: jest.fn(),
 | 
			
		||||
    hasOwnerAssetAccess: jest.fn(),
 | 
			
		||||
    hasPartnerAssetAccess: jest.fn(),
 | 
			
		||||
    hasSharedLinkAssetAccess: jest.fn(),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user