diff --git a/.env.template b/.env.template index e7d5b71dc..32e40e5be 100644 --- a/.env.template +++ b/.env.template @@ -42,4 +42,8 @@ STRIPE_SK_TEST=sk_test_y GATEWAY_USER=user GATEWAY_PASS=gatewaypass -PAYMENTS_API_URL=http://host.docker.internal:8003 \ No newline at end of file +PAYMENTS_API_URL=http://host.docker.internal:8003 + +#Workspaces +WORKSPACES_USER_INVITATION_EMAIL_ID=d-de1ed6df4a9947129c0bf592c808b58d +WORKSPACES_GUEST_USER_INVITATION_EMAIL_ID=d-41b4608fc94a41bca65aab7ed6ccad15 \ No newline at end of file diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 07e9dfb33..aa30889e5 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -99,6 +99,10 @@ export default () => ({ process.env.SENDGRID_TEMPLATE_DRIVE_UPDATE_USER_EMAIL || '', unblockAccountEmail: process.env.SENDGRID_TEMPLATE_DRIVE_UNBLOCK_ACCOUNT || '', + invitationToWorkspaceUser: + process.env.WORKSPACES_USER_INVITATION_EMAIL_ID || '', + invitationToWorkspaceGuestUser: + process.env.WORKSPACES_GUEST_USER_INVITATION_EMAIL_ID || '', }, }, newsletter: { diff --git a/src/externals/mailer/mailer.service.ts b/src/externals/mailer/mailer.service.ts index 38d672b7f..1c607a079 100644 --- a/src/externals/mailer/mailer.service.ts +++ b/src/externals/mailer/mailer.service.ts @@ -4,6 +4,7 @@ import sendgrid from '@sendgrid/mail'; import { User } from '../../modules/user/user.domain'; import { Folder } from '../../modules/folder/folder.domain'; import { File } from '../../modules/file/file.domain'; +import { Workspace } from '../../modules/workspaces/domains/workspaces.domain'; type SendInvitationToSharingContext = { notification_message: string; @@ -116,6 +117,62 @@ export class MailerService { ); } + async sendWorkspaceUserInvitation( + senderName: User['name'], + invitedUserEmail: User['email'], + workspaceName: Workspace['name'], + mailInfo: { + acceptUrl: string; + declineUrl: string; + }, + avatar?: { + pictureUrl: string; + initials: string; + }, + ): Promise { + const context = { + sender_name: senderName, + workspace_name: workspaceName, + avatar: { + picture_url: avatar.pictureUrl, + initials: avatar.initials, + }, + signup_url: mailInfo.acceptUrl, + decline_url: mailInfo.declineUrl, + }; + await this.send( + invitedUserEmail, + this.configService.get('mailer.templates.invitationToWorkspaceUser'), + context, + ); + } + + async sendWorkspaceUserExternalInvitation( + senderName: User['name'], + invitedUserEmail: User['email'], + workspaceName: Workspace['name'], + signUpUrl: string, + avatar?: { + pictureUrl: string; + initials: string; + }, + ): Promise { + const context = { + sender_name: senderName, + workspace_name: workspaceName, + avatar: { + picture_url: avatar.pictureUrl, + initials: avatar.initials, + }, + signup_url: signUpUrl, + }; + await this.send( + invitedUserEmail, + this.configService.get('mailer.templates.invitationToWorkspaceGuestUser'), + context, + ); + } + async sendRemovedFromSharingEmail( userRemovedFromSharingEmail: User['email'], itemName: File['plainName'] | Folder['plainName'], diff --git a/src/modules/workspaces/repositories/workspaces.repository.ts b/src/modules/workspaces/repositories/workspaces.repository.ts index a5e6ef1cf..60f691262 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.ts @@ -9,6 +9,7 @@ import { WorkspaceUser } from '../domains/workspace-user.domain'; import { WorkspaceInviteModel } from '../models/workspace-invite.model'; import { WorkspaceInvite } from '../domains/workspace-invite.domain'; import { WorkspaceInviteAttributes } from '../attributes/workspace-invite.attribute'; +import { WorkspaceUserAttributes } from '../attributes/workspace-users.attributes'; export interface WorkspaceRepository { findById(id: WorkspaceAttributes['id']): Promise; @@ -85,6 +86,35 @@ export class SequelizeWorkspaceRepository implements WorkspaceRepository { return invite ? WorkspaceInvite.build(invite) : null; } + async getWorkspaceInvitationsCount( + workspaceId: WorkspaceAttributes['id'], + ): Promise { + const totalInvites = await this.modelWorkspaceInvite.count({ + where: { workspaceId: workspaceId }, + }); + + return totalInvites; + } + + async findWorkspaceUser( + where: Partial, + ): Promise { + const workspaceUser = await this.modelWorkspaceUser.findOne({ + where, + }); + + return workspaceUser ? this.workspaceUserToDomain(workspaceUser) : null; + } + + async getWorkspaceUsersCount( + workspaceId: WorkspaceAttributes['id'], + ): Promise { + const totalUsers = await this.modelWorkspaceUser.count({ + where: { workspaceId: workspaceId }, + }); + + return totalUsers; + } async getSpaceLimitInInvitations( workspaceId: WorkspaceAttributes['id'], ): Promise { diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 8abe9461a..7793a6dee 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -44,8 +44,10 @@ export class WorkspacesController { async inviteUsersToWorkspace( @Param('workspaceId') workspaceId: WorkspaceAttributes['id'], @Body() createInviteDto: CreateWorkspaceInviteDto, + @UserDecorator() user: User, ) { return this.workspaceUseCases.inviteUserToWorkspace( + user, workspaceId, createInviteDto, ); diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 62f385c88..9e8c53a5d 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable, + Logger, NotFoundException, } from '@nestjs/common'; import { User } from '../user/user.domain'; @@ -20,6 +21,9 @@ import { WorkspaceTeamUser } from './domains/workspace-team-user.domain'; import { UserUseCases } from '../user/user.usecase'; import { WorkspaceInvite } from './domains/workspace-invite.domain'; import { CreateWorkspaceInviteDto } from './dto/create-workspace-invite.dto'; +import { MailerService } from '../../externals/mailer/mailer.service'; +import { ConfigService } from '@nestjs/config'; +import { Sign } from '../../middlewares/passport'; @Injectable() export class WorkspacesUsecases { @@ -29,6 +33,7 @@ export class WorkspacesUsecases { private networkService: BridgeService, private userRepository: SequelizeUserRepository, private userUsecases: UserUseCases, + private configService: ConfigService, ) {} async initiateWorkspace( @@ -97,6 +102,7 @@ export class WorkspacesUsecases { } async inviteUserToWorkspace( + user: User, workspaceId: Workspace['id'], createInviteDto: CreateWorkspaceInviteDto, ) { @@ -117,6 +123,19 @@ export class WorkspacesUsecases { throw new NotFoundException('Invited user not found'); } + const isUserPreCreated = !existentUser; + + if (!isUserPreCreated) { + const isUserAlreadyInWorkspace = + await this.workspaceRepository.findWorkspaceUser({ + workspaceId, + memberId: userJoining.uuid, + }); + if (isUserAlreadyInWorkspace) { + throw new BadRequestException('User is already part of the workspace'); + } + } + const invitation = await this.workspaceRepository.findInvite({ invitedUser: userJoining.uuid, workspaceId, @@ -126,22 +145,30 @@ export class WorkspacesUsecases { throw new BadRequestException('User is already invited to workspace'); } + const isWorkspaceFull = await this.isWorkspaceFull(workspaceId); + + if (isWorkspaceFull) { + throw new BadRequestException( + 'You can not invite more users to this workspace', + ); + } + const workspaceUser = await this.userRepository.findByUuid( workspace.workspaceUserId, ); - const spaceLimit = await this.networkService.getLimit( - workspaceUser.bridgeUser, - workspaceUser.userId, - ); - - const totalSpaceLimitAssigned = - await this.workspaceRepository.getTotalSpaceLimitInWorkspaceUsers( - workspace.id, - ); - - const totalSpaceAssignedInInvitations = - await this.workspaceRepository.getSpaceLimitInInvitations(workspaceId); + const [ + spaceLimit, + totalSpaceLimitAssigned, + totalSpaceAssignedInInvitations, + ] = await Promise.all([ + this.networkService.getLimit( + workspaceUser.bridgeUser, + workspaceUser.userId, + ), + this.workspaceRepository.getTotalSpaceLimitInWorkspaceUsers(workspace.id), + this.workspaceRepository.getSpaceLimitInInvitations(workspaceId), + ]); const spaceLeft = BigInt(spaceLimit) - @@ -154,7 +181,7 @@ export class WorkspacesUsecases { ); } - const invite = WorkspaceInvite.build({ + const newInvite = WorkspaceInvite.build({ id: v4(), workspaceId: workspaceId, invitedUser: userJoining.uuid, @@ -165,8 +192,75 @@ export class WorkspacesUsecases { updatedAt: new Date(), }); - await this.workspaceRepository.createInvite(invite); - return invite.toJSON(); + await this.workspaceRepository.createInvite(newInvite); + const inviterName = `${user.name} ${user.lastname}`; + + if (isUserPreCreated) { + const encodedUserEmail = encodeURIComponent(userJoining.email); + try { + await new MailerService( + this.configService, + ).sendWorkspaceUserExternalInvitation( + inviterName, + userJoining.email, + workspace.name, + `${this.configService.get( + 'clients.drive.web', + )}/workspace-guest?invitation=${ + newInvite.id + }&email=${encodedUserEmail}`, + { initials: user.name[0] + user.lastname[0], pictureUrl: null }, + ); + } catch (error) { + Logger.error( + `[WORKSPACE/GUESTUSEREMAIL] Error sending email pre created userId: ${ + userJoining.uuid + }, error: ${JSON.stringify(error)}`, + ); + throw error; + } + } else { + try { + const authToken = Sign( + this.userUsecases.getNewTokenPayload(userJoining), + this.configService.get('secrets.jwt'), + ); + await new MailerService(this.configService).sendWorkspaceUserInvitation( + inviterName, + userJoining.email, + workspace.name, + { + acceptUrl: `${this.configService.get( + 'clients.drive.web', + )}/workspaces/${newInvite.id}/accept?token=${authToken}`, + declineUrl: `${this.configService.get( + 'clients.drive.web', + )}/workspaces/${newInvite.id}/decline?token=${authToken}`, + }, + { initials: user.name[0] + user.lastname[0], pictureUrl: null }, + ); + } catch (error) { + Logger.error( + `[WORKSPACE/USEREMAIL] Error sending email invitation to existent user userId: ${ + userJoining.uuid + }, error: ${JSON.stringify(error)}`, + ); + } + } + + return newInvite.toJSON(); + } + + async isWorkspaceFull(workspaceId: Workspace['id']): Promise { + const [workspaceUsersCount, workspaceInvitationsCount] = await Promise.all([ + this.workspaceRepository.getWorkspaceUsersCount(workspaceId), + this.workspaceRepository.getWorkspaceInvitationsCount(workspaceId), + ]); + + const limit = 10; // Temporal limit + const count = workspaceUsersCount + workspaceInvitationsCount; + + return count >= limit; } async createTeam(