Skip to content

Commit

Permalink
feat: invite users to workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
apsantiso committed Mar 20, 2024
1 parent 292b754 commit 75e87c1
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 16 deletions.
6 changes: 5 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ STRIPE_SK_TEST=sk_test_y
GATEWAY_USER=user
GATEWAY_PASS=gatewaypass

PAYMENTS_API_URL=http://host.docker.internal:8003
PAYMENTS_API_URL=http://host.docker.internal:8003

#Workspaces
WORKSPACES_USER_INVITATION_EMAIL_ID=d-de1ed6df4a9947129c0bf592c808b58d
WORKSPACES_GUEST_USER_INVITATION_EMAIL_ID=d-41b4608fc94a41bca65aab7ed6ccad15
4 changes: 4 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
57 changes: 57 additions & 0 deletions src/externals/mailer/mailer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
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<void> {
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'],
Expand Down
30 changes: 30 additions & 0 deletions src/modules/workspaces/repositories/workspaces.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Workspace | null>;
Expand Down Expand Up @@ -85,6 +86,35 @@ export class SequelizeWorkspaceRepository implements WorkspaceRepository {
return invite ? WorkspaceInvite.build(invite) : null;
}

async getWorkspaceInvitationsCount(
workspaceId: WorkspaceAttributes['id'],
): Promise<number> {
const totalInvites = await this.modelWorkspaceInvite.count({
where: { workspaceId: workspaceId },
});

return totalInvites;
}

async findWorkspaceUser(
where: Partial<WorkspaceUserAttributes>,
): Promise<WorkspaceUser> {
const workspaceUser = await this.modelWorkspaceUser.findOne({
where,
});

return workspaceUser ? this.workspaceUserToDomain(workspaceUser) : null;
}

async getWorkspaceUsersCount(
workspaceId: WorkspaceAttributes['id'],
): Promise<number> {
const totalUsers = await this.modelWorkspaceUser.count({
where: { workspaceId: workspaceId },
});

return totalUsers;
}
async getSpaceLimitInInvitations(
workspaceId: WorkspaceAttributes['id'],
): Promise<bigint> {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/workspaces/workspaces.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
124 changes: 109 additions & 15 deletions src/modules/workspaces/workspaces.usecase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { User } from '../user/user.domain';
Expand All @@ -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 {
Expand All @@ -29,6 +33,7 @@ export class WorkspacesUsecases {
private networkService: BridgeService,
private userRepository: SequelizeUserRepository,
private userUsecases: UserUseCases,
private configService: ConfigService,
) {}

async initiateWorkspace(
Expand Down Expand Up @@ -97,6 +102,7 @@ export class WorkspacesUsecases {
}

async inviteUserToWorkspace(
user: User,
workspaceId: Workspace['id'],
createInviteDto: CreateWorkspaceInviteDto,
) {
Expand All @@ -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,
Expand All @@ -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) -
Expand All @@ -154,7 +181,7 @@ export class WorkspacesUsecases {
);
}

const invite = WorkspaceInvite.build({
const newInvite = WorkspaceInvite.build({
id: v4(),
workspaceId: workspaceId,
invitedUser: userJoining.uuid,
Expand All @@ -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<boolean> {
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(
Expand Down

0 comments on commit 75e87c1

Please sign in to comment.