feat: added all feedback
This commit is contained in:
committed by
Andrew Bastin
parent
54bef30cf8
commit
b58acfe8dc
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user