diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts index c9a7b288b..93991b1cd 100644 --- a/src/modules/file/file.repository.ts +++ b/src/modules/file/file.repository.ts @@ -64,6 +64,10 @@ export interface FileRepository { user: User, fileUuids: File['uuid'][], ): Promise; + findByFileIds( + userId: User['id'], + fileIds: FileAttributes['fileId'][], + ): Promise; } @Injectable() @@ -88,6 +92,22 @@ export class SequelizeFileRepository implements FileRepository { }); } + async findByFileIds( + userId: User['id'], + fileIds: FileAttributes['fileId'][], + ): Promise { + const files = await this.fileModel.findAll({ + where: { + userId: userId, + fileId: { + [Op.in]: fileIds, + }, + }, + }); + + return files.map(this.toDomain.bind(this)); + } + async findById( fileUuid: string, where: FindOptions = {}, diff --git a/src/modules/file/file.usecase.spec.ts b/src/modules/file/file.usecase.spec.ts index c1cb9b1be..f5a89b8d5 100644 --- a/src/modules/file/file.usecase.spec.ts +++ b/src/modules/file/file.usecase.spec.ts @@ -8,7 +8,6 @@ import { } from '@nestjs/common'; import { File, FileAttributes, FileStatus } from './file.domain'; import { User } from '../user/user.domain'; -import { ShareUseCases } from '../share/share.usecase'; import { BridgeModule } from '../../externals/bridge/bridge.module'; import { BridgeService } from '../../externals/bridge/bridge.service'; import { CryptoService } from '../../externals/crypto/crypto.service'; @@ -19,6 +18,8 @@ import { import { newFile, newFolder } from '../../../test/fixtures'; import { FolderUseCases } from '../folder/folder.usecase'; import { v4 } from 'uuid'; +import { SharingService } from '../sharing/sharing.service'; +import { SharingItemType } from '../sharing/sharing.domain'; const fileId = '6295c99a241bb000083f1c6a'; const userId = 1; @@ -28,7 +29,7 @@ describe('FileUseCases', () => { let folderUseCases: FolderUseCases; let fileRepository: FileRepository; let folderRepository: FolderRepository; - let shareUseCases: ShareUseCases; + let sharingService: SharingService; let bridgeService: BridgeService; let cryptoService: CryptoService; @@ -65,7 +66,7 @@ describe('FileUseCases', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [BridgeModule], - providers: [FileUseCases, FolderUseCases], + providers: [FileUseCases, FolderUseCases, SharingService], }) .useMocker(() => createMock()) .compile(); @@ -74,9 +75,9 @@ describe('FileUseCases', () => { fileRepository = module.get(SequelizeFileRepository); folderRepository = module.get(SequelizeFolderRepository); folderUseCases = module.get(FolderUseCases); - shareUseCases = module.get(ShareUseCases); bridgeService = module.get(BridgeService); cryptoService = module.get(CryptoService); + sharingService = module.get(SharingService); }); afterEach(() => { @@ -128,6 +129,23 @@ describe('FileUseCases', () => { fileRepository.updateFilesStatusToTrashedByUuid, ).toHaveBeenCalledWith(userMocked, fileUuids); }); + + it('When you try to trash files, then it stops sharing those files', async () => { + const files = [newFile(), newFile(), newFile()]; + const fileUuids = ['656a3abb-36ab-47ee-8303-6e4198f2a32a']; + const fileIds = [fileId]; + + jest.spyOn(sharingService, 'bulkRemoveSharings'); + jest.spyOn(fileRepository, 'findByFileIds').mockResolvedValueOnce(files); + + await service.moveFilesToTrash(userMocked, fileIds, fileUuids); + + expect(sharingService.bulkRemoveSharings).toHaveBeenCalledWith( + userMocked, + [...fileUuids, ...files.map((file) => file.uuid)], + SharingItemType.File, + ); + }); }); describe('get folder by folderId and User Id', () => { @@ -240,10 +258,6 @@ describe('FileUseCases', () => { .spyOn(fileRepository, 'deleteByFileId') .mockImplementationOnce(() => Promise.resolve()); - jest - .spyOn(shareUseCases, 'deleteFileShare') - .mockImplementationOnce(() => Promise.resolve()); - jest .spyOn(bridgeService, 'deleteFile') .mockImplementationOnce(() => Promise.resolve()); @@ -251,7 +265,6 @@ describe('FileUseCases', () => { await service.deleteFilePermanently(file, userMock); expect(fileRepository.deleteByFileId).toHaveBeenCalledWith(fileId); - expect(shareUseCases.deleteFileShare).toHaveBeenCalledTimes(1); }); it.skip('should fail when the folder trying to delete has not been trashed', async () => { @@ -295,14 +308,12 @@ describe('FileUseCases', () => { deleted: true, } as File; - jest.spyOn(shareUseCases, 'deleteFileShare'); jest.spyOn(bridgeService, 'deleteFile'); jest.spyOn(fileRepository, 'deleteByFileId'); expect(service.deleteFilePermanently(file, userMock)).rejects.toThrow( new ForbiddenException(`You are not owner of this share`), ); - expect(shareUseCases.deleteFileShare).not.toHaveBeenCalled(); expect(bridgeService.deleteFile).not.toHaveBeenCalled(); expect(fileRepository.deleteByFileId).not.toHaveBeenCalled(); }); @@ -334,9 +345,6 @@ describe('FileUseCases', () => { const errorReason = new Error('reason'); - jest - .spyOn(shareUseCases, 'deleteFileShare') - .mockImplementationOnce(() => Promise.reject(errorReason)); jest .spyOn(fileRepository, 'deleteByFileId') .mockImplementationOnce(() => Promise.resolve()); @@ -384,9 +392,7 @@ describe('FileUseCases', () => { jest .spyOn(fileRepository, 'deleteByFileId') .mockImplementationOnce(() => Promise.resolve()); - jest - .spyOn(shareUseCases, 'deleteFileShare') - .mockImplementationOnce(() => Promise.resolve()); + jest .spyOn(bridgeService, 'deleteFile') .mockImplementationOnce(() => Promise.reject(errorReason)); diff --git a/src/modules/file/file.usecase.ts b/src/modules/file/file.usecase.ts index f1bb24c42..5b49832b6 100644 --- a/src/modules/file/file.usecase.ts +++ b/src/modules/file/file.usecase.ts @@ -13,7 +13,6 @@ import { CryptoService } from '../../externals/crypto/crypto.service'; import { BridgeService } from '../../externals/bridge/bridge.service'; import { FolderAttributes } from '../folder/folder.attributes'; import { Share } from '../share/share.domain'; -import { ShareUseCases } from '../share/share.usecase'; import { User } from '../user/user.domain'; import { UserAttributes } from '../user/user.attributes'; import { @@ -27,6 +26,8 @@ import { SequelizeFileRepository } from './file.repository'; import { FolderUseCases } from '../folder/folder.usecase'; import { ReplaceFileDto } from './dto/replace-file.dto'; import { FileDto } from './dto/file.dto'; +import { SharingService } from '../sharing/sharing.service'; +import { SharingItemType } from '../sharing/sharing.domain'; type SortParams = Array<[SortableFileAttributes, 'ASC' | 'DESC']>; @@ -34,10 +35,10 @@ type SortParams = Array<[SortableFileAttributes, 'ASC' | 'DESC']>; export class FileUseCases { constructor( private fileRepository: SequelizeFileRepository, - @Inject(forwardRef(() => ShareUseCases)) - private shareUseCases: ShareUseCases, @Inject(forwardRef(() => FolderUseCases)) private folderUsecases: FolderUseCases, + @Inject(forwardRef(() => SharingService)) + private sharingUsecases: SharingService, private network: BridgeService, private cryptoService: CryptoService, ) {} @@ -245,14 +246,22 @@ export class FileUseCases { return files.map((file) => file.toJSON()); } - moveFilesToTrash( + async moveFilesToTrash( user: User, fileIds: FileAttributes['fileId'][], fileUuids: FileAttributes['uuid'][] = [], - ): Promise<[void, void]> { - return Promise.all([ + ): Promise { + const files = await this.fileRepository.findByFileIds(user.id, fileIds); + const allFileUuids = [...fileUuids, ...files.map((file) => file.uuid)]; + + await Promise.all([ this.fileRepository.updateFilesStatusToTrashed(user, fileIds), this.fileRepository.updateFilesStatusToTrashedByUuid(user, fileUuids), + this.sharingUsecases.bulkRemoveSharings( + user, + allFileUuids, + SharingItemType.File, + ), ]); } diff --git a/src/modules/folder/folder.module.ts b/src/modules/folder/folder.module.ts index 876863d48..bb6cdf005 100644 --- a/src/modules/folder/folder.module.ts +++ b/src/modules/folder/folder.module.ts @@ -9,6 +9,7 @@ import { CryptoService } from '../../externals/crypto/crypto.service'; import { FolderController } from './folder.controller'; import { UserModel } from '../user/user.model'; import { UserModule } from '../user/user.module'; +import { SharingModule } from '../sharing/sharing.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { UserModule } from '../user/user.module'; forwardRef(() => FileModule), forwardRef(() => UserModule), CryptoModule, + forwardRef(() => SharingModule), ], controllers: [FolderController], providers: [SequelizeFolderRepository, CryptoService, FolderUseCases], diff --git a/src/modules/folder/folder.usecase.spec.ts b/src/modules/folder/folder.usecase.spec.ts index 34243fb2c..2237444f5 100644 --- a/src/modules/folder/folder.usecase.spec.ts +++ b/src/modules/folder/folder.usecase.spec.ts @@ -17,6 +17,7 @@ import { CryptoService } from '../../externals/crypto/crypto.service'; import { User } from '../user/user.domain'; import { newFolder, newUser } from '../../../test/fixtures'; import { CalculateFolderSizeTimeoutException } from './exception/calculate-folder-size-timeout.exception'; +import { SharingService } from '../sharing/sharing.service'; const folderId = 4; const user = newUser(); @@ -25,6 +26,7 @@ describe('FolderUseCases', () => { let service: FolderUseCases; let folderRepository: FolderRepository; let cryptoService: CryptoService; + let sharingService: SharingService; const userMocked = User.build({ id: 1, @@ -67,6 +69,7 @@ describe('FolderUseCases', () => { service = module.get(FolderUseCases); folderRepository = module.get(SequelizeFolderRepository); cryptoService = module.get(CryptoService); + sharingService = module.get(SharingService); }); it('should be defined', () => { @@ -142,7 +145,7 @@ describe('FolderUseCases', () => { deletedAt: new Date(), createdAt: new Date(), updatedAt: new Date(), - uuid: '', + uuid: '656a3abb-36ab-47ee-8303-6e4198f2a32a', plainName: '', removed: false, removedAt: null, @@ -182,6 +185,11 @@ describe('FolderUseCases', () => { removedAt: expect.any(Date), }, ); + expect(sharingService.bulkRemoveSharings).toHaveBeenCalledWith( + user, + [mockBackupFolder.uuid, mockFolder.uuid], + 'folder', + ); }); it('When only ids are passed, then only folders by id should be searched', async () => { diff --git a/src/modules/folder/folder.usecase.ts b/src/modules/folder/folder.usecase.ts index 6e1508d31..07cb2f963 100644 --- a/src/modules/folder/folder.usecase.ts +++ b/src/modules/folder/folder.usecase.ts @@ -1,10 +1,12 @@ import { ConflictException, ForbiddenException, + Inject, Injectable, Logger, NotFoundException, UnprocessableEntityException, + forwardRef, } from '@nestjs/common'; import { CryptoService } from '../../externals/crypto/crypto.service'; import { User } from '../user/user.domain'; @@ -17,6 +19,8 @@ import { } from './folder.domain'; import { FolderAttributes } from './folder.attributes'; import { SequelizeFolderRepository } from './folder.repository'; +import { SharingService } from '../sharing/sharing.service'; +import { SharingItemType } from '../sharing/sharing.domain'; const invalidName = /[\\/]|^\s*$/; @@ -27,6 +31,8 @@ export class FolderUseCases { constructor( private folderRepository: SequelizeFolderRepository, private userRepository: SequelizeUserRepository, + @Inject(forwardRef(() => SharingService)) + private sharingUsecases: SharingService, private readonly cryptoService: CryptoService, ) {} @@ -329,6 +335,11 @@ export class FolderUseCases { }, ) : Promise.resolve(), + this.sharingUsecases.bulkRemoveSharings( + user, + folders.map((folder) => folder.uuid), + SharingItemType.Folder, + ), ]); } diff --git a/src/modules/sharing/sharing.domain.ts b/src/modules/sharing/sharing.domain.ts index 646fd5028..852961a66 100644 --- a/src/modules/sharing/sharing.domain.ts +++ b/src/modules/sharing/sharing.domain.ts @@ -14,6 +14,11 @@ export enum SharingType { Private = 'private', } +export enum SharingItemType { + File = 'file', + Folder = 'folder', +} + export interface SharingAttributes { id: string; itemId: ItemId; diff --git a/src/modules/sharing/sharing.repository.ts b/src/modules/sharing/sharing.repository.ts index 35be43e4b..77dd9b234 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -594,6 +594,36 @@ export class SequelizeSharingRepository implements SharingRepository { }); } + async bulkDeleteInvites( + itemIds: SharingInvite['itemId'][], + type: SharingInvite['itemType'], + ): Promise { + await this.sharingInvites.destroy({ + where: { + itemId: { + [Op.in]: itemIds, + }, + itemType: type, + }, + }); + } + + async bulkDeleteSharings( + userUuid: User['uuid'], + itemIds: SharingInvite['itemId'][], + type: SharingInvite['itemType'], + ): Promise { + await this.sharings.destroy({ + where: { + itemId: { + [Op.in]: itemIds, + }, + itemType: type, + ownerId: userUuid, + }, + }); + } + async deleteInvite(invite: SharingInvite): Promise { await this.sharingInvites.destroy({ where: { diff --git a/src/modules/sharing/sharing.service.spec.ts b/src/modules/sharing/sharing.service.spec.ts index 49b84684e..96a1da371 100644 --- a/src/modules/sharing/sharing.service.spec.ts +++ b/src/modules/sharing/sharing.service.spec.ts @@ -379,6 +379,26 @@ describe('Sharing Use Cases', () => { }); }); + describe('Bulk remove sharings', () => { + it('When function is called, then invitations and sharings are removed ', async () => { + const owner = newUser(); + const itemIds = ['uuid1', 'uuid2']; + const itemType = 'file'; + + await sharingService.bulkRemoveSharings(owner, itemIds, itemType); + + expect(sharingRepository.bulkDeleteInvites).toHaveBeenCalledWith( + itemIds, + itemType, + ); + expect(sharingRepository.bulkDeleteSharings).toHaveBeenCalledWith( + owner.uuid, + itemIds, + itemType, + ); + }); + }); + describe('Access to public shared item info', () => { const owner = newUser(); const otherUser = publicUser(); diff --git a/src/modules/sharing/sharing.service.ts b/src/modules/sharing/sharing.service.ts index 4e2b3b060..8a093ad66 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -2,9 +2,11 @@ import { BadRequestException, ConflictException, ForbiddenException, + Inject, Injectable, Logger, NotFoundException, + forwardRef, } from '@nestjs/common'; import { v4, validate as validateUuid } from 'uuid'; @@ -198,7 +200,9 @@ type SharingItemInfo = Pick; export class SharingService { constructor( private readonly sharingRepository: SequelizeSharingRepository, + @Inject(forwardRef(() => FileUseCases)) private readonly fileUsecases: FileUseCases, + @Inject(forwardRef(() => FolderUseCases)) private readonly folderUsecases: FolderUseCases, private readonly usersUsecases: UserUseCases, private readonly configService: ConfigService, @@ -1460,6 +1464,19 @@ export class SharingService { }); } + async bulkRemoveSharings( + user: User, + itemIds: Sharing['itemId'][], + itemType: Sharing['itemType'], + ) { + await this.sharingRepository.bulkDeleteInvites(itemIds, itemType); + await this.sharingRepository.bulkDeleteSharings( + user.uuid, + itemIds, + itemType, + ); + } + async getRoles(): Promise { return this.sharingRepository.findRoles(); } diff --git a/src/modules/user/user.usecase.ts b/src/modules/user/user.usecase.ts index 19864e47b..0dd542535 100644 --- a/src/modules/user/user.usecase.ts +++ b/src/modules/user/user.usecase.ts @@ -3,9 +3,11 @@ import { ForbiddenException, HttpException, HttpStatus, + Inject, Injectable, Logger, NotFoundException, + forwardRef, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Environment } from '@internxt/inxt-js'; @@ -129,7 +131,9 @@ export class UserUseCases { private userReferralsRepository: SequelizeUserReferralsRepository, private readonly attemptChangeEmailRepository: SequelizeAttemptChangeEmailRepository, private sharingRepository: SequelizeSharingRepository, + @Inject(forwardRef(() => FileUseCases)) private fileUseCases: FileUseCases, + @Inject(forwardRef(() => FolderUseCases)) private folderUseCases: FolderUseCases, private shareUseCases: ShareUseCases, private configService: ConfigService, @@ -137,7 +141,6 @@ export class UserUseCases { private networkService: BridgeService, private notificationService: NotificationService, private readonly paymentsService: PaymentsService, - private readonly newsletterService: NewsletterService, private readonly keyServerRepository: SequelizeKeyServerRepository, private readonly avatarService: AvatarService, private readonly mailerService: MailerService,