From aec037f8f415cd99ac4a5af9cddd0d2e9274da55 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Mon, 12 Aug 2024 03:38:09 -0400 Subject: [PATCH 01/10] feat: add support for sharings and multiple teams --- src/modules/sharing/sharing.repository.ts | 165 ++++++++++++++++++ src/modules/sharing/sharing.service.ts | 135 +++++++++++++- .../repositories/workspaces.repository.ts | 25 +++ .../workspaces/workspaces.controller.ts | 114 +++++++++++- src/modules/workspaces/workspaces.usecase.ts | 159 +++++++++++++---- 5 files changed, 561 insertions(+), 37 deletions(-) diff --git a/src/modules/sharing/sharing.repository.ts b/src/modules/sharing/sharing.repository.ts index 4de88afd..5389f541 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -453,6 +453,100 @@ export class SequelizeSharingRepository implements SharingRepository { }); } + async findSharingsBySharedWithAndAttributes( + sharedWithValues: SharingAttributes['sharedWith'][], + filters: Omit, 'sharedWith'> = {}, + options?: { offset: number; limit: number }, + ): Promise { + const where: WhereOptions = { + ...filters, + sharedWith: { + [Op.in]: sharedWithValues, + }, + }; + const sharings = await this.sharings.findAll({ + where, + limit: options.limit, + offset: options.offset, + }); + + return sharings.map((sharing) => + Sharing.build(sharing.get({ plain: true })), + ); + } + + async findFilesSharedInWorkspaceByOwnerAndTeams( + ownerId: WorkspaceItemUserAttributes['createdBy'], + workspaceId: WorkspaceAttributes['id'], + teamIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; orderBy?: [string, string][] }, + ): Promise { + const sharedFiles = await this.sharings.findAll({ + where: { + [Op.or]: [ + { + sharedWith: { [Op.in]: teamIds }, + sharedWithType: SharedWithType.WorkspaceTeam, + }, + { + '$file->workspaceUser.created_by$': ownerId, + }, + ], + }, + attributes: [ + [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], + ], + group: [ + 'file.id', + 'file->workspaceUser.id', + 'file->workspaceUser->creator.id', + 'SharingModel.item_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: options.orderBy, + limit: options.limit, + offset: options.offset, + }); + + return sharedFiles.map((shared) => { + const sharing = shared.get({ plain: true }); + const user = sharing.file.workspaceUser?.creator; + delete sharing.file.user; + + return Sharing.build({ + ...sharing, + file: File.build({ + ...sharing.file, + user: user ? User.build(user) : null, + }), + }); + }); + } + async findFilesByOwnerAndSharedWithTeamInworkspace( workspaceId: WorkspaceAttributes['id'], teamId: WorkspaceTeamAttributes['id'], @@ -600,6 +694,77 @@ export class SequelizeSharingRepository implements SharingRepository { }); } + async findFoldersSharedInWorkspaceByOwnerAndTeams( + ownerId: WorkspaceItemUserAttributes['createdBy'], + workspaceId: WorkspaceAttributes['id'], + teamsIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; orderBy?: [string, string][] }, + ): Promise { + const sharedFolders = await this.sharings.findAll({ + where: { + [Op.or]: [ + { + sharedWith: { [Op.in]: teamsIds }, + sharedWithType: SharedWithType.WorkspaceTeam, + }, + { + '$folder->workspaceUser.created_by$': ownerId, + }, + ], + }, + attributes: [ + [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], + ], + group: [ + 'folder.id', + 'folder->workspaceUser.id', + 'folder->workspaceUser->creator.id', + 'SharingModel.item_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: options.orderBy, + limit: options.limit, + offset: options.offset, + }); + + return sharedFolders.map((shared) => { + const sharing = shared.get({ plain: true }); + const user = sharing.folder.workspaceUser?.creator; + + return Sharing.build({ + ...sharing, + folder: Folder.build({ + ...sharing.folder, + user: user ? User.build(user) : null, + }), + }); + }); + } + private toDomain(model: SharingModel): Sharing { const folder = model.folder.get({ plain: true }); const user = model.folder.user.get({ plain: true }); diff --git a/src/modules/sharing/sharing.service.ts b/src/modules/sharing/sharing.service.ts index fca810cf..03a5fd02 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -34,6 +34,7 @@ import { UpdateSharingRoleDto } from './dto/update-sharing-role.dto'; import getEnv from '../../config/configuration'; import { generateTokenWithPlainSecret, + generateWithDefaultSecret, verifyWithDefaultSecret, } from '../../lib/jwt'; import { @@ -53,6 +54,8 @@ 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 { WorkspacesUsecases } from '../workspaces/workspaces.usecase'; +import { WorkspaceTeamAttributes } from '../workspaces/attributes/workspace-team.attributes'; export class InvalidOwnerError extends Error { constructor() { @@ -216,6 +219,18 @@ export class SharingService { return this.sharingRepository.findOneSharingBy(where); } + findSharingsBySharedWithAndAttributes( + sharedWithValues: Sharing['sharedWith'][], + filters: Omit, 'sharedWith'> = {}, + options?: { offset: number; limit: number }, + ): Promise { + return this.sharingRepository.findSharingsBySharedWithAndAttributes( + sharedWithValues, + filters, + options, + ); + } + findSharingRoleBy(where: Partial) { return this.sharingRepository.findSharingRoleBy(where); } @@ -1825,6 +1840,110 @@ export class SharingService { }), )) as FolderWithSharedInfo[]; + const sharedRootToken = generateWithDefaultSecret( + { + isRootToken: true, + workspace: { + workspaceId, + }, + owner: { + uuid: user.uuid, + }, + }, + '1d', + ); + + return { + folders: folders, + files: [], + credentials: { + networkPass: user.userId, + networkUser: user.bridgeUser, + }, + token: sharedRootToken, + role: 'OWNER', + }; + } + + async getSharedFilesInWorkspaceByTeams( + user: User, + workspaceId: Workspace['id'], + teamIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; orderBy?: [string, string][] }, + ): Promise { + const filesWithSharedInfo = + await this.sharingRepository.findFilesSharedInWorkspaceByOwnerAndTeams( + user.uuid, + workspaceId, + teamIds, + options, + ); + + const files = (await Promise.all( + filesWithSharedInfo.map(async (fileWithSharedInfo) => { + const avatar = fileWithSharedInfo.file?.user?.avatar; + return { + ...fileWithSharedInfo.file, + plainName: fileWithSharedInfo.file.plainName, + sharingId: fileWithSharedInfo.id, + encryptionKey: fileWithSharedInfo.encryptionKey, + dateShared: fileWithSharedInfo.createdAt, + user: { + ...fileWithSharedInfo.file.user, + avatar: avatar + ? await this.usersUsecases.getAvatarUrl(avatar) + : null, + }, + }; + }), + )) as FileWithSharedInfo[]; + + return { + folders: [], + files: files, + credentials: { + networkPass: user.userId, + networkUser: user.bridgeUser, + }, + token: '', + role: 'OWNER', + }; + } + + async getSharedFoldersInWorkspaceByTeams( + user: User, + workspaceId: Workspace['id'], + teamIds: WorkspaceTeamAttributes['id'][], + options: { offset: number; limit: number; orderBy?: [string, string][] }, + ): Promise { + const foldersWithSharedInfo = + await this.sharingRepository.findFoldersSharedInWorkspaceByOwnerAndTeams( + user.uuid, + workspaceId, + teamIds, + options, + ); + + const folders = (await Promise.all( + foldersWithSharedInfo.map(async (folderWithSharedInfo) => { + const avatar = folderWithSharedInfo.folder?.user?.avatar; + return { + ...folderWithSharedInfo.folder, + plainName: folderWithSharedInfo.folder.plainName, + sharingId: folderWithSharedInfo.id, + encryptionKey: folderWithSharedInfo.encryptionKey, + dateShared: folderWithSharedInfo.createdAt, + sharedWithMe: user.uuid !== folderWithSharedInfo.folder.user.uuid, + user: { + ...folderWithSharedInfo.folder.user, + avatar: avatar + ? await this.usersUsecases.getAvatarUrl(avatar) + : null, + }, + }; + }), + )) as FolderWithSharedInfo[]; + return { folders: folders, files: [], @@ -1876,6 +1995,20 @@ export class SharingService { }), )) as FileWithSharedInfo[]; + const sharedRootToken = generateWithDefaultSecret( + { + isRootToken: true, + workspace: { + workspaceId, + }, + owner: { + id: user.id, + uuid: user.uuid, + }, + }, + '1d', + ); + return { folders: [], files: files, @@ -1883,7 +2016,7 @@ export class SharingService { networkPass: user.userId, networkUser: user.bridgeUser, }, - token: '', + token: sharedRootToken, role: 'OWNER', }; } diff --git a/src/modules/workspaces/repositories/workspaces.repository.ts b/src/modules/workspaces/repositories/workspaces.repository.ts index 1fe7679c..3d3d9b9a 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.ts @@ -40,6 +40,31 @@ export class SequelizeWorkspaceRepository { 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.ts b/src/modules/workspaces/workspaces.controller.ts index 9871ef5e..25b399dd 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -579,6 +579,118 @@ export class WorkspacesController { ); } + @Get(':workspaceId/shared/files') + @ApiOperation({ + summary: 'Get shared files in teams', + }) + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getSharedFilesInWorkspace( + @Param('workspaceId', ValidateUUIDPipe) + workspaceId: WorkspaceTeamAttributes['id'], + @UserDecorator() user: User, + @Query('orderBy') orderBy: OrderBy, + @Query('page') page = 0, + @Query('perPage') perPage = 50, + ) { + const order = orderBy + ? [orderBy.split(':') as [string, string]] + : undefined; + + return this.workspaceUseCases.getSharedFilesInWorkspace(user, workspaceId, { + offset: page, + limit: perPage, + orderBy: order, + }); + } + + @Get(':workspaceId/shared/folders') + @ApiOperation({ + summary: 'Get shared files in teams', + }) + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getSharedFoldersInWorkspace( + @Param('workspaceId', ValidateUUIDPipe) + workspaceId: WorkspaceTeamAttributes['id'], + @UserDecorator() user: User, + @Query('orderBy') orderBy: OrderBy, + @Query('page') page = 0, + @Query('perPage') perPage = 50, + ) { + const order = orderBy + ? [orderBy.split(':') as [string, string]] + : undefined; + + return this.workspaceUseCases.getSharedFoldersInWorkspace( + user, + workspaceId, + { + offset: page, + limit: perPage, + orderBy: order, + }, + ); + } + + @Get(':workspaceId/shared/:sharedFolderId/folders') + @ApiOperation({ + summary: 'Get all folders inside a shared folder', + }) + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getFoldersInSharingFolder( + @Param('workspaceId', ValidateUUIDPipe) + workspaceId: WorkspaceAttributes['id'], + @UserDecorator() user: User, + @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], + @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, + ) { + const { orderBy, token, page, perPage } = queryDto; + + const order = orderBy + ? [orderBy.split(':') as [string, string]] + : undefined; + + return this.workspaceUseCases.getItemsInSharedFolder( + workspaceId, + user, + sharedFolderId, + WorkspaceItemType.Folder, + token, + { page, perPage, order }, + ); + } + + @Get(':workspaceId/shared/:sharedFolderId/files') + @ApiOperation({ + summary: 'Get files inside a shared folder', + }) + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) + async getFilesInSharingFolder( + @Param('workspaceId', ValidateUUIDPipe) + workspaceId: WorkspaceAttributes['id'], + @UserDecorator() user: User, + @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], + @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, + ) { + const { orderBy, token, page, perPage } = queryDto; + + const order = orderBy + ? [orderBy.split(':') as [string, string]] + : undefined; + + return this.workspaceUseCases.getItemsInSharedFolder( + workspaceId, + user, + sharedFolderId, + WorkspaceItemType.File, + token, + { page, perPage, order }, + ); + } + @Get(':workspaceId/teams/:teamId/shared/files') @ApiOperation({ summary: 'Get shared files with a team', @@ -662,7 +774,6 @@ export class WorkspacesController { return this.workspaceUseCases.getItemsInSharedFolder( workspaceId, - teamId, user, sharedFolderId, WorkspaceItemType.Folder, @@ -694,7 +805,6 @@ export class WorkspacesController { return this.workspaceUseCases.getItemsInSharedFolder( workspaceId, - teamId, user, sharedFolderId, WorkspaceItemType.File, diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index d465a1a9..667f4bd0 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -188,6 +188,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'], @@ -989,17 +1001,89 @@ export class WorkspacesUsecases { return createdSharing; } + async getSharedFilesInWorkspace( + user: User, + workspaceId: Workspace['id'], + options: { offset: number; limit: number; orderBy?: [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, + ); + + const sharedRootToken = generateWithDefaultSecret( + { + isRootToken: true, + workspace: { + workspaceId, + }, + owner: { + id: user.id, + uuid: user.uuid, + }, + }, + '1d', + ); + + return { ...response, token: sharedRootToken }; + } + + async getSharedFoldersInWorkspace( + user: User, + workspaceId: Workspace['id'], + options: { offset: number; limit: number; orderBy?: [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, + ); + + const sharedRootToken = generateWithDefaultSecret( + { + isRootToken: true, + workspace: { + workspaceId, + }, + owner: { + id: user.id, + uuid: user.uuid, + }, + }, + '1d', + ); + + return { ...response, token: sharedRootToken }; + } 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 +1142,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,19 +1152,28 @@ export class WorkspacesUsecases { throw new NotFoundException('Item not found in workspace'); } + 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 parentFolder = folder.parentUuid ? await this.folderUseCases.getByUuid(folder.parentUuid) : null; if (itemFolder.isOwnedBy(user)) { + const itemsInFolder = + itemsType === WorkspaceItemType.Folder + ? await getFoldersFromFolder(itemFolder.createdBy, folder.uuid) + : await getFilesFromFolder(itemFolder.createdBy, folder.uuid); + return { - items: - itemsType === WorkspaceItemType.Folder - ? await getFolderContentByCreatedBy( - itemFolder.createdBy, - folder.uuid, - ) - : await getFilesFromFolder(itemFolder.createdBy, folder.uuid), + items: itemsInFolder, name: folder.plainName, bucket: '', encryptionKey: null, @@ -1118,7 +1201,6 @@ export class WorkspacesUsecases { }; workspace: { workspaceId: Workspace['id']; - teamId: WorkspaceTeam['id']; }; owner: { id: User['id']; @@ -1131,13 +1213,26 @@ export class WorkspacesUsecases { 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 teamsIds = teamsUserBelongsTo.map((team) => team.id); + + const itemSharedWithTeam = + await this.sharingUseCases.findSharingsBySharedWithAndAttributes( + teamsIds, + { + sharedWithType: SharedWithType.WorkspaceTeam, + itemId: requestedFolderIsSharedRootFolder + ? folderUuid + : decoded.sharedRootFolderId, + }, + { limit: 1, offset: 0 }, + ); + + const sharing = itemSharedWithTeam[0]; if (!sharing) { throw new ForbiddenException('Team does not have access to this folder'); @@ -1162,11 +1257,8 @@ export class WorkspacesUsecases { } } - const workspace = await this.workspaceRepository.findById(workspaceId); - - const workspaceUser = await this.userUsecases.getUser( - workspace.workspaceUserId, - ); + const { workspaceUser, workspace } = + await this.workspaceRepository.findWorkspaceAndDefaultUser(workspaceId); const [ownerRootFolder, items, sharingRole] = await Promise.all([ this.folderUseCases.getFolderByUserId( @@ -1174,7 +1266,7 @@ export class WorkspacesUsecases { workspaceUser.id, ), itemsType === WorkspaceItemType.Folder - ? await getFolderContentByCreatedBy(itemFolder.createdBy, folder.uuid) + ? await getFoldersFromFolder(itemFolder.createdBy, folder.uuid) : await getFilesFromFolder(itemFolder.createdBy, folder.uuid), this.sharingUseCases.findSharingRoleBy({ sharingId: sharing.id }), ]); @@ -1196,7 +1288,6 @@ export class WorkspacesUsecases { }, workspace: { workspaceId: workspace.id, - teamId, }, owner: { uuid: itemFolder.createdBy, From 2447415da24b05b9eb6d7cda9fac6ca3ac78b46d Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Wed, 11 Sep 2024 12:30:50 -0400 Subject: [PATCH 02/10] force redeploy From f5c2e63fbc5b29243abb9fc583d8c040f2569e80 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Tue, 24 Sep 2024 03:01:07 -0400 Subject: [PATCH 03/10] feat: added support for multi teams with different roles --- .../guards/sharings-token.interface.ts | 10 +- src/modules/sharing/models/index.ts | 7 + src/modules/sharing/sharing.repository.ts | 25 ++- src/modules/sharing/sharing.service.ts | 33 +--- .../workspaces/workspaces.controller.spec.ts | 3 - .../workspaces/workspaces.controller.ts | 2 - .../workspaces/workspaces.usecase.spec.ts | 17 +- src/modules/workspaces/workspaces.usecase.ts | 155 +++++++----------- 8 files changed, 101 insertions(+), 151 deletions(-) diff --git a/src/modules/sharing/guards/sharings-token.interface.ts b/src/modules/sharing/guards/sharings-token.interface.ts index 16c3ae75..7634b50c 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 31a65b68..427b270a 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.repository.ts b/src/modules/sharing/sharing.repository.ts index 5389f541..e00c8e06 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -456,7 +456,7 @@ export class SequelizeSharingRepository implements SharingRepository { async findSharingsBySharedWithAndAttributes( sharedWithValues: SharingAttributes['sharedWith'][], filters: Omit, 'sharedWith'> = {}, - options?: { offset: number; limit: number }, + options?: { offset: number; limit: number; givePriorityToRole?: string }, ): Promise { const where: WhereOptions = { ...filters, @@ -464,10 +464,28 @@ export class SequelizeSharingRepository implements SharingRepository { [Op.in]: sharedWithValues, }, }; + + const queryOrder = []; + if (options?.givePriorityToRole) { + queryOrder.push([ + sequelize.literal( + `CASE WHEN "role->role"."name" = '${options.givePriorityToRole}' 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, }); return sharings.map((sharing) => @@ -493,14 +511,13 @@ export class SequelizeSharingRepository implements SharingRepository { }, ], }, - attributes: [ - [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], - ], + attributes: ['createdAt'], group: [ 'file.id', 'file->workspaceUser.id', 'file->workspaceUser->creator.id', 'SharingModel.item_id', + 'SharingModel.id', ], include: [ { diff --git a/src/modules/sharing/sharing.service.ts b/src/modules/sharing/sharing.service.ts index 03a5fd02..27427454 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -222,7 +222,7 @@ export class SharingService { findSharingsBySharedWithAndAttributes( sharedWithValues: Sharing['sharedWith'][], filters: Omit, 'sharedWith'> = {}, - options?: { offset: number; limit: number }, + options?: { offset: number; limit: number; givePriorityToRole?: string }, ): Promise { return this.sharingRepository.findSharingsBySharedWithAndAttributes( sharedWithValues, @@ -1840,19 +1840,6 @@ export class SharingService { }), )) as FolderWithSharedInfo[]; - const sharedRootToken = generateWithDefaultSecret( - { - isRootToken: true, - workspace: { - workspaceId, - }, - owner: { - uuid: user.uuid, - }, - }, - '1d', - ); - return { folders: folders, files: [], @@ -1860,7 +1847,7 @@ export class SharingService { networkPass: user.userId, networkUser: user.bridgeUser, }, - token: sharedRootToken, + token: '', role: 'OWNER', }; } @@ -1995,20 +1982,6 @@ export class SharingService { }), )) as FileWithSharedInfo[]; - const sharedRootToken = generateWithDefaultSecret( - { - isRootToken: true, - workspace: { - workspaceId, - }, - owner: { - id: user.id, - uuid: user.uuid, - }, - }, - '1d', - ); - return { folders: [], files: files, @@ -2016,7 +1989,7 @@ export class SharingService { networkPass: user.userId, networkUser: user.bridgeUser, }, - token: sharedRootToken, + token: '', role: 'OWNER', }; } diff --git a/src/modules/workspaces/workspaces.controller.spec.ts b/src/modules/workspaces/workspaces.controller.spec.ts index 23d9eeee..63047abf 100644 --- a/src/modules/workspaces/workspaces.controller.spec.ts +++ b/src/modules/workspaces/workspaces.controller.spec.ts @@ -567,7 +567,6 @@ describe('Workspace Controller', () => { it('When files 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'; @@ -577,7 +576,6 @@ describe('Workspace Controller', () => { await workspacesController.getFilesInsideSharedFolder( workspaceId, - teamId, user, sharedFolderId, { token, page, perPage, orderBy }, @@ -585,7 +583,6 @@ describe('Workspace Controller', () => { expect(workspacesUsecases.getItemsInSharedFolder).toHaveBeenCalledWith( workspaceId, - teamId, user, sharedFolderId, WorkspaceItemType.File, diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 25b399dd..cf4af642 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -791,8 +791,6 @@ export class WorkspacesController { async getFilesInsideSharedFolder( @Param('workspaceId', ValidateUUIDPipe) workspaceId: WorkspaceAttributes['id'], - @Param('teamId', ValidateUUIDPipe) - teamId: WorkspaceTeam['id'], @UserDecorator() user: User, @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 34e734a3..ca97a96f 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -2454,7 +2454,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2473,7 +2472,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2491,7 +2489,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2522,7 +2519,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2561,7 +2557,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2571,13 +2566,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 +2583,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2636,7 +2632,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2688,7 +2683,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2724,7 +2718,6 @@ describe('WorkspacesUsecases', () => { await expect( service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, itemsType, @@ -2755,7 +2748,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, WorkspaceItemType.File, @@ -2821,7 +2813,6 @@ describe('WorkspacesUsecases', () => { const result = await service.getItemsInSharedFolder( workspaceId, - teamId, user, folderUuid, WorkspaceItemType.File, diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 667f4bd0..4d64472e 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -65,7 +65,7 @@ import { WorkspaceItemUser } from './domains/workspace-item-user.domain'; import { 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 { @@ -75,7 +75,6 @@ export class WorkspacesUsecases { private readonly sharingUseCases: SharingService, private readonly paymentService: PaymentsService, private networkService: BridgeService, - private cryptoService: CryptoService, private userRepository: SequelizeUserRepository, private userUsecases: UserUseCases, private configService: ConfigService, @@ -1021,21 +1020,7 @@ export class WorkspacesUsecases { options, ); - const sharedRootToken = generateWithDefaultSecret( - { - isRootToken: true, - workspace: { - workspaceId, - }, - owner: { - id: user.id, - uuid: user.uuid, - }, - }, - '1d', - ); - - return { ...response, token: sharedRootToken }; + return { ...response, token: '' }; } async getSharedFoldersInWorkspace( @@ -1058,21 +1043,7 @@ export class WorkspacesUsecases { options, ); - const sharedRootToken = generateWithDefaultSecret( - { - isRootToken: true, - workspace: { - workspaceId, - }, - owner: { - id: user.id, - uuid: user.uuid, - }, - }, - '1d', - ); - - return { ...response, token: sharedRootToken }; + return { ...response, token: '' }; } async getItemsInSharedFolder( @@ -1152,64 +1123,52 @@ export class WorkspacesUsecases { throw new NotFoundException('Item not found in workspace'); } - const folder = await this.folderUseCases.getByUuid(folderUuid); + const currentFolder = await this.folderUseCases.getByUuid(folderUuid); - if (folder.isTrashed()) { + if (currentFolder.isTrashed()) { throw new BadRequestException('This folder is trashed'); } - if (folder.isRemoved()) { + if (currentFolder.isRemoved()) { throw new BadRequestException('This folder is removed'); } - const parentFolder = folder.parentUuid - ? await this.folderUseCases.getByUuid(folder.parentUuid) - : null; + const parentFolder = + currentFolder.parentUuid && + (await this.folderUseCases.getByUuid(currentFolder.parentUuid)); if (itemFolder.isOwnedBy(user)) { - const itemsInFolder = + const getItemsFromFolder = itemsType === WorkspaceItemType.Folder - ? await getFoldersFromFolder(itemFolder.createdBy, folder.uuid) - : await getFilesFromFolder(itemFolder.createdBy, folder.uuid); + ? getFoldersFromFolder + : getFilesFromFolder; + + const itemsInFolder = await getItemsFromFolder( + itemFolder.createdBy, + currentFolder.uuid, + ); return { items: itemsInFolder, - name: folder.plainName, + 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']; - }; - 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'); } @@ -1218,18 +1177,18 @@ export class WorkspacesUsecases { workspaceId, ); - const teamsIds = teamsUserBelongsTo.map((team) => team.id); + const teamIds = teamsUserBelongsTo.map((team) => team.id); const itemSharedWithTeam = await this.sharingUseCases.findSharingsBySharedWithAndAttributes( - teamsIds, + teamIds, { sharedWithType: SharedWithType.WorkspaceTeam, - itemId: requestedFolderIsSharedRootFolder + itemId: isSharedRootFolderRequest ? folderUuid - : decoded.sharedRootFolderId, + : decodedAccessToken.sharedRootFolderId, }, - { limit: 1, offset: 0 }, + { limit: 1, offset: 0, givePriorityToRole: 'EDITOR' }, ); const sharing = itemSharedWithTeam[0]; @@ -1238,19 +1197,19 @@ export class WorkspacesUsecases { 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', ); @@ -1260,19 +1219,21 @@ export class WorkspacesUsecases { const { workspaceUser, workspace } = await this.workspaceRepository.findWorkspaceAndDefaultUser(workspaceId); - const [ownerRootFolder, items, sharingRole] = await Promise.all([ - this.folderUseCases.getFolderByUserId( - workspaceUser.rootFolderId, - workspaceUser.id, - ), - itemsType === WorkspaceItemType.Folder - ? await getFoldersFromFolder(itemFolder.createdBy, folder.uuid) - : await getFilesFromFolder(itemFolder.createdBy, folder.uuid), - this.sharingUseCases.findSharingRoleBy({ sharingId: sharing.id }), - ]); + 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 }), + ], + ); return { - items, + items: folderItems, credentials: { networkPass: workspaceUser.userId, networkUser: workspaceUser.bridgeUser, @@ -1283,8 +1244,8 @@ 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, @@ -1301,8 +1262,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, }; } From 0d700d969f1b65b7a7972518114bd9965408ec4e Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Tue, 24 Sep 2024 21:17:36 -0400 Subject: [PATCH 04/10] chore: added tests --- .../sharing/sharing.repository.spec.ts | 354 +++++++++++++++++- src/modules/sharing/sharing.repository.ts | 13 +- src/modules/sharing/sharing.service.spec.ts | 198 ++++++++++ src/modules/sharing/sharing.service.ts | 6 +- .../workspaces.repository.spec.ts | 40 ++ .../workspaces/workspaces.controller.spec.ts | 114 ++++++ .../workspaces/workspaces.controller.ts | 4 +- .../workspaces/workspaces.usecase.spec.ts | 149 ++++++++ src/modules/workspaces/workspaces.usecase.ts | 4 +- 9 files changed, 866 insertions(+), 16 deletions(-) diff --git a/src/modules/sharing/sharing.repository.spec.ts b/src/modules/sharing/sharing.repository.spec.ts index 69d4296a..734832ca 100644 --- a/src/modules/sharing/sharing.repository.spec.ts +++ b/src/modules/sharing/sharing.repository.spec.ts @@ -1,12 +1,24 @@ 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 { + newFile, + newFolder, + newSharing, + newUser, +} from '../../../test/fixtures'; import { User } from '../user/user.domain'; 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; @@ -107,4 +119,340 @@ describe('SharingRepository', () => { expect(result[0].folder.user).toBeInstanceOf(User); }); }); + 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 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: ['createdAt'], + group: [ + 'file.id', + 'file->workspaceUser.id', + 'file->workspaceUser->creator.id', + 'SharingModel.item_id', + 'SharingModel.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 sharedFileWithUser = { + ...sharing, + get: jest.fn().mockReturnValue({ + ...sharing, + file: { + ...file, + workspaceUser: { + creator, + }, + }, + }), + }; + + jest + .spyOn(sharingModel, 'findAll') + .mockResolvedValue([sharedFileWithUser] as any); + + const result = await repository.findFilesSharedInWorkspaceByOwnerAndTeams( + ownerId, + workspaceId, + teamIds, + { offset, limit }, + ); + + expect(result[0].file).toMatchObject({ + ...file, + user: { + uuid: creator.uuid, + email: creator.email, + name: creator.name, + lastname: creator.lastname, + avatar: creator.avatar, + }, + }); + }); + }); + + describe('findFoldersSharedInWorkspaceByOwnerAndTeams', () => { + 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][] = [['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: [ + 'folder.id', + 'folder->workspaceUser.id', + 'folder->workspaceUser->creator.id', + 'SharingModel.item_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({ + ...sharedFolder, + folder: { + ...folder, + workspaceUser: { + creator, + }, + }, + }), + }; + + jest + .spyOn(sharingModel, 'findAll') + .mockResolvedValue([sharedFolderWithUser] as any); + + const result = + await repository.findFoldersSharedInWorkspaceByOwnerAndTeams( + ownerId, + workspaceId, + teamIds, + { offset, limit, order: orderBy }, + ); + + expect(result[0]).toBeInstanceOf(Sharing); + 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 e00c8e06..2afd6d52 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -469,7 +469,7 @@ export class SequelizeSharingRepository implements SharingRepository { if (options?.givePriorityToRole) { queryOrder.push([ sequelize.literal( - `CASE WHEN "role->role"."name" = '${options.givePriorityToRole}' THEN 1 ELSE 2 END`, + `CASE WHEN "role->role"."name" = :priorityRole THEN 1 ELSE 2 END`, ), 'ASC', ]); @@ -486,6 +486,9 @@ export class SequelizeSharingRepository implements SharingRepository { limit: options.limit, offset: options.offset, order: queryOrder, + replacements: { + priorityRole: options?.givePriorityToRole, + }, }); return sharings.map((sharing) => @@ -497,7 +500,7 @@ export class SequelizeSharingRepository implements SharingRepository { ownerId: WorkspaceItemUserAttributes['createdBy'], workspaceId: WorkspaceAttributes['id'], teamIds: WorkspaceTeamAttributes['id'][], - options: { offset: number; limit: number; orderBy?: [string, string][] }, + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { const sharedFiles = await this.sharings.findAll({ where: { @@ -544,7 +547,7 @@ export class SequelizeSharingRepository implements SharingRepository { ], }, ], - order: options.orderBy, + order: options.order, limit: options.limit, offset: options.offset, }); @@ -715,7 +718,7 @@ export class SequelizeSharingRepository implements SharingRepository { ownerId: WorkspaceItemUserAttributes['createdBy'], workspaceId: WorkspaceAttributes['id'], teamsIds: WorkspaceTeamAttributes['id'][], - options: { offset: number; limit: number; orderBy?: [string, string][] }, + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { const sharedFolders = await this.sharings.findAll({ where: { @@ -763,7 +766,7 @@ export class SequelizeSharingRepository implements SharingRepository { ], }, ], - order: options.orderBy, + order: options.order, limit: options.limit, offset: options.offset, }); diff --git a/src/modules/sharing/sharing.service.spec.ts b/src/modules/sharing/sharing.service.spec.ts index 6aa50cea..c630af63 100644 --- a/src/modules/sharing/sharing.service.spec.ts +++ b/src/modules/sharing/sharing.service.spec.ts @@ -758,6 +758,204 @@ describe('Sharing Use Cases', () => { ).rejects.toThrow(error); }); }); + + describe('getSharedFilesInWorkspaceByTeams', () => { + const user = newUser(); + 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 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, 'findFilesSharedInWorkspaceByOwnerAndTeams') + .mockResolvedValue(filesWithSharedInfo); + + jest + .spyOn(fileUsecases, 'decrypFileName') + .mockReturnValue({ plainName: 'DecryptedFileName' }); + + jest.spyOn(usersUsecases, 'getAvatarUrl').mockResolvedValue('avatar-url'); + + const result = await sharingService.getSharedFilesInWorkspaceByTeams( + user, + workspaceId, + teamIds, + { offset, limit, order }, + ); + + expect(result).toEqual( + expect.objectContaining({ + folders: [], + files: expect.arrayContaining([ + expect.objectContaining({ + plainName: sharing.file.plainName, + sharingId: sharing.id, + encryptionKey: sharing.encryptionKey, + dateShared: sharing.createdAt, + }), + ]), + credentials: { + networkPass: user.userId, + networkUser: user.bridgeUser, + }, + token: '', + role: 'OWNER', + }), + ); + }); + + it('When no files are shared with teams user belongs to, then it should return nothing', async () => { + jest + .spyOn(sharingRepository, 'findFilesSharedInWorkspaceByOwnerAndTeams') + .mockResolvedValue([]); + + const result = await sharingService.getSharedFilesInWorkspaceByTeams( + user, + workspaceId, + teamIds, + { offset, limit, order }, + ); + + expect(result).toEqual({ + folders: [], + files: [], + credentials: { + networkPass: user.userId, + networkUser: user.bridgeUser, + }, + token: '', + role: 'OWNER', + }); + }); + + it('When there is an error fetching shared files, then it should throw', async () => { + const error = new Error('Database error'); + + jest + .spyOn(sharingRepository, 'findFilesSharedInWorkspaceByOwnerAndTeams') + .mockRejectedValue(error); + + await expect( + sharingService.getSharedFilesInWorkspaceByTeams( + user, + workspaceId, + teamIds, + { + offset, + limit, + order, + }, + ), + ).rejects.toThrow(error); + }); + }); + + describe('getSharedFoldersInWorkspaceByTeams', () => { + const user = newUser(); + 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 a team the user belongs to, then it should return the folders', async () => { + const sharing = newSharing(); + const folder = newFolder(); + folder.user = newUser(); + sharing.folder = folder; + const foldersWithSharedInfo = [sharing]; + + jest + .spyOn(sharingRepository, 'findFoldersSharedInWorkspaceByOwnerAndTeams') + .mockResolvedValue(foldersWithSharedInfo); + + jest + .spyOn(folderUseCases, 'decryptFolderName') + .mockReturnValue({ plainName: 'DecryptedFolderName' }); + + jest.spyOn(usersUsecases, 'getAvatarUrl').mockResolvedValue('avatar-url'); + + const result = await sharingService.getSharedFoldersInWorkspaceByTeams( + user, + workspaceId, + teamIds, + { offset, limit, order }, + ); + + expect(result).toEqual( + expect.objectContaining({ + folders: expect.arrayContaining([ + expect.objectContaining({ + sharingId: sharing.id, + encryptionKey: sharing.encryptionKey, + dateShared: sharing.createdAt, + sharedWithMe: true, + }), + ]), + files: [], + credentials: { + networkPass: user.userId, + networkUser: user.bridgeUser, + }, + token: '', + role: 'OWNER', + }), + ); + }); + + 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, 'findFoldersSharedInWorkspaceByOwnerAndTeams') + .mockResolvedValue([]); + + const result = await sharingService.getSharedFoldersInWorkspaceByTeams( + user, + workspaceId, + teamIds, + { offset, limit, order }, + ); + + expect(result).toEqual({ + folders: [], + files: [], + credentials: { + networkPass: user.userId, + networkUser: user.bridgeUser, + }, + token: '', + role: 'OWNER', + }); + }); + + it('When there is an error fetching shared folders, then it should throw an error', async () => { + const error = new Error('Database error'); + + jest + .spyOn(sharingRepository, 'findFoldersSharedInWorkspaceByOwnerAndTeams') + .mockRejectedValue(error); + + await expect( + sharingService.getSharedFoldersInWorkspaceByTeams( + user, + workspaceId, + 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 27427454..d2e1d5df 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -34,7 +34,6 @@ import { UpdateSharingRoleDto } from './dto/update-sharing-role.dto'; import getEnv from '../../config/configuration'; import { generateTokenWithPlainSecret, - generateWithDefaultSecret, verifyWithDefaultSecret, } from '../../lib/jwt'; import { @@ -54,7 +53,6 @@ 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 { WorkspacesUsecases } from '../workspaces/workspaces.usecase'; import { WorkspaceTeamAttributes } from '../workspaces/attributes/workspace-team.attributes'; export class InvalidOwnerError extends Error { @@ -1856,7 +1854,7 @@ export class SharingService { user: User, workspaceId: Workspace['id'], teamIds: WorkspaceTeamAttributes['id'][], - options: { offset: number; limit: number; orderBy?: [string, string][] }, + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { const filesWithSharedInfo = await this.sharingRepository.findFilesSharedInWorkspaceByOwnerAndTeams( @@ -1901,7 +1899,7 @@ export class SharingService { user: User, workspaceId: Workspace['id'], teamIds: WorkspaceTeamAttributes['id'][], - options: { offset: number; limit: number; orderBy?: [string, string][] }, + options: { offset: number; limit: number; order?: [string, string][] }, ): Promise { const foldersWithSharedInfo = await this.sharingRepository.findFoldersSharedInWorkspaceByOwnerAndTeams( diff --git a/src/modules/workspaces/repositories/workspaces.repository.spec.ts b/src/modules/workspaces/repositories/workspaces.repository.spec.ts index 1806c6f6..5a0a5cc1 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/workspaces.controller.spec.ts b/src/modules/workspaces/workspaces.controller.spec.ts index 63047abf..efb2f645 100644 --- a/src/modules/workspaces/workspaces.controller.spec.ts +++ b/src/modules/workspaces/workspaces.controller.spec.ts @@ -592,6 +592,120 @@ describe('Workspace Controller', () => { }); }); + describe('GET /:workspaceId/shared/files', () => { + it('When files shared with user teams are requested, then it should call the service with the respective arguments', async () => { + const user = newUser(); + const workspaceId = v4(); + const orderBy = 'createdAt:ASC'; + const page = 1; + const perPage = 50; + const order = [['createdAt', 'ASC']]; + + await workspacesController.getSharedFilesInWorkspace( + workspaceId, + user, + orderBy, + page, + perPage, + ); + + expect(workspacesUsecases.getSharedFilesInWorkspace).toHaveBeenCalledWith( + user, + workspaceId, + { + offset: page, + limit: perPage, + order, + }, + ); + }); + }); + + describe('GET /:workspaceId/shared/folders', () => { + it('When folders shared with user teams are requested, then it should call the service with the respective arguments', async () => { + const user = newUser(); + const workspaceId = v4(); + const orderBy = 'createdAt:ASC'; + const page = 1; + const perPage = 50; + const order = [['createdAt', 'ASC']]; + + 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 workspaceId = v4(); + const sharedFolderId = v4(); + const orderBy = 'createdAt:ASC'; + const token = 'token'; + const page = 1; + const perPage = 50; + const order = [['createdAt', 'ASC']]; + + await workspacesController.getFilesInSharingFolder( + workspaceId, + user, + sharedFolderId, + { token, page, perPage, orderBy }, + ); + + expect(workspacesUsecases.getItemsInSharedFolder).toHaveBeenCalledWith( + workspaceId, + user, + sharedFolderId, + WorkspaceItemType.File, + token, + { page, perPage, order }, + ); + }); + }); + + 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 sharedFolderId = v4(); + const orderBy = 'createdAt:ASC'; + const token = 'token'; + const page = 1; + const perPage = 50; + const order = [['createdAt', 'ASC']]; + + await workspacesController.getFoldersInSharingFolder( + workspaceId, + user, + sharedFolderId, + { token, page, perPage, orderBy }, + ); + + expect(workspacesUsecases.getItemsInSharedFolder).toHaveBeenCalledWith( + workspaceId, + user, + sharedFolderId, + WorkspaceItemType.Folder, + token, + { page, perPage, order }, + ); + }); + }); + describe('POST /:workspaceId/folders', () => { it('When a folder is created successfully, then it should call the service with the respective arguments', async () => { const user = newUser(); diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index cf4af642..0a7bd003 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -600,7 +600,7 @@ export class WorkspacesController { return this.workspaceUseCases.getSharedFilesInWorkspace(user, workspaceId, { offset: page, limit: perPage, - orderBy: order, + order, }); } @@ -628,7 +628,7 @@ export class WorkspacesController { { offset: page, limit: perPage, - orderBy: order, + order, }, ); } diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index ca97a96f..aa5f3a48 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -50,6 +50,10 @@ import { Role } 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'); @@ -4071,6 +4075,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(); diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 4d64472e..98d5de5a 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -1003,7 +1003,7 @@ export class WorkspacesUsecases { async getSharedFilesInWorkspace( user: User, workspaceId: Workspace['id'], - options: { offset: number; limit: number; orderBy?: [string, string][] }, + options: { offset: number; limit: number; order?: [string, string][] }, ) { const teams = await this.getWorkspaceTeamsUserBelongsTo( user.uuid, @@ -1026,7 +1026,7 @@ export class WorkspacesUsecases { async getSharedFoldersInWorkspace( user: User, workspaceId: Workspace['id'], - options: { offset: number; limit: number; orderBy?: [string, string][] }, + options: { offset: number; limit: number; order?: [string, string][] }, ) { const teams = await this.getWorkspaceTeamsUserBelongsTo( user.uuid, From 16c5b3607f12c35b3e033382576c8383d79d276d Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 26 Sep 2024 09:49:02 -0400 Subject: [PATCH 05/10] chore: changed group by order while fetching files and folders --- .../sharing/sharing.repository.spec.ts | 9 +- src/modules/sharing/sharing.repository.ts | 97 ++++++++++--------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/src/modules/sharing/sharing.repository.spec.ts b/src/modules/sharing/sharing.repository.spec.ts index 734832ca..1c54281f 100644 --- a/src/modules/sharing/sharing.repository.spec.ts +++ b/src/modules/sharing/sharing.repository.spec.ts @@ -246,13 +246,14 @@ describe('SharingRepository', () => { }, ], }, - attributes: ['createdAt'], + attributes: [ + [Sequelize.literal('MAX("SharingModel"."created_at")'), 'createdAt'], + ], group: [ + 'SharingModel.item_id', 'file.id', 'file->workspaceUser.id', 'file->workspaceUser->creator.id', - 'SharingModel.item_id', - 'SharingModel.id', ], include: [ { @@ -364,10 +365,10 @@ describe('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: [ { diff --git a/src/modules/sharing/sharing.repository.ts b/src/modules/sharing/sharing.repository.ts index 2afd6d52..ae1fc12f 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -514,13 +514,14 @@ export class SequelizeSharingRepository implements SharingRepository { }, ], }, - attributes: ['createdAt'], + attributes: [ + [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], + ], group: [ + 'SharingModel.item_id', 'file.id', 'file->workspaceUser.id', 'file->workspaceUser->creator.id', - 'SharingModel.item_id', - 'SharingModel.id', ], include: [ { @@ -567,23 +568,21 @@ export class SequelizeSharingRepository implements SharingRepository { }); } - async findFilesByOwnerAndSharedWithTeamInworkspace( - 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 sharedFiles = await this.sharings.findAll({ + const sharedFolders = await this.sharings.findAll({ where: { [Op.or]: [ { - sharedWith: teamId, + sharedWith: { [Op.in]: teamsIds }, sharedWithType: SharedWithType.WorkspaceTeam, }, { - '$file->workspaceUser.created_by$': ownerId, + '$folder->workspaceUser.created_by$': ownerId, }, ], }, @@ -591,21 +590,21 @@ export class SequelizeSharingRepository implements SharingRepository { [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], ], group: [ - 'file.id', - 'file->workspaceUser.id', - 'file->workspaceUser->creator.id', 'SharingModel.item_id', + 'folder.id', + 'folder->workspaceUser.id', + 'folder->workspaceUser->creator.id', ], include: [ { - model: FileModel, + model: FolderModel, where: { - status: FileStatus.EXISTS, + deleted: false, + removed: false, }, include: [ { model: WorkspaceItemUserModel, - as: 'workspaceUser', required: true, where: { workspaceId, @@ -621,27 +620,26 @@ export class SequelizeSharingRepository implements SharingRepository { ], }, ], - order: orderBy, - limit, - offset, + order: options.order, + limit: options.limit, + offset: options.offset, }); - return sharedFiles.map((shared) => { + return sharedFolders.map((shared) => { const sharing = shared.get({ plain: true }); - const user = sharing.file.workspaceUser?.creator; - delete sharing.file.user; + const user = sharing.folder.workspaceUser?.creator; return Sharing.build({ ...sharing, - file: File.build({ - ...sharing.file, + folder: Folder.build({ + ...sharing.folder, user: user ? User.build(user) : null, }), }); }); } - async findFoldersByOwnerAndSharedWithTeamInworkspace( + async findFilesByOwnerAndSharedWithTeamInworkspace( workspaceId: WorkspaceAttributes['id'], teamId: WorkspaceTeamAttributes['id'], ownerId: WorkspaceItemUserAttributes['createdBy'], @@ -649,7 +647,7 @@ export class SequelizeSharingRepository implements SharingRepository { limit: number, orderBy?: [string, string][], ): Promise { - const sharedFolders = await this.sharings.findAll({ + const sharedFiles = await this.sharings.findAll({ where: { [Op.or]: [ { @@ -657,7 +655,7 @@ export class SequelizeSharingRepository implements SharingRepository { sharedWithType: SharedWithType.WorkspaceTeam, }, { - '$folder->workspaceUser.created_by$': ownerId, + '$file->workspaceUser.created_by$': ownerId, }, ], }, @@ -665,21 +663,21 @@ export class SequelizeSharingRepository implements SharingRepository { [sequelize.literal(`MAX("SharingModel"."created_at")`), 'createdAt'], ], group: [ - 'folder.id', - 'folder->workspaceUser.id', - 'folder->workspaceUser->creator.id', 'SharingModel.item_id', + 'file.id', + 'file->workspaceUser.id', + 'file->workspaceUser->creator.id', ], include: [ { - model: FolderModel, + model: FileModel, where: { - deleted: false, - removed: false, + status: FileStatus.EXISTS, }, include: [ { model: WorkspaceItemUserModel, + as: 'workspaceUser', required: true, where: { workspaceId, @@ -700,31 +698,34 @@ export class SequelizeSharingRepository implements SharingRepository { offset, }); - return sharedFolders.map((shared) => { + return sharedFiles.map((shared) => { const sharing = shared.get({ plain: true }); - const user = sharing.folder.workspaceUser?.creator; + const user = sharing.file.workspaceUser?.creator; + delete sharing.file.user; return Sharing.build({ ...sharing, - folder: Folder.build({ - ...sharing.folder, + file: File.build({ + ...sharing.file, user: user ? User.build(user) : null, }), }); }); } - async findFoldersSharedInWorkspaceByOwnerAndTeams( - ownerId: WorkspaceItemUserAttributes['createdBy'], + async findFoldersByOwnerAndSharedWithTeamInworkspace( workspaceId: WorkspaceAttributes['id'], - teamsIds: WorkspaceTeamAttributes['id'][], - options: { offset: number; limit: number; order?: [string, string][] }, + teamId: WorkspaceTeamAttributes['id'], + ownerId: WorkspaceItemUserAttributes['createdBy'], + offset: number, + limit: number, + orderBy?: [string, string][], ): Promise { const sharedFolders = await this.sharings.findAll({ where: { [Op.or]: [ { - sharedWith: { [Op.in]: teamsIds }, + sharedWith: teamId, sharedWithType: SharedWithType.WorkspaceTeam, }, { @@ -736,10 +737,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: [ { @@ -766,9 +767,9 @@ export class SequelizeSharingRepository implements SharingRepository { ], }, ], - order: options.order, - limit: options.limit, - offset: options.offset, + order: orderBy, + limit, + offset, }); return sharedFolders.map((shared) => { From 05848f819a88ab78f5053679d04f9d9757d47874 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 26 Sep 2024 11:08:21 -0400 Subject: [PATCH 06/10] chore: remove not needed controllers and move old routes to new controller functions --- .../workspaces/workspaces.controller.spec.ts | 89 ----------- .../workspaces/workspaces.controller.ts | 141 ++---------------- 2 files changed, 15 insertions(+), 215 deletions(-) diff --git a/src/modules/workspaces/workspaces.controller.spec.ts b/src/modules/workspaces/workspaces.controller.spec.ts index efb2f645..9e31fa18 100644 --- a/src/modules/workspaces/workspaces.controller.spec.ts +++ b/src/modules/workspaces/workspaces.controller.spec.ts @@ -503,95 +503,6 @@ 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( - workspaceId, - teamId, - user, - orderBy, - page, - perPage, - ); - - expect(sharingUseCases.getSharedFilesInWorkspaces).toHaveBeenCalledWith( - user, - workspaceId, - teamId, - page, - 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 () => { - 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.getSharedFolders( - workspaceId, - teamId, - user, - orderBy, - page, - perPage, - ); - - expect(sharingUseCases.getSharedFoldersInWorkspace).toHaveBeenCalledWith( - user, - workspaceId, - teamId, - 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 () => { - const user = newUser(); - 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.getFilesInsideSharedFolder( - workspaceId, - user, - sharedFolderId, - { token, page, perPage, orderBy }, - ); - - expect(workspacesUsecases.getItemsInSharedFolder).toHaveBeenCalledWith( - workspaceId, - user, - sharedFolderId, - WorkspaceItemType.File, - token, - { page, perPage, order }, - ); - }); - }); - describe('GET /:workspaceId/shared/files', () => { it('When files shared with user teams are requested, then it should call the service with the respective arguments', async () => { const user = newUser(); diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 0a7bd003..e3f99b82 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -579,7 +579,7 @@ export class WorkspacesController { ); } - @Get(':workspaceId/shared/files') + @Get([':workspaceId/teams/:teamId/shared/files', ':workspaceId/shared/files']) @ApiOperation({ summary: 'Get shared files in teams', }) @@ -604,9 +604,12 @@ export class WorkspacesController { }); } - @Get(':workspaceId/shared/folders') + @Get([ + ':workspaceId/teams/:teamId/shared/folders', + ':workspaceId/shared/folders', + ]) @ApiOperation({ - summary: 'Get shared files in teams', + summary: 'Get shared folders in teams', }) @UseGuards(WorkspaceGuard) @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) @@ -633,9 +636,12 @@ export class WorkspacesController { ); } - @Get(':workspaceId/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) @@ -662,7 +668,10 @@ export class WorkspacesController { ); } - @Get(':workspaceId/shared/:sharedFolderId/files') + @Get([ + ':workspaceId/teams/:teamId/shared/:sharedFolderId/files', + ':workspaceId/shared/:sharedFolderId/files', + ]) @ApiOperation({ summary: 'Get files inside a shared folder', }) @@ -691,126 +700,6 @@ export class WorkspacesController { ); } - @Get(':workspaceId/teams/:teamId/shared/files') - @ApiOperation({ - summary: 'Get shared files with a team', - }) - @UseGuards(WorkspaceGuard) - @WorkspaceRequiredAccess(AccessContext.TEAM, WorkspaceRole.MEMBER) - async getSharedFiles( - @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, - ) { - const order = orderBy - ? [orderBy.split(':') as [string, string]] - : undefined; - - return this.sharingUseCases.getSharedFilesInWorkspaces( - user, - workspaceId, - teamId, - page, - perPage, - order, - ); - } - - @Get(':workspaceId/teams/:teamId/shared/folders') - @ApiOperation({ - summary: 'Get shared folders with a team', - }) - @UseGuards(WorkspaceGuard) - @WorkspaceRequiredAccess(AccessContext.TEAM, WorkspaceRole.MEMBER) - async getSharedFolders( - @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, - ) { - const order = orderBy - ? [orderBy.split(':') as [string, string]] - : undefined; - - return this.sharingUseCases.getSharedFoldersInWorkspace( - user, - workspaceId, - teamId, - page, - perPage, - order, - ); - } - - @Get(':workspaceId/teams/:teamId/shared/:sharedFolderId/folders') - @ApiOperation({ - summary: 'Get all folders inside a shared folder', - }) - @UseGuards(WorkspaceGuard) - @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) - async getFoldersInsideSharedFolder( - @Param('workspaceId', ValidateUUIDPipe) - workspaceId: WorkspaceAttributes['id'], - @Param('teamId', ValidateUUIDPipe) - teamId: WorkspaceTeam['id'], - @UserDecorator() user: User, - @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], - @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, - ) { - const { orderBy, token, page, perPage } = queryDto; - - const order = orderBy - ? [orderBy.split(':') as [string, string]] - : undefined; - - return this.workspaceUseCases.getItemsInSharedFolder( - workspaceId, - user, - sharedFolderId, - WorkspaceItemType.Folder, - token, - { page, perPage, order }, - ); - } - - @Get(':workspaceId/teams/:teamId/shared/:sharedFolderId/files') - @ApiOperation({ - summary: 'Get files inside a shared folder', - }) - @UseGuards(WorkspaceGuard) - @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER) - async getFilesInsideSharedFolder( - @Param('workspaceId', ValidateUUIDPipe) - workspaceId: WorkspaceAttributes['id'], - @UserDecorator() user: User, - @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], - @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, - ) { - const { orderBy, token, page, perPage } = queryDto; - - const order = orderBy - ? [orderBy.split(':') as [string, string]] - : undefined; - - return this.workspaceUseCases.getItemsInSharedFolder( - workspaceId, - user, - sharedFolderId, - WorkspaceItemType.File, - token, - { page, perPage, order }, - ); - } - @Post('/:workspaceId/folders') @ApiOperation({ summary: 'Create folder', From 8050225f418131ce45b4d82d6159f1b6ac7ccefc Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 26 Sep 2024 11:19:08 -0400 Subject: [PATCH 07/10] chore: remove not needed functions to prevent code duplication --- .../sharing/sharing.repository.spec.ts | 83 ------- src/modules/sharing/sharing.repository.ts | 147 ------------ src/modules/sharing/sharing.service.spec.ts | 219 ------------------ src/modules/sharing/sharing.service.ts | 104 --------- ...-folder.dto.ts => get-shared-items.dto.ts} | 2 +- .../workspaces/workspaces.controller.spec.ts | 35 ++- .../workspaces/workspaces.controller.ts | 21 +- 7 files changed, 27 insertions(+), 584 deletions(-) rename src/modules/workspaces/dto/{get-items-inside-shared-folder.dto.ts => get-shared-items.dto.ts} (94%) diff --git a/src/modules/sharing/sharing.repository.spec.ts b/src/modules/sharing/sharing.repository.spec.ts index 1c54281f..14334a17 100644 --- a/src/modules/sharing/sharing.repository.spec.ts +++ b/src/modules/sharing/sharing.repository.spec.ts @@ -10,7 +10,6 @@ import { newSharing, newUser, } from '../../../test/fixtures'; -import { User } from '../user/user.domain'; import { v4 } from 'uuid'; import { SharingRolesModel } from './models/sharing-roles.model'; import { Op, Sequelize } from 'sequelize'; @@ -37,88 +36,6 @@ 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(); - const offset = 0; - const limit = 10; - const orderBy = [['name', 'ASC']] as any; - const sharing = newSharing(); - const file = newFile(); - const creator = newUser(); - - const mockSharing = { - get: jest.fn().mockReturnValue({ - ...sharing, - file: { - ...file, - workspaceUser: { - creator, - }, - }, - }), - }; - - jest - .spyOn(sharingModel, 'findAll') - .mockResolvedValue([mockSharing] as any); - - const result = - await repository.findFilesByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, - ownerId, - offset, - limit, - orderBy, - ); - - expect(result[0]).toBeInstanceOf(Sharing); - expect(result[0].file.user).toBeInstanceOf(User); - expect(result[0].file).toMatchObject({ ...file, user: creator }); - }); - }); - - describe('findFoldersByOwnerAndSharedWithTeamInworkspace', () => { - 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 mockSharing = { - get: jest.fn().mockReturnValue({ - ...newSharing(), - folder: { - workspaceUser: { - creator: newUser(), - }, - }, - }), - }; - - jest - .spyOn(sharingModel, 'findAll') - .mockResolvedValue([mockSharing] as any); - - const result = - await repository.findFoldersByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, - ownerId, - offset, - limit, - orderBy, - ); - - expect(result[0]).toBeInstanceOf(Sharing); - expect(result[0].folder.user).toBeInstanceOf(User); - }); - }); describe('findSharingsBySharedWithAndAttributes', () => { it('When filters are included, then it should call the query with the correct filters', async () => { const sharedWithValues = [v4(), v4()]; diff --git a/src/modules/sharing/sharing.repository.ts b/src/modules/sharing/sharing.repository.ts index ae1fc12f..9f2be5e3 100644 --- a/src/modules/sharing/sharing.repository.ts +++ b/src/modules/sharing/sharing.repository.ts @@ -639,153 +639,6 @@ export class SequelizeSharingRepository implements SharingRepository { }); } - async findFilesByOwnerAndSharedWithTeamInworkspace( - workspaceId: WorkspaceAttributes['id'], - teamId: WorkspaceTeamAttributes['id'], - ownerId: WorkspaceItemUserAttributes['createdBy'], - offset: number, - limit: number, - orderBy?: [string, string][], - ): Promise { - const sharedFiles = await this.sharings.findAll({ - where: { - [Op.or]: [ - { - sharedWith: teamId, - 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, - }); - - return sharedFiles.map((shared) => { - const sharing = shared.get({ plain: true }); - const user = sharing.file.workspaceUser?.creator; - delete sharing.file.user; - - return Sharing.build({ - ...sharing, - file: File.build({ - ...sharing.file, - user: user ? User.build(user) : null, - }), - }); - }); - } - - async findFoldersByOwnerAndSharedWithTeamInworkspace( - workspaceId: WorkspaceAttributes['id'], - teamId: WorkspaceTeamAttributes['id'], - ownerId: WorkspaceItemUserAttributes['createdBy'], - offset: number, - limit: number, - orderBy?: [string, string][], - ): Promise { - const sharedFolders = await this.sharings.findAll({ - where: { - [Op.or]: [ - { - sharedWith: teamId, - 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, - }); - - return sharedFolders.map((shared) => { - const sharing = shared.get({ plain: true }); - const user = sharing.folder.workspaceUser?.creator; - - return Sharing.build({ - ...sharing, - folder: Folder.build({ - ...sharing.folder, - user: user ? User.build(user) : null, - }), - }); - }); - } - private toDomain(model: SharingModel): Sharing { const folder = model.folder.get({ plain: true }); const user = model.folder.user.get({ plain: true }); diff --git a/src/modules/sharing/sharing.service.spec.ts b/src/modules/sharing/sharing.service.spec.ts index c630af63..1f4c30fe 100644 --- a/src/modules/sharing/sharing.service.spec.ts +++ b/src/modules/sharing/sharing.service.spec.ts @@ -490,115 +490,6 @@ describe('Sharing Use Cases', () => { }); }); - describe('getSharedFilesInWorkspaces', () => { - const user = newUser(); - const teamId = 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 () => { - const sharing = newSharing(); - sharing.file = newFile({ owner: newUser() }); - - const filesWithSharedInfo = [sharing]; - - jest - .spyOn( - sharingRepository, - 'findFilesByOwnerAndSharedWithTeamInworkspace', - ) - .mockResolvedValue(filesWithSharedInfo); - - jest - .spyOn(fileUsecases, 'decrypFileName') - .mockReturnValue({ plainName: 'DecryptedFileName' }); - - jest.spyOn(usersUsecases, 'getAvatarUrl').mockResolvedValue('avatar-url'); - - const result = await sharingService.getSharedFilesInWorkspaces( - user, - workspaceId, - teamId, - offset, - limit, - order, - ); - - expect(result).toEqual( - expect.objectContaining({ - folders: [], - files: expect.arrayContaining([ - expect.objectContaining({ - plainName: sharing.file.plainName, - sharingId: sharing.id, - encryptionKey: sharing.encryptionKey, - dateShared: sharing.createdAt, - }), - ]), - credentials: { - networkPass: user.userId, - networkUser: user.bridgeUser, - }, - token: '', - role: 'OWNER', - }), - ); - }); - - it('When no files are shared with the team, then it should return nothing', async () => { - jest - .spyOn( - sharingRepository, - 'findFilesByOwnerAndSharedWithTeamInworkspace', - ) - .mockResolvedValue([]); - - const result = await sharingService.getSharedFilesInWorkspaces( - user, - workspaceId, - teamId, - offset, - limit, - order, - ); - - expect(result).toEqual({ - folders: [], - files: [], - credentials: { - networkPass: user.userId, - networkUser: user.bridgeUser, - }, - token: '', - role: 'OWNER', - }); - }); - - it('When there is an error fetching shared files, then it should throw', async () => { - const error = new Error('Database error'); - - jest - .spyOn( - sharingRepository, - 'findFilesByOwnerAndSharedWithTeamInworkspace', - ) - .mockRejectedValue(error); - - await expect( - sharingService.getSharedFilesInWorkspaces( - user, - workspaceId, - teamId, - offset, - limit, - order, - ), - ).rejects.toThrow(error); - }); - }); - describe('removeSharing', () => { const owner = newUser(); const itemFile = newFile(); @@ -649,116 +540,6 @@ describe('Sharing Use Cases', () => { }); }); - describe('getSharedFoldersInWorkspace', () => { - const user = newUser(); - const teamId = 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 () => { - const sharing = newSharing(); - const folder = newFolder(); - folder.user = newUser(); - sharing.folder = folder; - const foldersWithSharedInfo = [sharing]; - - jest - .spyOn( - sharingRepository, - 'findFoldersByOwnerAndSharedWithTeamInworkspace', - ) - .mockResolvedValue(foldersWithSharedInfo); - - jest - .spyOn(folderUseCases, 'decryptFolderName') - .mockReturnValue({ plainName: 'DecryptedFolderName' }); - - jest.spyOn(usersUsecases, 'getAvatarUrl').mockResolvedValue('avatar-url'); - - const result = await sharingService.getSharedFoldersInWorkspace( - user, - workspaceId, - teamId, - offset, - limit, - order, - ); - - expect(result).toEqual( - expect.objectContaining({ - folders: expect.arrayContaining([ - expect.objectContaining({ - sharingId: sharing.id, - encryptionKey: sharing.encryptionKey, - dateShared: sharing.createdAt, - sharedWithMe: true, - }), - ]), - files: [], - credentials: { - networkPass: user.userId, - networkUser: user.bridgeUser, - }, - token: '', - role: 'OWNER', - }), - ); - }); - - it('When no folders are shared with the team, then it should return an empty folders array', async () => { - jest - .spyOn( - sharingRepository, - 'findFoldersByOwnerAndSharedWithTeamInworkspace', - ) - .mockResolvedValue([]); - - const result = await sharingService.getSharedFoldersInWorkspace( - user, - workspaceId, - teamId, - offset, - limit, - order, - ); - - expect(result).toEqual({ - folders: [], - files: [], - credentials: { - networkPass: user.userId, - networkUser: user.bridgeUser, - }, - token: '', - role: 'OWNER', - }); - }); - - it('When there is an error fetching shared folders, then it should throw an error', async () => { - const error = new Error('Database error'); - - jest - .spyOn( - sharingRepository, - 'findFoldersByOwnerAndSharedWithTeamInworkspace', - ) - .mockRejectedValue(error); - - await expect( - sharingService.getSharedFoldersInWorkspace( - user, - workspaceId, - teamId, - offset, - limit, - order, - ), - ).rejects.toThrow(error); - }); - }); - describe('getSharedFilesInWorkspaceByTeams', () => { const user = newUser(); const teamIds = [v4(), v4()]; diff --git a/src/modules/sharing/sharing.service.ts b/src/modules/sharing/sharing.service.ts index d2e1d5df..a43507ec 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -1797,59 +1797,6 @@ export class SharingService { }; } - async getSharedFoldersInWorkspace( - user: User, - workspaceId: Workspace['id'], - teamId: Sharing['sharedWith'], - offset: number, - limit: number, - order: [string, string][], - ): Promise { - const foldersWithSharedInfo = - await this.sharingRepository.findFoldersByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, - user.uuid, - offset, - limit, - order, - ); - - const folders = (await Promise.all( - foldersWithSharedInfo.map(async (folderWithSharedInfo) => { - const avatar = folderWithSharedInfo.folder?.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, - user: { - ...folderWithSharedInfo.folder.user, - avatar: avatar - ? await this.usersUsecases.getAvatarUrl(avatar) - : null, - }, - }; - }), - )) as FolderWithSharedInfo[]; - - return { - folders: folders, - files: [], - credentials: { - networkPass: user.userId, - networkUser: user.bridgeUser, - }, - token: '', - role: 'OWNER', - }; - } - async getSharedFilesInWorkspaceByTeams( user: User, workspaceId: Workspace['id'], @@ -1941,57 +1888,6 @@ export class SharingService { }; } - async getSharedFilesInWorkspaces( - user: User, - workspaceId: Workspace['id'], - teamId: Sharing['sharedWith'], - offset: number, - limit: number, - order: [string, string][], - ): Promise { - const filesWithSharedInfo = - await this.sharingRepository.findFilesByOwnerAndSharedWithTeamInworkspace( - workspaceId, - teamId, - user.uuid, - offset, - limit, - order, - ); - - const files = (await Promise.all( - filesWithSharedInfo.map(async (fileWithSharedInfo) => { - const avatar = fileWithSharedInfo.file?.user?.avatar; - return { - ...fileWithSharedInfo.file, - plainName: - fileWithSharedInfo.file.plainName || - this.fileUsecases.decrypFileName(fileWithSharedInfo.file).plainName, - sharingId: fileWithSharedInfo.id, - encryptionKey: fileWithSharedInfo.encryptionKey, - dateShared: fileWithSharedInfo.createdAt, - user: { - ...fileWithSharedInfo.file.user, - avatar: avatar - ? await this.usersUsecases.getAvatarUrl(avatar) - : null, - }, - }; - }), - )) as FileWithSharedInfo[]; - - return { - folders: [], - files: files, - credentials: { - networkPass: user.userId, - networkUser: user.bridgeUser, - }, - token: '', - role: 'OWNER', - }; - } - async getItemSharedWith( user: User, itemId: Sharing['itemId'], 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 4d811246..337ffe77 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/workspaces.controller.spec.ts b/src/modules/workspaces/workspaces.controller.spec.ts index 9e31fa18..1da508d9 100644 --- a/src/modules/workspaces/workspaces.controller.spec.ts +++ b/src/modules/workspaces/workspaces.controller.spec.ts @@ -504,20 +504,19 @@ describe('Workspace Controller', () => { }); describe('GET /:workspaceId/shared/files', () => { - it('When files shared with user teams are requested, then it should call the service with the respective arguments', async () => { - const user = newUser(); - const workspaceId = v4(); - const orderBy = 'createdAt:ASC'; - const page = 1; - const perPage = 50; - const order = [['createdAt', 'ASC']]; + 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, user, orderBy, - page, - perPage, + { page, perPage }, ); expect(workspacesUsecases.getSharedFilesInWorkspace).toHaveBeenCalledWith( @@ -533,20 +532,20 @@ describe('Workspace Controller', () => { }); describe('GET /:workspaceId/shared/folders', () => { - it('When folders shared with user teams are requested, then it should call the service with the respective arguments', async () => { - const user = newUser(); - const workspaceId = v4(); - const orderBy = 'createdAt:ASC'; - const page = 1; - const perPage = 50; - const order = [['createdAt', 'ASC']]; + 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, + { page, perPage }, + ); expect( diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index e3f99b82..02e756d1 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -63,12 +63,11 @@ import { RequiredSharingPermissions } from '../sharing/guards/sharing-permission 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'; @ApiTags('Workspaces') @Controller('workspaces') @@ -590,16 +589,15 @@ export class WorkspacesController { workspaceId: 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.workspaceUseCases.getSharedFilesInWorkspace(user, workspaceId, { - offset: page, - limit: perPage, + offset: pagination.page, + limit: pagination.perPage, order, }); } @@ -618,8 +616,7 @@ export class WorkspacesController { workspaceId: 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]] @@ -629,8 +626,8 @@ export class WorkspacesController { user, workspaceId, { - offset: page, - limit: perPage, + offset: pagination.page, + limit: pagination.perPage, order, }, ); @@ -650,7 +647,7 @@ export class WorkspacesController { workspaceId: WorkspaceAttributes['id'], @UserDecorator() user: User, @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], - @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, + @Query() queryDto: GetSharedItemsDto, ) { const { orderBy, token, page, perPage } = queryDto; @@ -682,7 +679,7 @@ export class WorkspacesController { workspaceId: WorkspaceAttributes['id'], @UserDecorator() user: User, @Param('sharedFolderId', ValidateUUIDPipe) sharedFolderId: Folder['uuid'], - @Query() queryDto: GetItemsInsideSharedFolderDtoQuery, + @Query() queryDto: GetSharedItemsDto, ) { const { orderBy, token, page, perPage } = queryDto; From e154dc304c86dbe60667883968e4841e735a5fbc Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 26 Sep 2024 12:11:29 -0400 Subject: [PATCH 08/10] chore: fix some linting problems --- src/modules/workspaces/workspaces.controller.ts | 6 +----- src/modules/workspaces/workspaces.usecase.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 02e756d1..2adf4fd1 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -62,7 +62,6 @@ 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 { WorkspaceUserAttributes } from './attributes/workspace-users.attributes'; import { ChangeUserAssignedSpaceDto } from './dto/change-user-assigned-space.dto'; import { Public } from '../auth/decorators/public.decorator'; @@ -73,10 +72,7 @@ import { GetSharedItemsDto } from './dto/get-shared-items.dto'; @Controller('workspaces') @UseFilters(ExtendedHttpExceptionFilter) export class WorkspacesController { - constructor( - private workspaceUseCases: WorkspacesUsecases, - private sharingUseCases: SharingService, - ) {} + constructor(private readonly workspaceUseCases: WorkspacesUsecases) {} @Get('/') @ApiOperation({ diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 98d5de5a..be0f6e48 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -74,13 +74,13 @@ export class WorkspacesUsecases { private readonly workspaceRepository: SequelizeWorkspaceRepository, private readonly sharingUseCases: SharingService, private readonly paymentService: PaymentsService, - private networkService: BridgeService, - 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, ) {} From 3ee766f6db4da37b3a162c3425ec5615baab37a5 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 26 Sep 2024 12:29:05 -0400 Subject: [PATCH 09/10] fix: fixed workspace controller test --- .../workspaces/repositories/workspaces.repository.ts | 8 ++++---- src/modules/workspaces/workspaces.controller.spec.ts | 9 +-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/modules/workspaces/repositories/workspaces.repository.ts b/src/modules/workspaces/repositories/workspaces.repository.ts index 3d3d9b9a..f673eff2 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.ts @@ -28,13 +28,13 @@ 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); diff --git a/src/modules/workspaces/workspaces.controller.spec.ts b/src/modules/workspaces/workspaces.controller.spec.ts index 1da508d9..c849cb69 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', () => { @@ -545,7 +539,6 @@ describe('Workspace Controller', () => { user, orderBy, { page, perPage }, - ); expect( From 9b38c860e4eaedcbb0ffe1a37eef5323a5d8c541 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Sat, 28 Sep 2024 00:47:21 -0400 Subject: [PATCH 10/10] feat: added get shared-with endpoint for workspaces --- src/modules/sharing/sharing.controller.ts | 30 +-- src/modules/sharing/sharing.service.ts | 11 +- src/modules/workspaces/dto/shared-with.dto.ts | 21 ++ .../workspaces/workspaces.controller.ts | 27 +++ .../workspaces/workspaces.usecase.spec.ts | 192 +++++++++++++++++- src/modules/workspaces/workspaces.usecase.ts | 125 +++++++++++- test/fixtures.spec.ts | 17 ++ test/fixtures.ts | 10 + 8 files changed, 404 insertions(+), 29 deletions(-) create mode 100644 src/modules/workspaces/dto/shared-with.dto.ts diff --git a/src/modules/sharing/sharing.controller.ts b/src/modules/sharing/sharing.controller.ts index d95a113d..6bdf93bd 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.service.ts b/src/modules/sharing/sharing.service.ts index a43507ec..4a4aec90 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -174,7 +174,7 @@ export class PasswordNeededError extends ForbiddenException { } } -type SharingInfo = Pick< +export type SharingInfo = Pick< User, 'name' | 'lastname' | 'uuid' | 'avatar' | 'email' > & { @@ -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/shared-with.dto.ts b/src/modules/workspaces/dto/shared-with.dto.ts new file mode 100644 index 00000000..75b7738f --- /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/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 2adf4fd1..033ed429 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -67,6 +67,7 @@ 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') @@ -693,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 aa5f3a48..98d40718 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,7 +47,7 @@ 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'; @@ -4947,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 be0f6e48..a2ab6b6f 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -62,7 +62,7 @@ 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 { SharingAccessTokenData } from '../sharing/guards/sharings-token.interface'; @@ -1000,6 +1000,129 @@ 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'], diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index 47a28495..2677738e 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 57846cb6..f97023fb 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;