feat: added all feedback

This commit is contained in:
Mir Arif Hasan
2023-07-11 16:58:17 +06:00
committed by Andrew Bastin
parent 54bef30cf8
commit b58acfe8dc
4 changed files with 54 additions and 21 deletions

View File

@@ -15,10 +15,7 @@ import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model'; import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
import { import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
TEAM_INVITE_NO_INVITE_FOUND,
USER_NOT_FOUND,
} from 'src/errors';
import { GqlUser } from 'src/decorators/gql-user.decorator'; import { GqlUser } from 'src/decorators/gql-user.decorator';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
@@ -34,6 +31,7 @@ import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler'; import { SkipThrottle } from '@nestjs/throttler';
import { AuthUser } from 'src/types/AuthUser';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => TeamInvitation) @Resolver(() => TeamInvitation)
@@ -78,7 +76,7 @@ export class TeamInvitationResolver {
}) })
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard) @UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
async teamInvitation( async teamInvitation(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args({ @Args({
name: 'inviteID', name: 'inviteID',
description: 'ID of the Team Invitation to lookup', description: 'ID of the Team Invitation to lookup',
@@ -100,7 +98,7 @@ export class TeamInvitationResolver {
@RequiresTeamRole(TeamMemberRole.OWNER) @RequiresTeamRole(TeamMemberRole.OWNER)
async createTeamInvitation( async createTeamInvitation(
@GqlUser() @GqlUser()
user: User, user: AuthUser,
@Args({ @Args({
name: 'teamID', name: 'teamID',
@@ -156,7 +154,7 @@ export class TeamInvitationResolver {
}) })
@UseGuards(GqlAuthGuard, TeamInviteeGuard) @UseGuards(GqlAuthGuard, TeamInviteeGuard)
async acceptTeamInvitation( async acceptTeamInvitation(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args({ @Args({
name: 'inviteID', name: 'inviteID',
type: () => ID, type: () => ID,

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import * as TE from 'fp-ts/TaskEither';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { TeamInvitation as DBTeamInvitation } from '@prisma/client'; import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
import { TeamMember, TeamMemberRole } from 'src/team/team.model'; import { TeamMember, TeamMemberRole } from 'src/team/team.model';
@@ -15,13 +14,13 @@ import {
TEAM_INVITE_MEMBER_HAS_INVITE, TEAM_INVITE_MEMBER_HAS_INVITE,
TEAM_INVITE_NO_INVITE_FOUND, TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND, TEAM_MEMBER_NOT_FOUND,
USER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { TeamInvitation } from './team-invitation.model'; import { TeamInvitation } from './team-invitation.model';
import { MailerService } from 'src/mailer/mailer.service'; import { MailerService } from 'src/mailer/mailer.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils'; import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser';
@Injectable() @Injectable()
export class TeamInvitationService { export class TeamInvitationService {
@@ -32,17 +31,25 @@ export class TeamInvitationService {
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
) { ) {}
this.getInvitation = this.getInvitation.bind(this);
}
castToTeamInvitation(dbTeamInvitation: DBTeamInvitation): TeamInvitation { /**
* Cast a DBTeamInvitation to a TeamInvitation
* @param dbTeamInvitation database TeamInvitation
* @returns TeamInvitation model
*/
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
return { return {
...dbTeamInvitation, ...dbTeamInvitation,
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole], inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
}; };
} }
/**
* Get the team invite
* @param inviteID invite id
* @returns an Option of team invitation or none
*/
async getInvitation( async getInvitation(
inviteID: string, inviteID: string,
): Promise<O.None | O.Some<TeamInvitation>> { ): Promise<O.None | O.Some<TeamInvitation>> {
@@ -53,7 +60,7 @@ export class TeamInvitationService {
}, },
}); });
return O.some(this.castToTeamInvitation(dbInvitation)); return O.some(this.cast(dbInvitation));
} catch (e) { } catch (e) {
return O.none; return O.none;
} }
@@ -85,8 +92,16 @@ export class TeamInvitationService {
} }
} }
/**
* Create a team invitation
* @param creator creator of the invitation
* @param teamID team id
* @param inviteeEmail invitee email
* @param inviteeRole invitee role
* @returns an Either of team invitation or error message
*/
async createInvitation( async createInvitation(
creator: User, creator: AuthUser,
teamID: string, teamID: string,
inviteeEmail: string, inviteeEmail: string,
inviteeRole: TeamMemberRole, inviteeRole: TeamMemberRole,
@@ -106,7 +121,7 @@ export class TeamInvitationService {
); );
if (!teamMemberCreator) return E.left(TEAM_MEMBER_NOT_FOUND); if (!teamMemberCreator) return E.left(TEAM_MEMBER_NOT_FOUND);
// invitee email should be a infra user // Checking to see if the invitee is already part of the team or not
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail); const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
if (O.isSome(inviteeUser)) { if (O.isSome(inviteeUser)) {
// invitee should not already a member // invitee should not already a member
@@ -143,12 +158,17 @@ export class TeamInvitationService {
}, },
}); });
const invitation = this.castToTeamInvitation(dbInvitation); const invitation = this.cast(dbInvitation);
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation); this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
return E.right(invitation); return E.right(invitation);
} }
/**
* Revoke a team invitation
* @param inviteID invite id
* @returns an Either of true or error message
*/
async revokeInvitation( async revokeInvitation(
inviteID: string, inviteID: string,
): Promise<E.Left<string> | E.Right<boolean>> { ): Promise<E.Left<string> | E.Right<boolean>> {
@@ -171,9 +191,15 @@ export class TeamInvitationService {
return E.right(true); return E.right(true);
} }
/**
* Accept a team invitation
* @param inviteID invite id
* @param acceptedBy user who accepted the invitation
* @returns an Either of team member or error message
*/
async acceptInvitation( async acceptInvitation(
inviteID: string, inviteID: string,
acceptedBy: User, acceptedBy: AuthUser,
): Promise<E.Left<string> | E.Right<TeamMember>> { ): Promise<E.Left<string> | E.Right<TeamMember>> {
// check if the invite exists // check if the invite exists
const invitation = await this.getInvitation(inviteID); const invitation = await this.getInvitation(inviteID);
@@ -224,7 +250,7 @@ export class TeamInvitationService {
}); });
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) => const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
this.castToTeamInvitation(dbInvitation), this.cast(dbInvitation),
); );
return invitations; return invitations;

View File

@@ -13,6 +13,9 @@ import {
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { TeamMemberRole } from 'src/team/team.model'; import { TeamMemberRole } from 'src/team/team.model';
/**
* This guard only allows team owner to execute the resolver
*/
@Injectable() @Injectable()
export class TeamInviteTeamOwnerGuard implements CanActivate { export class TeamInviteTeamOwnerGuard implements CanActivate {
constructor( constructor(

View File

@@ -11,6 +11,13 @@ import {
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { TeamService } from 'src/team/team.service'; import { TeamService } from 'src/team/team.service';
/**
* This guard only allows user to execute the resolver
* 1. If user is invitee, allow
* 2. Or else, if user is team member, allow
*
* TLDR: Allow if user is invitee or team member
*/
@Injectable() @Injectable()
export class TeamInviteViewerGuard implements CanActivate { export class TeamInviteViewerGuard implements CanActivate {
constructor( constructor(
@@ -33,8 +40,7 @@ export class TeamInviteViewerGuard implements CanActivate {
const invitation = await this.teamInviteService.getInvitation(inviteID); const invitation = await this.teamInviteService.getInvitation(inviteID);
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND); if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
// Check if the user and the invite email match, else if we can resolver the user as a team member // Check if the user and the invite email match, else if user is a team member
// any better solution ?
if ( if (
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase() user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
) { ) {