diff --git a/src/modules/sharing/guards/sharings-token.interface.ts b/src/modules/sharing/guards/sharings-token.interface.ts index 16c3ae759..7634b50c8 100644 --- a/src/modules/sharing/guards/sharings-token.interface.ts +++ b/src/modules/sharing/guards/sharings-token.interface.ts @@ -4,12 +4,18 @@ import { Workspace } from '../../workspaces/domains/workspaces.domain'; import { SharedWithType } from '../sharing.domain'; export interface SharingAccessTokenData { + sharedRootFolderId?: FolderAttributes['uuid']; + sharedWithType: SharedWithType; + parentFolderId?: FolderAttributes['parent']['uuid']; owner?: { uuid?: User['uuid']; + id?: User['id']; }; - sharedRootFolderId?: FolderAttributes['uuid']; - sharedWithType: SharedWithType; workspace?: { workspaceId: Workspace['id']; }; + folder?: { + uuid: FolderAttributes['uuid']; + id: FolderAttributes['id']; + }; } diff --git a/src/modules/sharing/models/index.ts b/src/modules/sharing/models/index.ts index 31a65b683..427b270a2 100644 --- a/src/modules/sharing/models/index.ts +++ b/src/modules/sharing/models/index.ts @@ -6,6 +6,7 @@ import { Default, ForeignKey, HasMany, + HasOne, Model, PrimaryKey, Table, @@ -157,6 +158,12 @@ export class SharingModel extends Model implements SharingAttributes { @Column(DataType.ENUM('public', 'private')) type: SharingAttributes['type']; + @HasOne(() => SharingRolesModel, { + foreignKey: 'sharingId', + sourceKey: 'id', + }) + role: RoleModel; + @Column createdAt: Date; diff --git a/src/modules/sharing/sharing.controller.ts b/src/modules/sharing/sharing.controller.ts index d95a113db..6bdf93bd8 100644 --- a/src/modules/sharing/sharing.controller.ts +++ b/src/modules/sharing/sharing.controller.ts @@ -18,6 +18,7 @@ import { Headers, Patch, UseFilters, + InternalServerErrorException, } from '@nestjs/common'; import { Response } from 'express'; import { @@ -919,39 +920,24 @@ export class SharingController { }) async getItemsSharedsWith( @UserDecorator() user: User, - @Query('limit') limit = 0, - @Query('offset') offset = 50, @Param('itemId') itemId: Sharing['itemId'], @Param('itemType') itemType: Sharing['itemType'], - @Res({ passthrough: true }) res: Response, - ): Promise<{ users: Array } | { error: string }> { + ): Promise<{ users: Array }> { try { const users = await this.sharingService.getItemSharedWith( user, itemId, itemType, - offset, - limit, ); return { users }; } catch (error) { - let errorMessage = error.message; - - if (error instanceof InvalidSharedFolderError) { - res.status(HttpStatus.BAD_REQUEST); - } else if (error instanceof UserNotInvitedError) { - res.status(HttpStatus.FORBIDDEN); - } else { - Logger.error( - `[SHARING/GETSHAREDWITHME] Error while getting shared with by folder id ${ - user.uuid - }, ${error.stack || 'No stack trace'}`, - ); - res.status(HttpStatus.INTERNAL_SERVER_ERROR); - errorMessage = 'Internal server error'; - } - return { error: errorMessage }; + Logger.error( + `[SHARING/GETSHAREDWITHME] Error while getting shared with by folder id ${ + user.uuid + }, ${error.stack || 'No stack trace'}`, + ); + throw error; } } diff --git a/src/modules/sharing/sharing.repository.spec.ts b/src/modules/sharing/sharing.repository.spec.ts index 69d4296aa..14334a17f 100644 --- a/src/modules/sharing/sharing.repository.spec.ts +++ b/src/modules/sharing/sharing.repository.spec.ts @@ -1,12 +1,23 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getModelToken } from '@nestjs/sequelize'; import { createMock } from '@golevelup/ts-jest'; -import { SharingModel } from './models'; -import { Sharing } from './sharing.domain'; +import { RoleModel, SharingModel } from './models'; +import { SharedWithType, Sharing } from './sharing.domain'; import { SequelizeSharingRepository } from './sharing.repository'; -import { newFile, newSharing, newUser } from '../../../test/fixtures'; -import { User } from '../user/user.domain'; +import { + newFile, + newFolder, + newSharing, + newUser, +} from '../../../test/fixtures'; import { v4 } from 'uuid'; +import { SharingRolesModel } from './models/sharing-roles.model'; +import { Op, Sequelize } from 'sequelize'; +import { WorkspaceItemUserModel } from '../workspaces/models/workspace-items-users.model'; +import { FileStatus } from '../file/file.domain'; +import { FileModel } from '../file/file.model'; +import { UserModel } from '../user/user.model'; +import { FolderModel } from '../folder/folder.model'; describe('SharingRepository', () => { let repository: SequelizeSharingRepository; @@ -25,19 +36,191 @@ describe('SharingRepository', () => { sharingModel = module.get(getModelToken(SharingModel)); }); - describe('findFilesByOwnerAndSharedWithTeamInworkspace', () => { - it('When files are searched by owner and team in workspace, then it should return the shared files', async () => { - const teamId = v4(); - const workspaceId = v4(); - const ownerId = v4(); + describe('findSharingsBySharedWithAndAttributes', () => { + it('When filters are included, then it should call the query with the correct filters', async () => { + const sharedWithValues = [v4(), v4()]; + const filters = { sharedWithType: SharedWithType.Individual }; const offset = 0; const limit = 10; - const orderBy = [['name', 'ASC']] as any; + + const expectedQuery = { + where: { + ...filters, + sharedWith: { + [Op.in]: sharedWithValues, + }, + }, + include: [ + { + model: SharingRolesModel, + include: [RoleModel], + }, + ], + limit, + offset, + order: [], + replacements: { + priorityRole: undefined, + }, + }; + + await repository.findSharingsBySharedWithAndAttributes( + sharedWithValues, + filters, + { offset, limit }, + ); + + expect(sharingModel.findAll).toHaveBeenCalledWith(expectedQuery); + }); + + it('When givePriorityToRole is provided, then it should call the query prioritizing the role', async () => { + const sharedWithValues = [v4(), v4()]; + const filters = {}; + const offset = 0; + const limit = 10; + const givePriorityToRole = 'admin'; + + const expectedQuery = { + where: { + ...filters, + sharedWith: { + [Op.in]: sharedWithValues, + }, + }, + include: [ + { + model: SharingRolesModel, + include: [RoleModel], + }, + ], + limit, + offset, + }; + + await repository.findSharingsBySharedWithAndAttributes( + sharedWithValues, + filters, + { offset, limit, givePriorityToRole }, + ); + + expect(sharingModel.findAll).toHaveBeenCalledWith( + expect.objectContaining({ + ...expectedQuery, + order: [ + [ + { + val: expect.stringContaining( + `CASE WHEN "role->role"."name" = :priorityRole THEN 1 ELSE 2 END`, + ), + }, + 'ASC', + ], + ], + replacements: { + priorityRole: givePriorityToRole, + }, + }), + ); + }); + + it('When no results are found, then it should return an empty array', async () => { + const sharedWithValues = [v4(), v4()]; + const filters = {}; + const offset = 0; + const limit = 10; + + jest.spyOn(sharingModel, 'findAll').mockResolvedValue([]); + + const result = await repository.findSharingsBySharedWithAndAttributes( + sharedWithValues, + filters, + { offset, limit }, + ); + + expect(result).toEqual([]); + }); + }); + + describe('findFilesSharedInWorkspaceByOwnerAndTeams', () => { + const ownerId = v4(); + const workspaceId = v4(); + const teamIds = [v4(), v4()]; + const offset = 0; + const limit = 10; + + it('When called, then it should call the query with the correct owner, teams and order', async () => { + const orderBy: [string, string][] = [['name', 'ASC']]; + + const expectedQuery = { + where: { + [Op.or]: [ + { + sharedWith: { [Op.in]: teamIds }, + sharedWithType: SharedWithType.WorkspaceTeam, + }, + { + '$file->workspaceUser.created_by$': ownerId, + }, + ], + }, + attributes: [ + [Sequelize.literal('MAX("SharingModel"."created_at")'), 'createdAt'], + ], + group: [ + 'SharingModel.item_id', + 'file.id', + 'file->workspaceUser.id', + 'file->workspaceUser->creator.id', + ], + include: [ + { + model: FileModel, + where: { + status: FileStatus.EXISTS, + }, + include: [ + { + model: WorkspaceItemUserModel, + as: 'workspaceUser', + required: true, + where: { + workspaceId, + }, + include: [ + { + model: UserModel, + as: 'creator', + attributes: ['uuid', 'email', 'name', 'lastname', 'avatar'], + }, + ], + }, + ], + }, + ], + order: orderBy, + limit, + offset, + }; + + await repository.findFilesSharedInWorkspaceByOwnerAndTeams( + ownerId, + workspaceId, + teamIds, + { offset, limit, order: orderBy }, + ); + + expect(sharingModel.findAll).toHaveBeenCalledWith( + expect.objectContaining(expectedQuery), + ); + }); + + it('When returned successfully, then it returns a folder and its creator', async () => { const sharing = newSharing(); const file = newFile(); const creator = newUser(); - const mockSharing = { + const sharedFileWithUser = { + ...sharing, get: jest.fn().mockReturnValue({ ...sharing, file: { @@ -51,39 +234,115 @@ describe('SharingRepository', () => { jest .spyOn(sharingModel, 'findAll') - .mockResolvedValue([mockSharing] as any); + .mockResolvedValue([sharedFileWithUser] as any); - const result = - await repository.findFilesByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, - ownerId, - offset, - limit, - orderBy, - ); + const result = await repository.findFilesSharedInWorkspaceByOwnerAndTeams( + ownerId, + workspaceId, + teamIds, + { offset, limit }, + ); - expect(result[0]).toBeInstanceOf(Sharing); - expect(result[0].file.user).toBeInstanceOf(User); - expect(result[0].file).toMatchObject({ ...file, user: creator }); + expect(result[0].file).toMatchObject({ + ...file, + user: { + uuid: creator.uuid, + email: creator.email, + name: creator.name, + lastname: creator.lastname, + avatar: creator.avatar, + }, + }); }); }); - describe('findFoldersByOwnerAndSharedWithTeamInworkspace', () => { + describe('findFoldersSharedInWorkspaceByOwnerAndTeams', () => { + const ownerId = v4(); const workspaceId = v4(); - it('When folders are searched by owner and team in workspace, then it should return the shared folders', async () => { - const teamId = v4(); - const ownerId = v4(); - const offset = 0; - const limit = 10; - const orderBy = [['name', 'ASC']] as any; + const teamIds = [v4(), v4()]; + const offset = 0; + const limit = 10; - const mockSharing = { + it('When called, then it should call the query with the correct owner, teams and order', async () => { + const orderBy: [string, string][] = [['plainName', 'ASC']]; + + const expectedQuery = { + where: { + [Op.or]: [ + { + sharedWith: { [Op.in]: teamIds }, + sharedWithType: SharedWithType.WorkspaceTeam, + }, + { + '$folder->workspaceUser.created_by$': ownerId, + }, + ], + }, + attributes: [ + [Sequelize.literal('MAX("SharingModel"."created_at")'), 'createdAt'], + ], + group: [ + 'SharingModel.item_id', + 'folder.id', + 'folder->workspaceUser.id', + 'folder->workspaceUser->creator.id', + ], + include: [ + { + model: FolderModel, + where: { + deleted: false, + removed: false, + }, + include: [ + { + model: WorkspaceItemUserModel, + required: true, + where: { + workspaceId, + }, + include: [ + { + model: UserModel, + as: 'creator', + attributes: ['uuid', 'email', 'name', 'lastname', 'avatar'], + }, + ], + }, + ], + }, + ], + order: orderBy, + limit, + offset, + }; + + await repository.findFoldersSharedInWorkspaceByOwnerAndTeams( + ownerId, + workspaceId, + teamIds, + { offset, limit, order: orderBy }, + ); + + expect(sharingModel.findAll).toHaveBeenCalledWith( + expect.objectContaining(expectedQuery), + ); + }); + + it('When returned successfully, then it returns a folder and its creator', async () => { + const orderBy: [string, string][] = [['plainName', 'ASC']]; + const sharedFolder = newSharing(); + const folder = newFolder(); + const creator = newUser(); + + const sharedFolderWithUser = { + ...sharedFolder, get: jest.fn().mockReturnValue({ - ...newSharing(), + ...sharedFolder, folder: { + ...folder, workspaceUser: { - creator: newUser(), + creator, }, }, }), @@ -91,20 +350,27 @@ describe('SharingRepository', () => { jest .spyOn(sharingModel, 'findAll') - .mockResolvedValue([mockSharing] as any); + .mockResolvedValue([sharedFolderWithUser] as any); const result = - await repository.findFoldersByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, + await repository.findFoldersSharedInWorkspaceByOwnerAndTeams( ownerId, - offset, - limit, - orderBy, + workspaceId, + teamIds, + { offset, limit, order: orderBy }, ); expect(result[0]).toBeInstanceOf(Sharing); - expect(result[0].folder.user).toBeInstanceOf(User); + expect(result[0].folder).toMatchObject({ + ...folder, + user: { + uuid: creator.uuid, + email: creator.email, + name: creator.name, + lastname: creator.lastname, + avatar: creator.avatar, + }, + }); }); }); }); diff --git a/src/modules/sharing/sharing.repository.ts b/src/modules/sharing/sharing.repository.ts index 4de88afd2..9f2be5e3f 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -453,19 +453,60 @@ export class SequelizeSharingRepository implements SharingRepository { }); } - async findFilesByOwnerAndSharedWithTeamInworkspace( - workspaceId: WorkspaceAttributes['id'], - teamId: WorkspaceTeamAttributes['id'], + async findSharingsBySharedWithAndAttributes( + sharedWithValues: SharingAttributes['sharedWith'][], + filters: Omit, 'sharedWith'> = {}, + options?: { offset: number; limit: number; givePriorityToRole?: string }, + ): Promise { + const where: WhereOptions = { + ...filters, + sharedWith: { + [Op.in]: sharedWithValues, + }, + }; + + const queryOrder = []; + if (options?.givePriorityToRole) { + queryOrder.push([ + sequelize.literal( + `CASE WHEN "role->role"."name" = :priorityRole THEN 1 ELSE 2 END`, + ), + 'ASC', + ]); + } + + const sharings = await this.sharings.findAll({ + where, + include: [ + { + model: SharingRolesModel, + include: [RoleModel], + }, + ], + limit: options.limit, + offset: options.offset, + order: queryOrder, + replacements: { + priorityRole: options?.givePriorityToRole, + }, + }); + + return sharings.map((sharing) => + Sharing.build(sharing.get({ plain: true })), + ); + } + + async findFilesSharedInWorkspaceByOwnerAndTeams( ownerId: WorkspaceItemUserAttributes['createdBy'], - offset: number, - limit: number, - orderBy?: [string, string][], + workspaceId: WorkspaceAttributes['id'], + teamIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { const sharedFiles = await this.sharings.findAll({ where: { [Op.or]: [ { - sharedWith: teamId, + sharedWith: { [Op.in]: teamIds }, sharedWithType: SharedWithType.WorkspaceTeam, }, { @@ -477,10 +518,10 @@ export class SequelizeSharingRepository implements SharingRepository { [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], ], group: [ + 'SharingModel.item_id', 'file.id', 'file->workspaceUser.id', 'file->workspaceUser->creator.id', - 'SharingModel.item_id', ], include: [ { @@ -507,9 +548,9 @@ export class SequelizeSharingRepository implements SharingRepository { ], }, ], - order: orderBy, - limit, - offset, + order: options.order, + limit: options.limit, + offset: options.offset, }); return sharedFiles.map((shared) => { @@ -527,19 +568,17 @@ export class SequelizeSharingRepository implements SharingRepository { }); } - async findFoldersByOwnerAndSharedWithTeamInworkspace( - workspaceId: WorkspaceAttributes['id'], - teamId: WorkspaceTeamAttributes['id'], + async findFoldersSharedInWorkspaceByOwnerAndTeams( ownerId: WorkspaceItemUserAttributes['createdBy'], - offset: number, - limit: number, - orderBy?: [string, string][], + workspaceId: WorkspaceAttributes['id'], + teamsIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { const sharedFolders = await this.sharings.findAll({ where: { [Op.or]: [ { - sharedWith: teamId, + sharedWith: { [Op.in]: teamsIds }, sharedWithType: SharedWithType.WorkspaceTeam, }, { @@ -551,10 +590,10 @@ export class SequelizeSharingRepository implements SharingRepository { [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], ], group: [ + 'SharingModel.item_id', 'folder.id', 'folder->workspaceUser.id', 'folder->workspaceUser->creator.id', - 'SharingModel.item_id', ], include: [ { @@ -581,9 +620,9 @@ export class SequelizeSharingRepository implements SharingRepository { ], }, ], - order: orderBy, - limit, - offset, + order: options.order, + limit: options.limit, + offset: options.offset, }); return sharedFolders.map((shared) => { diff --git a/src/modules/sharing/sharing.service.spec.ts b/src/modules/sharing/sharing.service.spec.ts index 6aa50ceaa..1f4c30fe3 100644 --- a/src/modules/sharing/sharing.service.spec.ts +++ b/src/modules/sharing/sharing.service.spec.ts @@ -490,25 +490,72 @@ describe('Sharing Use Cases', () => { }); }); - describe('getSharedFilesInWorkspaces', () => { + describe('removeSharing', () => { + const owner = newUser(); + const itemFile = newFile(); + const itemId = itemFile.uuid; + const itemType = SharingItemType.File; + const sharing = newSharing({ owner, item: itemFile }); + + it('When sharing exists and user is owner, then it removes invites and sharings', async () => { + sharingRepository.findOneSharing.mockResolvedValue(sharing); + + await sharingService.removeSharing(owner, itemId, itemType); + + expect(sharingRepository.findOneSharing).toHaveBeenCalledWith({ + itemId, + itemType, + }); + expect(sharingRepository.deleteInvitesBy).toHaveBeenCalledWith({ + itemId, + itemType, + }); + expect(sharingRepository.deleteSharingsBy).toHaveBeenCalledWith({ + itemId, + itemType, + }); + }); + + it('When sharing does not exist, then it does nothing', async () => { + sharingRepository.findOneSharing.mockResolvedValue(null); + + await sharingService.removeSharing(owner, itemId, itemType); + + expect(sharingRepository.findOneSharing).toHaveBeenCalledWith({ + itemId, + itemType, + }); + expect(sharingRepository.deleteInvitesBy).not.toHaveBeenCalled(); + expect(sharingRepository.deleteSharingsBy).not.toHaveBeenCalled(); + }); + + it('When user is not owner, then it throws', async () => { + const otherUser = newUser(); + + sharingRepository.findOneSharing.mockResolvedValue(sharing); + + await expect( + sharingService.removeSharing(otherUser, itemId, itemType), + ).rejects.toThrow(ForbiddenException); + }); + }); + + describe('getSharedFilesInWorkspaceByTeams', () => { const user = newUser(); - const teamId = v4(); + const teamIds = [v4(), v4()]; const workspaceId = v4(); const offset = 0; const limit = 10; const order: [string, string][] = [['name', 'asc']]; - it('When files are shared with the team, then it should return the files', async () => { + it('When files are shared with teams user belongs to, then it should return the files', async () => { const sharing = newSharing(); sharing.file = newFile({ owner: newUser() }); const filesWithSharedInfo = [sharing]; jest - .spyOn( - sharingRepository, - 'findFilesByOwnerAndSharedWithTeamInworkspace', - ) + .spyOn(sharingRepository, 'findFilesSharedInWorkspaceByOwnerAndTeams') .mockResolvedValue(filesWithSharedInfo); jest @@ -517,13 +564,11 @@ describe('Sharing Use Cases', () => { jest.spyOn(usersUsecases, 'getAvatarUrl').mockResolvedValue('avatar-url'); - const result = await sharingService.getSharedFilesInWorkspaces( + const result = await sharingService.getSharedFilesInWorkspaceByTeams( user, workspaceId, - teamId, - offset, - limit, - order, + teamIds, + { offset, limit, order }, ); expect(result).toEqual( @@ -547,21 +592,16 @@ describe('Sharing Use Cases', () => { ); }); - it('When no files are shared with the team, then it should return nothing', async () => { + it('When no files are shared with teams user belongs to, then it should return nothing', async () => { jest - .spyOn( - sharingRepository, - 'findFilesByOwnerAndSharedWithTeamInworkspace', - ) + .spyOn(sharingRepository, 'findFilesSharedInWorkspaceByOwnerAndTeams') .mockResolvedValue([]); - const result = await sharingService.getSharedFilesInWorkspaces( + const result = await sharingService.getSharedFilesInWorkspaceByTeams( user, workspaceId, - teamId, - offset, - limit, - order, + teamIds, + { offset, limit, order }, ); expect(result).toEqual({ @@ -580,84 +620,33 @@ describe('Sharing Use Cases', () => { const error = new Error('Database error'); jest - .spyOn( - sharingRepository, - 'findFilesByOwnerAndSharedWithTeamInworkspace', - ) + .spyOn(sharingRepository, 'findFilesSharedInWorkspaceByOwnerAndTeams') .mockRejectedValue(error); await expect( - sharingService.getSharedFilesInWorkspaces( + sharingService.getSharedFilesInWorkspaceByTeams( user, workspaceId, - teamId, - offset, - limit, - order, + teamIds, + { + offset, + limit, + order, + }, ), ).rejects.toThrow(error); }); }); - describe('removeSharing', () => { - const owner = newUser(); - const itemFile = newFile(); - const itemId = itemFile.uuid; - const itemType = SharingItemType.File; - const sharing = newSharing({ owner, item: itemFile }); - - it('When sharing exists and user is owner, then it removes invites and sharings', async () => { - sharingRepository.findOneSharing.mockResolvedValue(sharing); - - await sharingService.removeSharing(owner, itemId, itemType); - - expect(sharingRepository.findOneSharing).toHaveBeenCalledWith({ - itemId, - itemType, - }); - expect(sharingRepository.deleteInvitesBy).toHaveBeenCalledWith({ - itemId, - itemType, - }); - expect(sharingRepository.deleteSharingsBy).toHaveBeenCalledWith({ - itemId, - itemType, - }); - }); - - it('When sharing does not exist, then it does nothing', async () => { - sharingRepository.findOneSharing.mockResolvedValue(null); - - await sharingService.removeSharing(owner, itemId, itemType); - - expect(sharingRepository.findOneSharing).toHaveBeenCalledWith({ - itemId, - itemType, - }); - expect(sharingRepository.deleteInvitesBy).not.toHaveBeenCalled(); - expect(sharingRepository.deleteSharingsBy).not.toHaveBeenCalled(); - }); - - it('When user is not owner, then it throws', async () => { - const otherUser = newUser(); - - sharingRepository.findOneSharing.mockResolvedValue(sharing); - - await expect( - sharingService.removeSharing(otherUser, itemId, itemType), - ).rejects.toThrow(ForbiddenException); - }); - }); - - describe('getSharedFoldersInWorkspace', () => { + describe('getSharedFoldersInWorkspaceByTeams', () => { const user = newUser(); - const teamId = v4(); + const teamIds = [v4(), v4()]; const workspaceId = v4(); const offset = 0; const limit = 10; const order: [string, string][] = [['name', 'asc']]; - it('When folders are shared with the team, then it should return the folders', async () => { + it('When folders are shared with a team the user belongs to, then it should return the folders', async () => { const sharing = newSharing(); const folder = newFolder(); folder.user = newUser(); @@ -665,10 +654,7 @@ describe('Sharing Use Cases', () => { const foldersWithSharedInfo = [sharing]; jest - .spyOn( - sharingRepository, - 'findFoldersByOwnerAndSharedWithTeamInworkspace', - ) + .spyOn(sharingRepository, 'findFoldersSharedInWorkspaceByOwnerAndTeams') .mockResolvedValue(foldersWithSharedInfo); jest @@ -677,13 +663,11 @@ describe('Sharing Use Cases', () => { jest.spyOn(usersUsecases, 'getAvatarUrl').mockResolvedValue('avatar-url'); - const result = await sharingService.getSharedFoldersInWorkspace( + const result = await sharingService.getSharedFoldersInWorkspaceByTeams( user, workspaceId, - teamId, - offset, - limit, - order, + teamIds, + { offset, limit, order }, ); expect(result).toEqual( @@ -707,21 +691,16 @@ describe('Sharing Use Cases', () => { ); }); - it('When no folders are shared with the team, then it should return an empty folders array', async () => { + it('When no folders are shared with a team the user belongs to, then it should return an empty folders array', async () => { jest - .spyOn( - sharingRepository, - 'findFoldersByOwnerAndSharedWithTeamInworkspace', - ) + .spyOn(sharingRepository, 'findFoldersSharedInWorkspaceByOwnerAndTeams') .mockResolvedValue([]); - const result = await sharingService.getSharedFoldersInWorkspace( + const result = await sharingService.getSharedFoldersInWorkspaceByTeams( user, workspaceId, - teamId, - offset, - limit, - order, + teamIds, + { offset, limit, order }, ); expect(result).toEqual({ @@ -740,24 +719,24 @@ describe('Sharing Use Cases', () => { const error = new Error('Database error'); jest - .spyOn( - sharingRepository, - 'findFoldersByOwnerAndSharedWithTeamInworkspace', - ) + .spyOn(sharingRepository, 'findFoldersSharedInWorkspaceByOwnerAndTeams') .mockRejectedValue(error); await expect( - sharingService.getSharedFoldersInWorkspace( + sharingService.getSharedFoldersInWorkspaceByTeams( user, workspaceId, - teamId, - offset, - limit, - order, + teamIds, + { + offset, + limit, + order, + }, ), ).rejects.toThrow(error); }); }); + 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 fca810cff..4a4aec90a 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -53,6 +53,7 @@ import { Environment } from '@internxt/inxt-js'; import { SequelizeUserReferralsRepository } from '../user/user-referrals.repository'; import { SharingNotFoundException } from './exception/sharing-not-found.exception'; import { Workspace } from '../workspaces/domains/workspaces.domain'; +import { WorkspaceTeamAttributes } from '../workspaces/attributes/workspace-team.attributes'; export class InvalidOwnerError extends Error { constructor() { @@ -173,7 +174,7 @@ export class PasswordNeededError extends ForbiddenException { } } -type SharingInfo = Pick< +export type SharingInfo = Pick< User, 'name' | 'lastname' | 'uuid' | 'avatar' | 'email' > & { @@ -216,6 +217,18 @@ export class SharingService { return this.sharingRepository.findOneSharingBy(where); } + findSharingsBySharedWithAndAttributes( + sharedWithValues: Sharing['sharedWith'][], + filters: Omit, 'sharedWith'> = {}, + options?: { offset: number; limit: number; givePriorityToRole?: string }, + ): Promise { + return this.sharingRepository.findSharingsBySharedWithAndAttributes( + sharedWithValues, + filters, + options, + ); + } + findSharingRoleBy(where: Partial) { return this.sharingRepository.findSharingRoleBy(where); } @@ -1784,50 +1797,42 @@ export class SharingService { }; } - async getSharedFoldersInWorkspace( + async getSharedFilesInWorkspaceByTeams( user: User, workspaceId: Workspace['id'], - teamId: Sharing['sharedWith'], - offset: number, - limit: number, - order: [string, string][], + teamIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { - const foldersWithSharedInfo = - await this.sharingRepository.findFoldersByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, + const filesWithSharedInfo = + await this.sharingRepository.findFilesSharedInWorkspaceByOwnerAndTeams( user.uuid, - offset, - limit, - order, + workspaceId, + teamIds, + options, ); - const folders = (await Promise.all( - foldersWithSharedInfo.map(async (folderWithSharedInfo) => { - const avatar = folderWithSharedInfo.folder?.user?.avatar; + const files = (await Promise.all( + filesWithSharedInfo.map(async (fileWithSharedInfo) => { + const avatar = fileWithSharedInfo.file?.user?.avatar; return { - ...folderWithSharedInfo.folder, - plainName: - folderWithSharedInfo.folder.plainName || - this.folderUsecases.decryptFolderName(folderWithSharedInfo.folder) - .plainName, - sharingId: folderWithSharedInfo.id, - encryptionKey: folderWithSharedInfo.encryptionKey, - dateShared: folderWithSharedInfo.createdAt, - sharedWithMe: user.uuid !== folderWithSharedInfo.folder.user.uuid, + ...fileWithSharedInfo.file, + plainName: fileWithSharedInfo.file.plainName, + sharingId: fileWithSharedInfo.id, + encryptionKey: fileWithSharedInfo.encryptionKey, + dateShared: fileWithSharedInfo.createdAt, user: { - ...folderWithSharedInfo.folder.user, + ...fileWithSharedInfo.file.user, avatar: avatar ? await this.usersUsecases.getAvatarUrl(avatar) : null, }, }; }), - )) as FolderWithSharedInfo[]; + )) as FileWithSharedInfo[]; return { - folders: folders, - files: [], + folders: [], + files: files, credentials: { networkPass: user.userId, networkUser: user.bridgeUser, @@ -1837,48 +1842,43 @@ export class SharingService { }; } - async getSharedFilesInWorkspaces( + async getSharedFoldersInWorkspaceByTeams( user: User, workspaceId: Workspace['id'], - teamId: Sharing['sharedWith'], - offset: number, - limit: number, - order: [string, string][], + teamIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { - const filesWithSharedInfo = - await this.sharingRepository.findFilesByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, + const foldersWithSharedInfo = + await this.sharingRepository.findFoldersSharedInWorkspaceByOwnerAndTeams( user.uuid, - offset, - limit, - order, + workspaceId, + teamIds, + options, ); - const files = (await Promise.all( - filesWithSharedInfo.map(async (fileWithSharedInfo) => { - const avatar = fileWithSharedInfo.file?.user?.avatar; + const folders = (await Promise.all( + foldersWithSharedInfo.map(async (folderWithSharedInfo) => { + const avatar = folderWithSharedInfo.folder?.user?.avatar; return { - ...fileWithSharedInfo.file, - plainName: - fileWithSharedInfo.file.plainName || - this.fileUsecases.decrypFileName(fileWithSharedInfo.file).plainName, - sharingId: fileWithSharedInfo.id, - encryptionKey: fileWithSharedInfo.encryptionKey, - dateShared: fileWithSharedInfo.createdAt, + ...folderWithSharedInfo.folder, + plainName: folderWithSharedInfo.folder.plainName, + sharingId: folderWithSharedInfo.id, + encryptionKey: folderWithSharedInfo.encryptionKey, + dateShared: folderWithSharedInfo.createdAt, + sharedWithMe: user.uuid !== folderWithSharedInfo.folder.user.uuid, user: { - ...fileWithSharedInfo.file.user, + ...folderWithSharedInfo.folder.user, avatar: avatar ? await this.usersUsecases.getAvatarUrl(avatar) : null, }, }; }), - )) as FileWithSharedInfo[]; + )) as FolderWithSharedInfo[]; return { - folders: [], - files: files, + folders: folders, + files: [], credentials: { networkPass: user.userId, networkUser: user.bridgeUser, @@ -1888,12 +1888,14 @@ export class SharingService { }; } + async findSharingsWithRolesByItem(item: File | Folder) { + return this.sharingRepository.findSharingsWithRolesByItem(item); + } + async getItemSharedWith( user: User, itemId: Sharing['itemId'], itemType: Sharing['itemType'], - offset: number, - limit: number, ): Promise { let item: Item; @@ -1909,8 +1911,7 @@ export class SharingService { throw new NotFoundException('Item not found'); } - const sharingsWithRoles = - await this.sharingRepository.findSharingsWithRolesByItem(item); + const sharingsWithRoles = await this.findSharingsWithRolesByItem(item); if (sharingsWithRoles.length === 0) { throw new BadRequestException('This item is not being shared'); diff --git a/src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts b/src/modules/workspaces/dto/get-shared-items.dto.ts similarity index 94% rename from src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts rename to src/modules/workspaces/dto/get-shared-items.dto.ts index 4d811246a..337ffe77e 100644 --- a/src/modules/workspaces/dto/get-items-inside-shared-folder.dto.ts +++ b/src/modules/workspaces/dto/get-shared-items.dto.ts @@ -3,7 +3,7 @@ import { Type } from 'class-transformer'; import { ApiPropertyOptional } from '@nestjs/swagger'; import { OrderBy } from '../../../common/order.type'; -export class GetItemsInsideSharedFolderDtoQuery { +export class GetSharedItemsDto { @ApiPropertyOptional({ description: 'Order by', example: 'name:asc', diff --git a/src/modules/workspaces/dto/shared-with.dto.ts b/src/modules/workspaces/dto/shared-with.dto.ts new file mode 100644 index 000000000..75b7738f7 --- /dev/null +++ b/src/modules/workspaces/dto/shared-with.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { WorkspaceItemUser } from '../domains/workspace-item-user.domain'; +import { WorkspaceItemType } from '../attributes/workspace-items-users.attributes'; + +export class GetSharedWithDto { + @ApiProperty({ + example: 'uuid', + description: 'The uuid of the item to share', + }) + @IsNotEmpty() + itemId: WorkspaceItemUser['itemId']; + + @ApiProperty({ + example: WorkspaceItemType, + description: 'The type of the resource to share', + }) + @IsNotEmpty() + @IsEnum(WorkspaceItemType) + itemType: WorkspaceItemUser['itemType']; +} diff --git a/src/modules/workspaces/repositories/workspaces.repository.spec.ts b/src/modules/workspaces/repositories/workspaces.repository.spec.ts index 1806c6f67..5a0a5cc14 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.spec.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.spec.ts @@ -337,4 +337,44 @@ describe('SequelizeWorkspaceRepository', () => { expect(response).toEqual([]); }); }); + + describe('findWorkspaceAndDefaultUser', () => { + it('When workspace and default user are found, it should return successfully', async () => { + const mockWorkspace = newWorkspace(); + const mockUser = newUser(); + const mockWorkspaceWithUser = { + id: mockWorkspace.id, + workpaceUser: { + ...mockUser, + get: jest.fn().mockReturnValue(mockUser), + }, + toJSON: jest.fn().mockReturnValue({ + id: mockWorkspace.id, + }), + }; + + jest + .spyOn(workspaceModel, 'findOne') + .mockResolvedValueOnce(mockWorkspaceWithUser as any); + + const result = await repository.findWorkspaceAndDefaultUser( + mockWorkspace.id, + ); + + expect(result).toEqual({ + workspaceUser: expect.any(User), + workspace: expect.any(Workspace), + }); + expect(result.workspace.id).toEqual(mockWorkspace.id); + expect(result.workspaceUser.uuid).toEqual(mockUser.uuid); + }); + + it('When workspace is not found, it should return null', async () => { + jest.spyOn(workspaceModel, 'findOne').mockResolvedValueOnce(null); + + const result = + await repository.findWorkspaceAndDefaultUser('non-existent-id'); + expect(result).toBeNull(); + }); + }); }); diff --git a/src/modules/workspaces/repositories/workspaces.repository.ts b/src/modules/workspaces/repositories/workspaces.repository.ts index 1fe7679ca..f673eff2b 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.ts @@ -28,18 +28,43 @@ import { FolderModel } from '../../folder/folder.model'; export class SequelizeWorkspaceRepository { constructor( @InjectModel(WorkspaceModel) - private modelWorkspace: typeof WorkspaceModel, + private readonly modelWorkspace: typeof WorkspaceModel, @InjectModel(WorkspaceUserModel) - private modelWorkspaceUser: typeof WorkspaceUserModel, + private readonly modelWorkspaceUser: typeof WorkspaceUserModel, @InjectModel(WorkspaceInviteModel) - private modelWorkspaceInvite: typeof WorkspaceInviteModel, + private readonly modelWorkspaceInvite: typeof WorkspaceInviteModel, @InjectModel(WorkspaceItemUserModel) - private modelWorkspaceItemUser: typeof WorkspaceItemUserModel, + private readonly modelWorkspaceItemUser: typeof WorkspaceItemUserModel, ) {} async findById(id: WorkspaceAttributes['id']): Promise { const workspace = await this.modelWorkspace.findByPk(id); return workspace ? this.toDomain(workspace) : null; } + + async findWorkspaceAndDefaultUser( + workspaceId: WorkspaceAttributes['id'], + ): Promise<{ workspaceUser: User; workspace: Workspace } | null> { + const workspaceAndDefaultUser = await this.modelWorkspace.findOne({ + where: { id: workspaceId }, + include: { + model: UserModel, + as: 'workpaceUser', + required: true, + }, + }); + + if (!workspaceAndDefaultUser) { + return null; + } + + return { + workspaceUser: User.build({ + ...workspaceAndDefaultUser.workpaceUser.get({ plain: true }), + }), + workspace: this.toDomain(workspaceAndDefaultUser), + }; + } + async findByOwner(ownerId: Workspace['ownerId']): Promise { const workspaces = await this.modelWorkspace.findAll({ where: { ownerId }, diff --git a/src/modules/workspaces/workspaces.controller.spec.ts b/src/modules/workspaces/workspaces.controller.spec.ts index 23d9eeee9..c849cb69b 100644 --- a/src/modules/workspaces/workspaces.controller.spec.ts +++ b/src/modules/workspaces/workspaces.controller.spec.ts @@ -12,23 +12,17 @@ import { } from '../../../test/fixtures'; import { v4 } from 'uuid'; import { WorkspaceUserMemberDto } from './dto/workspace-user-member.dto'; -import { SharingService } from '../sharing/sharing.service'; import { CreateWorkspaceFolderDto } from './dto/create-workspace-folder.dto'; import { WorkspaceItemType } from './attributes/workspace-items-users.attributes'; describe('Workspace Controller', () => { let workspacesController: WorkspacesController; let workspacesUsecases: DeepMocked; - let sharingUseCases: DeepMocked; beforeEach(async () => { workspacesUsecases = createMock(); - sharingUseCases = createMock(); - workspacesController = new WorkspacesController( - workspacesUsecases, - sharingUseCases, - ); + workspacesController = new WorkspacesController(workspacesUsecases); }); it('should be defined', () => { @@ -503,71 +497,93 @@ describe('Workspace Controller', () => { }); }); - describe('GET /:workspaceId/teams/:teamId/shared/files', () => { - it('When shared files are requested, then it should call the service with the respective arguments', async () => { - const user = newUser(); - const teamId = v4(); - const workspaceId = v4(); - const orderBy = 'createdAt:ASC'; - const page = 1; - const perPage = 50; - const order = [['createdAt', 'ASC']]; - - await workspacesController.getSharedFiles( + describe('GET /:workspaceId/shared/files', () => { + const user = newUser(); + const workspaceId = v4(); + const orderBy = 'createdAt:ASC'; + const page = 1; + const perPage = 50; + const order = [['createdAt', 'ASC']]; + + it('When files shared with user teams are requested, then it should call the service with the respective arguments', async () => { + await workspacesController.getSharedFilesInWorkspace( workspaceId, - teamId, user, orderBy, - page, - perPage, + { page, perPage }, ); - expect(sharingUseCases.getSharedFilesInWorkspaces).toHaveBeenCalledWith( + expect(workspacesUsecases.getSharedFilesInWorkspace).toHaveBeenCalledWith( user, workspaceId, - teamId, - page, - perPage, - order, + { + offset: page, + limit: perPage, + order, + }, ); }); }); - describe('GET /:workspaceId/teams/:teamId/shared/folders', () => { - it('When shared folders are requested, then it should call the service with the respective arguments', async () => { + describe('GET /:workspaceId/shared/folders', () => { + const user = newUser(); + const workspaceId = v4(); + const orderBy = 'createdAt:ASC'; + const page = 1; + const perPage = 50; + const order = [['createdAt', 'ASC']]; + + it('When folders shared with user teams are requested, then it should call the service with the respective arguments', async () => { + await workspacesController.getSharedFoldersInWorkspace( + workspaceId, + user, + orderBy, + { page, perPage }, + ); + + expect( + workspacesUsecases.getSharedFoldersInWorkspace, + ).toHaveBeenCalledWith(user, workspaceId, { + offset: page, + limit: perPage, + order, + }); + }); + }); + + describe('GET /:workspaceId/shared/:sharedFolderId/files', () => { + it('When files inside a shared folder are requested, then it should call the service with the respective arguments', async () => { const user = newUser(); - const teamId = v4(); const workspaceId = v4(); + const sharedFolderId = v4(); const orderBy = 'createdAt:ASC'; + const token = 'token'; const page = 1; const perPage = 50; const order = [['createdAt', 'ASC']]; - await workspacesController.getSharedFolders( + await workspacesController.getFilesInSharingFolder( workspaceId, - teamId, user, - orderBy, - page, - perPage, + sharedFolderId, + { token, page, perPage, orderBy }, ); - expect(sharingUseCases.getSharedFoldersInWorkspace).toHaveBeenCalledWith( - user, + expect(workspacesUsecases.getItemsInSharedFolder).toHaveBeenCalledWith( workspaceId, - teamId, - page, - perPage, - order, + user, + sharedFolderId, + WorkspaceItemType.File, + token, + { page, perPage, order }, ); }); }); - describe('GET /:workspaceId/teams/:teamId/shared/:sharedFolderId/files', () => { - it('When files inside a shared folder are requested, then it should call the service with the respective arguments', async () => { + describe('GET /:workspaceId/shared/:sharedFolderId/folders', () => { + it('When folders inside a shared folder are requested, then it should call the service with the respective arguments', async () => { const user = newUser(); const workspaceId = v4(); - const teamId = v4(); const sharedFolderId = v4(); const orderBy = 'createdAt:ASC'; const token = 'token'; @@ -575,9 +591,8 @@ describe('Workspace Controller', () => { const perPage = 50; const order = [['createdAt', 'ASC']]; - await workspacesController.getFilesInsideSharedFolder( + await workspacesController.getFoldersInSharingFolder( workspaceId, - teamId, user, sharedFolderId, { token, page, perPage, orderBy }, @@ -585,10 +600,9 @@ describe('Workspace Controller', () => { expect(workspacesUsecases.getItemsInSharedFolder).toHaveBeenCalledWith( workspaceId, - teamId, user, sharedFolderId, - WorkspaceItemType.File, + WorkspaceItemType.Folder, token, { page, perPage, order }, ); diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 9871ef5ee..033ed4296 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -62,22 +62,18 @@ import { SharingPermissionsGuard } from '../sharing/guards/sharing-permissions.g import { RequiredSharingPermissions } from '../sharing/guards/sharing-permissions.decorator'; import { SharingActionName } from '../sharing/sharing.domain'; import { WorkspaceItemType } from './attributes/workspace-items-users.attributes'; -import { SharingService } from '../sharing/sharing.service'; -import { WorkspaceTeam } from './domains/workspace-team.domain'; -import { GetItemsInsideSharedFolderDtoQuery } from './dto/get-items-inside-shared-folder.dto'; import { WorkspaceUserAttributes } from './attributes/workspace-users.attributes'; import { ChangeUserAssignedSpaceDto } from './dto/change-user-assigned-space.dto'; import { Public } from '../auth/decorators/public.decorator'; import { BasicPaginationDto } from '../../common/dto/basic-pagination.dto'; +import { GetSharedItemsDto } from './dto/get-shared-items.dto'; +import { GetSharedWithDto } from './dto/shared-with.dto'; @ApiTags('Workspaces') @Controller('workspaces') @UseFilters(ExtendedHttpExceptionFilter) export class WorkspacesController { - constructor( - private workspaceUseCases: WorkspacesUsecases, - private sharingUseCases: SharingService, - ) {} + constructor(private readonly workspaceUseCases: WorkspacesUsecases) {} @Get('/') @ApiOperation({ @@ -579,80 +575,76 @@ export class WorkspacesController { ); } - @Get(':workspaceId/teams/:teamId/shared/files') + @Get([':workspaceId/teams/:teamId/shared/files', ':workspaceId/shared/files']) @ApiOperation({ - summary: 'Get shared files with a team', + summary: 'Get shared files in teams', }) @UseGuards(WorkspaceGuard) - @WorkspaceRequiredAccess(AccessContext.TEAM, WorkspaceRole.MEMBER) - async getSharedFiles( + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getSharedFilesInWorkspace( @Param('workspaceId', ValidateUUIDPipe) workspaceId: WorkspaceTeamAttributes['id'], - @Param('teamId', ValidateUUIDPipe) - teamId: WorkspaceTeamAttributes['id'], @UserDecorator() user: User, @Query('orderBy') orderBy: OrderBy, - @Query('page') page = 0, - @Query('perPage') perPage = 50, + @Query() pagination: GetSharedItemsDto, ) { const order = orderBy ? [orderBy.split(':') as [string, string]] : undefined; - return this.sharingUseCases.getSharedFilesInWorkspaces( - user, - workspaceId, - teamId, - page, - perPage, + return this.workspaceUseCases.getSharedFilesInWorkspace(user, workspaceId, { + offset: pagination.page, + limit: pagination.perPage, order, - ); + }); } - @Get(':workspaceId/teams/:teamId/shared/folders') + @Get([ + ':workspaceId/teams/:teamId/shared/folders', + ':workspaceId/shared/folders', + ]) @ApiOperation({ - summary: 'Get shared folders with a team', + summary: 'Get shared folders in teams', }) @UseGuards(WorkspaceGuard) - @WorkspaceRequiredAccess(AccessContext.TEAM, WorkspaceRole.MEMBER) - async getSharedFolders( + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getSharedFoldersInWorkspace( @Param('workspaceId', ValidateUUIDPipe) workspaceId: WorkspaceTeamAttributes['id'], - @Param('teamId', ValidateUUIDPipe) - teamId: WorkspaceTeamAttributes['id'], @UserDecorator() user: User, @Query('orderBy') orderBy: OrderBy, - @Query('page') page = 0, - @Query('perPage') perPage = 50, + @Query() pagination: GetSharedItemsDto, ) { const order = orderBy ? [orderBy.split(':') as [string, string]] : undefined; - return this.sharingUseCases.getSharedFoldersInWorkspace( + return this.workspaceUseCases.getSharedFoldersInWorkspace( user, workspaceId, - teamId, - page, - perPage, - order, + { + offset: pagination.page, + limit: pagination.perPage, + order, + }, ); } - @Get(':workspaceId/teams/:teamId/shared/:sharedFolderId/folders') + @Get([ + ':workspaceId/teams/:teamId/shared/:sharedFolderId/folders', + ':workspaceId/shared/:sharedFolderId/folders', + ]) @ApiOperation({ - summary: 'Get all folders inside a shared folder', + summary: 'Get folders inside a shared folder', }) @UseGuards(WorkspaceGuard) @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) - async getFoldersInsideSharedFolder( + async getFoldersInSharingFolder( @Param('workspaceId', ValidateUUIDPipe) workspaceId: WorkspaceAttributes['id'], - @Param('teamId', ValidateUUIDPipe) - teamId: WorkspaceTeam['id'], @UserDecorator() user: User, @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], - @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, + @Query() queryDto: GetSharedItemsDto, ) { const { orderBy, token, page, perPage } = queryDto; @@ -662,7 +654,6 @@ export class WorkspacesController { return this.workspaceUseCases.getItemsInSharedFolder( workspaceId, - teamId, user, sharedFolderId, WorkspaceItemType.Folder, @@ -671,20 +662,21 @@ export class WorkspacesController { ); } - @Get(':workspaceId/teams/:teamId/shared/:sharedFolderId/files') + @Get([ + ':workspaceId/teams/:teamId/shared/:sharedFolderId/files', + ':workspaceId/shared/:sharedFolderId/files', + ]) @ApiOperation({ summary: 'Get files inside a shared folder', }) @UseGuards(WorkspaceGuard) @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) - async getFilesInsideSharedFolder( + async getFilesInSharingFolder( @Param('workspaceId', ValidateUUIDPipe) workspaceId: WorkspaceAttributes['id'], - @Param('teamId', ValidateUUIDPipe) - teamId: WorkspaceTeam['id'], @UserDecorator() user: User, @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], - @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, + @Query() queryDto: GetSharedItemsDto, ) { const { orderBy, token, page, perPage } = queryDto; @@ -694,7 +686,6 @@ export class WorkspacesController { return this.workspaceUseCases.getItemsInSharedFolder( workspaceId, - teamId, user, sharedFolderId, WorkspaceItemType.File, @@ -703,6 +694,32 @@ export class WorkspacesController { ); } + @Get('/:workspaceId/shared/:itemType/:itemId/shared-with') + @ApiOperation({ + summary: 'Get users and teams an item is shared with', + }) + @ApiBearerAuth() + @ApiParam({ name: 'workspaceId', type: String, required: true }) + @ApiParam({ name: 'itemType', type: String, required: true }) + @ApiParam({ name: 'itemId', type: String, required: true }) + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getItemSharedWith( + @Param('workspaceId', ValidateUUIDPipe) + workspaceId: WorkspaceAttributes['id'], + @UserDecorator() user: User, + @Param() sharedWithParams: GetSharedWithDto, + ) { + const { itemId, itemType } = sharedWithParams; + + return this.workspaceUseCases.getItemSharedWith( + user, + workspaceId, + itemId, + itemType, + ); + } + @Post('/:workspaceId/folders') @ApiOperation({ summary: 'Create folder', diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 34e734a33..98d40718d 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -11,6 +11,7 @@ import { newFile, newFolder, newPreCreatedUser, + newRole, newSharing, newSharingRole, newUser, @@ -46,10 +47,14 @@ import { generateTokenWithPlainSecret, verifyWithDefaultSecret, } from '../../lib/jwt'; -import { Role } from '../sharing/sharing.domain'; +import { Role, SharedWithType } from '../sharing/sharing.domain'; import { WorkspaceAttributes } from './attributes/workspace.attributes'; import * as jwtUtils from '../../lib/jwt'; import { PaymentsService } from '../../externals/payments/payments.service'; +import { + FileWithSharedInfo, + FolderWithSharedInfo, +} from '../sharing/dto/get-items-and-shared-folders.dto'; jest.mock('../../middlewares/passport', () => { const originalModule = jest.requireActual('../../middlewares/passport'); @@ -2454,7 +2459,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2473,7 +2477,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2491,7 +2494,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2522,7 +2524,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2561,7 +2562,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2571,13 +2571,15 @@ describe('WorkspacesUsecases', () => { ).rejects.toThrow(ForbiddenException); }); - it('When team does not have access to the folder, then it should throw', async () => { + it('When user team does not have access to the folder, then it should throw', async () => { const folder = newFolder(); jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValue(folder); jest .spyOn(workspaceRepository, 'getItemBy') .mockResolvedValue(newWorkspaceItemUser()); - jest.spyOn(sharingUseCases, 'findSharingBy').mockResolvedValue(null); + jest + .spyOn(sharingUseCases, 'findSharingsBySharedWithAndAttributes') + .mockResolvedValue([]); (verifyWithDefaultSecret as jest.Mock).mockReturnValue({ sharedRootFolderId: v4(), @@ -2586,7 +2588,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2636,7 +2637,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2688,7 +2688,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2724,7 +2723,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2755,7 +2753,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, WorkspaceItemType.File, @@ -2821,7 +2818,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, WorkspaceItemType.File, @@ -4080,6 +4076,151 @@ describe('WorkspacesUsecases', () => { }); }); + describe('getWorkspaceTeamsUserBelongsTo', () => { + it('When user teams are fetched, then it should return teams', async () => { + const userUuid = v4(); + const workspaceId = v4(); + const teams = [ + newWorkspaceTeam({ workspaceId }), + newWorkspaceTeam({ workspaceId }), + ]; + + jest + .spyOn(teamRepository, 'getTeamsUserBelongsTo') + .mockResolvedValueOnce(teams); + + const result = await service.getTeamsUserBelongsTo( + userUuid, + workspaceId, + ); + + expect(teams).toBe(result); + }); + }); + + describe('getSharedFoldersInWorkspace', () => { + const mockUser = newUser(); + const mockWorkspace = newWorkspace({ owner: mockUser }); + const mockTeams = [newWorkspaceTeam({ workspaceId: mockWorkspace.id })]; + const mockFolder = newFolder({ owner: newUser() }); + const mockSharing = newSharing({ item: mockFolder }); + + it('When folders shared with user teams are fetched, then it returns successfully', async () => { + const mockFolderWithSharedInfo = { + ...mockFolder, + encryptionKey: mockSharing.encryptionKey, + dateShared: mockSharing.createdAt, + sharedWithMe: false, + sharingId: mockSharing.id, + sharingType: mockSharing.type, + credentials: { + networkPass: mockUser.userId, + networkUser: mockUser.bridgeUser, + }, + } as FolderWithSharedInfo; + + jest + .spyOn(service, 'getWorkspaceTeamsUserBelongsTo') + .mockResolvedValue(mockTeams); + jest + .spyOn(sharingUseCases, 'getSharedFoldersInWorkspaceByTeams') + .mockResolvedValue({ + folders: [mockFolderWithSharedInfo], + files: [], + credentials: { + networkPass: mockUser.userId, + networkUser: mockUser.bridgeUser, + }, + token: '', + role: 'OWNER', + }); + + const result = await service.getSharedFoldersInWorkspace( + mockUser, + mockWorkspace.id, + { + offset: 0, + limit: 10, + order: [['createdAt', 'DESC']], + }, + ); + + expect(service.getWorkspaceTeamsUserBelongsTo).toHaveBeenCalledWith( + mockUser.uuid, + mockWorkspace.id, + ); + expect(result.folders[0]).toMatchObject({ + plainName: mockFolder.plainName, + sharingId: mockSharing.id, + encryptionKey: mockSharing.encryptionKey, + dateShared: mockSharing.createdAt, + sharedWithMe: false, + }); + }); + }); + + describe('getSharedFilesInWorkspace', () => { + const mockUser = newUser(); + const mockWorkspace = newWorkspace({ owner: mockUser }); + const mockTeams = [newWorkspaceTeam({ workspaceId: mockWorkspace.id })]; + const mockFile = newFile({ owner: newUser() }); + const mockSharing = newSharing({ item: mockFile }); + + it('When files shared with user teams are fetched, then it returns successfully', async () => { + const mockFileWithSharedInfo = { + ...mockFile, + encryptionKey: mockSharing.encryptionKey, + dateShared: mockSharing.createdAt, + sharedWithMe: false, + sharingId: mockSharing.id, + sharingType: mockSharing.type, + credentials: { + networkPass: mockUser.userId, + networkUser: mockUser.bridgeUser, + }, + } as FileWithSharedInfo; + + jest + .spyOn(service, 'getWorkspaceTeamsUserBelongsTo') + .mockResolvedValue(mockTeams); + jest + .spyOn(sharingUseCases, 'getSharedFilesInWorkspaceByTeams') + .mockResolvedValue({ + files: [mockFileWithSharedInfo], + folders: [], + credentials: { + networkPass: mockUser.userId, + networkUser: mockUser.bridgeUser, + }, + token: '', + role: 'OWNER', + }); + + const result = await service.getSharedFilesInWorkspace( + mockUser, + mockWorkspace.id, + { + offset: 0, + limit: 10, + order: [['createdAt', 'DESC']], + }, + ); + + expect(service.getWorkspaceTeamsUserBelongsTo).toHaveBeenCalledWith( + mockUser.uuid, + mockWorkspace.id, + ); + + expect(result.files[0]).toMatchObject({ + name: mockFile.name, + sharingId: mockSharing.id, + encryptionKey: mockSharing.encryptionKey, + dateShared: mockSharing.createdAt, + sharedWithMe: false, + }); + }); + }); + describe('getWorkspaceTeams', () => { it('When workspace is not found, then fail', async () => { const user = newUser(); @@ -4807,4 +4948,193 @@ describe('WorkspacesUsecases', () => { }); }); }); + + describe('getItemSharedWith', () => { + const user = newUser(); + const workspace = newWorkspace({ owner: user }); + const itemId = v4(); + const itemType = WorkspaceItemType.File; + + it('When item is not found in workspace, then it should throw', async () => { + jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(null); + jest.spyOn(workspaceRepository, 'getItemBy').mockResolvedValueOnce(null); + + await expect( + service.getItemSharedWith(user, workspace.id, itemId, itemType), + ).rejects.toThrow(NotFoundException); + }); + + it('When item is not being shared, then it should throw', async () => { + const mockFile = newFile({ owner: user }); + const mockWorkspaceFile = newWorkspaceItemUser({ + itemId: mockFile.uuid, + itemType: WorkspaceItemType.File, + }); + jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(mockFile); + jest + .spyOn(workspaceRepository, 'getItemBy') + .mockResolvedValueOnce(mockWorkspaceFile); + jest + .spyOn(sharingUseCases, 'findSharingsWithRolesByItem') + .mockResolvedValueOnce([]); + + await expect( + service.getItemSharedWith(user, workspace.id, itemId, itemType), + ).rejects.toThrow(BadRequestException); + }); + + it('When user is not the owner, invited or part of shared team, then it should throw', async () => { + const mockFile = newFile({ owner: newUser() }); + const mockWorkspaceFile = newWorkspaceItemUser({ + itemId: mockFile.uuid, + itemType: WorkspaceItemType.File, + }); + const mockRole = newRole(); + const mockSharing = newSharing({ + item: mockFile, + }); + + jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(mockFile); + jest + .spyOn(workspaceRepository, 'getItemBy') + .mockResolvedValueOnce(mockWorkspaceFile); + jest + .spyOn(sharingUseCases, 'findSharingsWithRolesByItem') + .mockResolvedValueOnce([{ ...mockSharing, role: mockRole }]); + jest + .spyOn(service, 'getWorkspaceTeamsUserBelongsTo') + .mockResolvedValueOnce([]); + + await expect( + service.getItemSharedWith(user, workspace.id, itemId, itemType), + ).rejects.toThrow(ForbiddenException); + }); + + it('When user is owner, then it should return shared info', async () => { + const file = newFile(); + const workspaceFile = newWorkspaceItemUser({ + itemId: file.uuid, + itemType: WorkspaceItemType.File, + attributes: { createdBy: user.uuid }, + }); + const role = newRole(); + const sharing = newSharing({ + item: file, + }); + + jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(file); + jest + .spyOn(workspaceRepository, 'getItemBy') + .mockResolvedValueOnce(workspaceFile); + jest + .spyOn(sharingUseCases, 'findSharingsWithRolesByItem') + .mockResolvedValueOnce([{ ...sharing, role }]); + jest + .spyOn(service, 'getWorkspaceTeamsUserBelongsTo') + .mockResolvedValueOnce([]); + jest.spyOn(userUsecases, 'findByUuids').mockResolvedValueOnce([user]); + jest + .spyOn(userUsecases, 'getAvatarUrl') + .mockResolvedValueOnce('avatar-url'); + + const result = await service.getItemSharedWith( + user, + workspace.id, + itemId, + itemType, + ); + + expect(result.usersWithRoles[1]).toMatchObject({ + uuid: user.uuid, + role: { + id: 'NONE', + name: 'OWNER', + createdAt: file.createdAt, + updatedAt: file.createdAt, + }, + }); + }); + + it('When user is invited, then it should return shared info', async () => { + const invitedUser = newUser(); + const file = newFile(); + const workspaceFile = newWorkspaceItemUser({ + itemId: file.uuid, + itemType: WorkspaceItemType.File, + }); + const role = newRole(); + const sharing = newSharing({ + item: file, + }); + sharing.sharedWith = invitedUser.uuid; + + jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(file); + jest + .spyOn(workspaceRepository, 'getItemBy') + .mockResolvedValueOnce(workspaceFile); + jest + .spyOn(sharingUseCases, 'findSharingsWithRolesByItem') + .mockResolvedValueOnce([{ ...sharing, role }]); + jest + .spyOn(service, 'getWorkspaceTeamsUserBelongsTo') + .mockResolvedValueOnce([]); + jest + .spyOn(userUsecases, 'findByUuids') + .mockResolvedValueOnce([invitedUser, user]); + jest + .spyOn(userUsecases, 'getAvatarUrl') + .mockResolvedValueOnce('avatar-url'); + + const result = await service.getItemSharedWith( + invitedUser, + workspace.id, + itemId, + itemType, + ); + + expect(result.usersWithRoles[0]).toMatchObject({ + sharingId: sharing.id, + role: role, + }); + }); + + it('When user belongs to a shared team, then it should return shared info', async () => { + const userAsignedToTeam = newUser(); + const invitedTeam = newWorkspaceTeam(); + const file = newFile(); + const workspaceFile = newWorkspaceItemUser({ + itemId: file.uuid, + itemType: WorkspaceItemType.File, + }); + const role = newRole(); + const sharing = newSharing({ + item: file, + }); + sharing.sharedWith = invitedTeam.id; + sharing.sharedWithType = SharedWithType.WorkspaceTeam; + + jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(file); + jest + .spyOn(workspaceRepository, 'getItemBy') + .mockResolvedValueOnce(workspaceFile); + jest + .spyOn(sharingUseCases, 'findSharingsWithRolesByItem') + .mockResolvedValueOnce([{ ...sharing, role }]); + jest + .spyOn(service, 'getWorkspaceTeamsUserBelongsTo') + .mockResolvedValueOnce([invitedTeam]); + + const result = await service.getItemSharedWith( + userAsignedToTeam, + workspace.id, + itemId, + itemType, + ); + + expect(result.teamsWithRoles[0]).toMatchObject({ + sharingId: sharing.id, + role: role, + }); + }); + }); }); diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index d465a1a94..a2ab6b6f4 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -62,10 +62,10 @@ import { verifyWithDefaultSecret, } from '../../lib/jwt'; import { WorkspaceItemUser } from './domains/workspace-item-user.domain'; -import { SharingService } from '../sharing/sharing.service'; +import { SharingInfo, SharingService } from '../sharing/sharing.service'; import { ChangeUserAssignedSpaceDto } from './dto/change-user-assigned-space.dto'; import { PaymentsService } from '../../externals/payments/payments.service'; -import { CryptoService } from '../../externals/crypto/crypto.service'; +import { SharingAccessTokenData } from '../sharing/guards/sharings-token.interface'; @Injectable() export class WorkspacesUsecases { @@ -74,14 +74,13 @@ export class WorkspacesUsecases { private readonly workspaceRepository: SequelizeWorkspaceRepository, private readonly sharingUseCases: SharingService, private readonly paymentService: PaymentsService, - private networkService: BridgeService, - private cryptoService: CryptoService, - private userRepository: SequelizeUserRepository, - private userUsecases: UserUseCases, - private configService: ConfigService, - private mailerService: MailerService, - private fileUseCases: FileUseCases, - private folderUseCases: FolderUseCases, + private readonly networkService: BridgeService, + private readonly userRepository: SequelizeUserRepository, + private readonly userUsecases: UserUseCases, + private readonly configService: ConfigService, + private readonly mailerService: MailerService, + private readonly fileUseCases: FileUseCases, + private readonly folderUseCases: FolderUseCases, private readonly avatarService: AvatarService, ) {} @@ -188,6 +187,18 @@ export class WorkspacesUsecases { return workspace.toJSON(); } + async getWorkspaceTeamsUserBelongsTo( + memberId: WorkspaceTeamUser['memberId'], + workspaceId: Workspace['id'], + ) { + const teams = await this.teamRepository.getTeamsUserBelongsTo( + memberId, + workspaceId, + ); + + return teams; + } + async setupWorkspace( user: User, workspaceId: WorkspaceAttributes['id'], @@ -990,16 +1001,183 @@ export class WorkspacesUsecases { return createdSharing; } + async getItemSharedWith( + user: User, + workspaceId: string, + itemId: Sharing['itemId'], + itemType: WorkspaceItemType, + ) { + const [item, itemInWorkspace] = await Promise.all([ + itemType === WorkspaceItemType.File + ? this.fileUseCases.getByUuid(itemId) + : this.folderUseCases.getByUuid(itemId), + this.workspaceRepository.getItemBy({ + itemId, + itemType, + }), + ]); + + if (!itemInWorkspace || !item) { + throw new NotFoundException('Item not found'); + } + + const sharingsWithRoles = + await this.sharingUseCases.findSharingsWithRolesByItem(item); + + if (!sharingsWithRoles.length) { + throw new BadRequestException( + 'This item is not being shared with anyone', + ); + } + + const sharedWithIndividuals = sharingsWithRoles.filter( + (s) => s.sharedWithType === SharedWithType.Individual, + ); + + const sharedWithTeams = sharingsWithRoles.filter( + (s) => s.sharedWithType === SharedWithType.WorkspaceTeam, + ); + + const [teams, users] = await Promise.all([ + this.getWorkspaceTeamsUserBelongsTo(user.uuid, workspaceId), + this.userUsecases.findByUuids( + sharedWithIndividuals.map((s) => s.sharedWith), + ), + ]); + + const teamsIds = teams.map((team) => team.id); + + const isAnInvitedUser = sharedWithIndividuals.some( + (s) => s.sharedWith === user.uuid, + ); + const isTheOwner = itemInWorkspace.isOwnedBy(user); + const belongsToSharedTeam = sharedWithTeams.some((s) => + teamsIds.includes(s.sharedWith), + ); + + if (!isTheOwner && !isAnInvitedUser && !belongsToSharedTeam) { + throw new ForbiddenException(); + } + + const usersWithRoles = await Promise.all( + sharedWithIndividuals.map(async (sharingWithRole) => { + const user = users.find( + (user) => + user.uuid === sharingWithRole.sharedWith && + sharingWithRole.sharedWithType == SharedWithType.Individual, + ); + + return { + ...user, + sharingId: sharingWithRole.id, + avatar: user?.avatar + ? await this.userUsecases.getAvatarUrl(user.avatar) + : null, + role: sharingWithRole.role, + }; + }), + ); + + const { createdBy } = itemInWorkspace; + + const { name, lastname, email, avatar, uuid } = + createdBy === user.uuid + ? user + : await this.userUsecases.getUser(createdBy); + + const ownerWithRole: SharingInfo = { + name, + lastname, + email, + sharingId: null, + avatar: avatar ? await this.userUsecases.getAvatarUrl(avatar) : null, + uuid, + role: { + id: 'NONE', + name: 'OWNER', + createdAt: item.createdAt, + updatedAt: item.createdAt, + }, + }; + + usersWithRoles.push(ownerWithRole); + + const workspaceTeams = + await this.teamRepository.getTeamsAndMembersCountByWorkspace(workspaceId); + + const teamsWithRoles = sharedWithTeams.map((sharingWithRole) => { + const team = workspaceTeams.find( + (team) => + team.team.id === sharingWithRole.sharedWith && + sharingWithRole.sharedWithType == SharedWithType.WorkspaceTeam, + ); + + return { + ...team.team, + membersCount: team.membersCount, + sharingId: sharingWithRole.id, + role: sharingWithRole.role, + }; + }); + + return { usersWithRoles, teamsWithRoles }; + } + + async getSharedFilesInWorkspace( + user: User, + workspaceId: Workspace['id'], + options: { offset: number; limit: number; order?: [string, string][] }, + ) { + const teams = await this.getWorkspaceTeamsUserBelongsTo( + user.uuid, + workspaceId, + ); + + const teamsIds = teams.map((team) => team.id); + + const response = + await this.sharingUseCases.getSharedFilesInWorkspaceByTeams( + user, + workspaceId, + teamsIds, + options, + ); + + return { ...response, token: '' }; + } + + async getSharedFoldersInWorkspace( + user: User, + workspaceId: Workspace['id'], + options: { offset: number; limit: number; order?: [string, string][] }, + ) { + const teams = await this.getWorkspaceTeamsUserBelongsTo( + user.uuid, + workspaceId, + ); + + const teamsIds = teams.map((team) => team.id); + + const response = + await this.sharingUseCases.getSharedFoldersInWorkspaceByTeams( + user, + workspaceId, + teamsIds, + options, + ); + + return { ...response, token: '' }; + } + async getItemsInSharedFolder( workspaceId: Workspace['id'], - teamId: WorkspaceTeam['id'], user: User, folderUuid: Folder['uuid'], itemsType: WorkspaceItemType, token: string | null, options?: { page: number; perPage: number; order: string[][] }, ) { - const getFolderContentByCreatedBy = async ( + const getFoldersFromFolder = async ( createdBy: User['uuid'], folderUuid: Folder['uuid'], ) => { @@ -1058,16 +1236,6 @@ export class WorkspacesUsecases { return files; }; - const folder = await this.folderUseCases.getByUuid(folderUuid); - - if (folder.isTrashed()) { - throw new BadRequestException('This folder is trashed'); - } - - if (folder.isRemoved()) { - throw new BadRequestException('This folder is removed'); - } - const itemFolder = await this.workspaceRepository.getItemBy({ itemId: folderUuid, itemType: WorkspaceItemType.Folder, @@ -1078,109 +1246,117 @@ export class WorkspacesUsecases { throw new NotFoundException('Item not found in workspace'); } - const parentFolder = folder.parentUuid - ? await this.folderUseCases.getByUuid(folder.parentUuid) - : null; + const currentFolder = await this.folderUseCases.getByUuid(folderUuid); + + if (currentFolder.isTrashed()) { + throw new BadRequestException('This folder is trashed'); + } + + if (currentFolder.isRemoved()) { + throw new BadRequestException('This folder is removed'); + } + + const parentFolder = + currentFolder.parentUuid && + (await this.folderUseCases.getByUuid(currentFolder.parentUuid)); if (itemFolder.isOwnedBy(user)) { + const getItemsFromFolder = + itemsType === WorkspaceItemType.Folder + ? getFoldersFromFolder + : getFilesFromFolder; + + const itemsInFolder = await getItemsFromFolder( + itemFolder.createdBy, + currentFolder.uuid, + ); + return { - items: - itemsType === WorkspaceItemType.Folder - ? await getFolderContentByCreatedBy( - itemFolder.createdBy, - folder.uuid, - ) - : await getFilesFromFolder(itemFolder.createdBy, folder.uuid), - name: folder.plainName, + items: itemsInFolder, + name: currentFolder.plainName, bucket: '', encryptionKey: null, token: '', parent: { - uuid: parentFolder?.uuid || null, - name: parentFolder?.plainName || null, + uuid: parentFolder?.uuid ?? null, + name: parentFolder?.plainName ?? null, }, role: 'OWNER', }; } - const requestedFolderIsSharedRootFolder = !token; + const isSharedRootFolderRequest = !token; - const decoded = requestedFolderIsSharedRootFolder + const decodedAccessToken = isSharedRootFolderRequest ? null - : (verifyWithDefaultSecret(token) as - | { - sharedRootFolderId: Folder['uuid']; - sharedWithType: SharedWithType; - parentFolderId: Folder['parent']['uuid']; - folder: { - uuid: Folder['uuid']; - id: Folder['id']; - }; - workspace: { - workspaceId: Workspace['id']; - teamId: WorkspaceTeam['id']; - }; - owner: { - id: User['id']; - uuid: User['uuid']; - }; - } - | string); - - if (typeof decoded === 'string') { + : (verifyWithDefaultSecret(token) as SharingAccessTokenData); + + if (typeof decodedAccessToken === 'string') { throw new ForbiddenException('Invalid token'); } - const sharing = await this.sharingUseCases.findSharingBy({ - sharedWith: teamId, - itemId: requestedFolderIsSharedRootFolder - ? folderUuid - : decoded.sharedRootFolderId, - sharedWithType: SharedWithType.WorkspaceTeam, - }); + const teamsUserBelongsTo = await this.teamRepository.getTeamsUserBelongsTo( + user.uuid, + workspaceId, + ); + + const teamIds = teamsUserBelongsTo.map((team) => team.id); + + const itemSharedWithTeam = + await this.sharingUseCases.findSharingsBySharedWithAndAttributes( + teamIds, + { + sharedWithType: SharedWithType.WorkspaceTeam, + itemId: isSharedRootFolderRequest + ? folderUuid + : decodedAccessToken.sharedRootFolderId, + }, + { limit: 1, offset: 0, givePriorityToRole: 'EDITOR' }, + ); + + const sharing = itemSharedWithTeam[0]; if (!sharing) { throw new ForbiddenException('Team does not have access to this folder'); } - if (!requestedFolderIsSharedRootFolder) { - const navigationUp = folder.uuid === decoded.parentFolderId; - const navigationDown = folder.parentId === decoded.folder.id; - const navigationUpFromSharedFolder = - navigationUp && decoded.sharedRootFolderId === decoded.folder.uuid; + if (!isSharedRootFolderRequest) { + const { + folder: sourceFolder, + parentFolderId: sourceParentFolderId, + sharedRootFolderId, + } = decodedAccessToken; - if (navigationUpFromSharedFolder) { - throw new ForbiddenException( - 'Team does not have access to this folder', - ); - } + const navigationUp = currentFolder.uuid === sourceParentFolderId; + const navigationDown = currentFolder.parentId === sourceFolder.id; + const navigationUpFromSharedFolder = + navigationUp && sharedRootFolderId === sourceFolder.uuid; - if (!navigationDown && !navigationUp) { + if (navigationUpFromSharedFolder || (!navigationDown && !navigationUp)) { throw new ForbiddenException( 'Team does not have access to this folder', ); } } - const workspace = await this.workspaceRepository.findById(workspaceId); + const { workspaceUser, workspace } = + await this.workspaceRepository.findWorkspaceAndDefaultUser(workspaceId); - const workspaceUser = await this.userUsecases.getUser( - workspace.workspaceUserId, + const [ownerRootFolder, folderItems, sharingAccessRole] = await Promise.all( + [ + this.folderUseCases.getFolderByUserId( + workspaceUser.rootFolderId, + workspaceUser.id, + ), + itemsType === WorkspaceItemType.Folder + ? await getFoldersFromFolder(itemFolder.createdBy, currentFolder.uuid) + : await getFilesFromFolder(itemFolder.createdBy, currentFolder.uuid), + this.sharingUseCases.findSharingRoleBy({ sharingId: sharing.id }), + ], ); - const [ownerRootFolder, items, sharingRole] = await Promise.all([ - this.folderUseCases.getFolderByUserId( - workspaceUser.rootFolderId, - workspaceUser.id, - ), - itemsType === WorkspaceItemType.Folder - ? await getFolderContentByCreatedBy(itemFolder.createdBy, folder.uuid) - : await getFilesFromFolder(itemFolder.createdBy, folder.uuid), - this.sharingUseCases.findSharingRoleBy({ sharingId: sharing.id }), - ]); - return { - items, + items: folderItems, credentials: { networkPass: workspaceUser.userId, networkUser: workspaceUser.bridgeUser, @@ -1191,12 +1367,11 @@ export class WorkspacesUsecases { sharedWithType: sharing.sharedWithType, parentFolderId: parentFolder?.uuid || null, folder: { - uuid: folder.uuid, - id: folder.id, + uuid: currentFolder.uuid, + id: currentFolder.id, }, workspace: { workspaceId: workspace.id, - teamId, }, owner: { uuid: itemFolder.createdBy, @@ -1210,8 +1385,8 @@ export class WorkspacesUsecases { uuid: parentFolder?.uuid || null, name: parentFolder?.plainName || null, }, - name: folder.plainName, - role: sharingRole.role.name, + name: currentFolder.plainName, + role: sharingAccessRole.role.name, }; } diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index 47a284953..2677738e3 100644 --- a/test/fixtures.spec.ts +++ b/test/fixtures.spec.ts @@ -565,4 +565,21 @@ describe('Testing fixtures tests', () => { expect(user.username).toBe(user.email); }); }); + + describe("Role's fixture", () => { + it('When it generates a role, then the name should be random', () => { + const role = fixtures.newRole(); + const otherRole = fixtures.newRole(); + + expect(role.name).toBeTruthy(); + expect(role.name).not.toBe(otherRole.name); + }); + + it('When it generates a role and a name is provided, then that name should be set', () => { + const customName = 'CustomRoleName'; + const role = fixtures.newRole(customName); + + expect(role.name).toBe(customName); + }); + }); }); diff --git a/test/fixtures.ts b/test/fixtures.ts index 57846cb6b..f97023fb8 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -5,6 +5,7 @@ import { Folder } from '../src/modules/folder/folder.domain'; import { User } from '../src/modules/user/user.domain'; import { Permission, + Role, Sharing, SharingActionName, SharingRole, @@ -271,6 +272,15 @@ export const newSharingRole = (bindTo?: { }); }; +export const newRole = (name?: string): Role => { + return Role.build({ + id: v4(), + name: name ?? randomDataGenerator.string(), + createdAt: randomDataGenerator.date(), + updatedAt: randomDataGenerator.date(), + }); +}; + export const newMailLimit = (bindTo?: { userId?: number; mailType?: MailTypes;