Compare commits
5 Commits
refactor/w
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
089f6823e6 | ||
|
|
1d93745a4e | ||
|
|
e0cc143436 | ||
|
|
b58acfe8dc | ||
|
|
54bef30cf8 |
@@ -181,7 +181,7 @@ export class AdminService {
|
|||||||
* @returns an array team invitations
|
* @returns an array team invitations
|
||||||
*/
|
*/
|
||||||
async pendingInvitationCountInTeam(teamID: string) {
|
async pendingInvitationCountInTeam(teamID: string) {
|
||||||
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
const invitations = await this.teamInvitationService.getTeamInvitations(
|
||||||
teamID,
|
teamID,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ export class AdminService {
|
|||||||
if (E.isRight(userInvitation)) {
|
if (E.isRight(userInvitation)) {
|
||||||
await this.teamInvitationService.revokeInvitation(
|
await this.teamInvitationService.revokeInvitation(
|
||||||
userInvitation.right.id,
|
userInvitation.right.id,
|
||||||
)();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return E.right(addedUser.right);
|
return E.right(addedUser.right);
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export class AuthService {
|
|||||||
url = process.env.VITE_BASE_URL;
|
url = process.env.VITE_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendAuthEmail(email, {
|
await this.mailerService.sendEmail(email, {
|
||||||
template: 'code-your-own',
|
template: 'code-your-own',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
UserMagicLinkMailDescription,
|
UserMagicLinkMailDescription,
|
||||||
} from './MailDescriptions';
|
} from './MailDescriptions';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { EMAIL_FAILED } from 'src/errors';
|
import { EMAIL_FAILED } from 'src/errors';
|
||||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||||
|
|
||||||
@@ -35,33 +34,14 @@ export class MailerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an email to the given email address given a mail description
|
* Sends an email to the given email address given a mail description
|
||||||
* @param to The email address to be sent to (NOTE: this is not validated)
|
* @param to Receiver's email id
|
||||||
* @param mailDesc Definition of what email to be sent
|
* @param mailDesc Definition of what email to be sent
|
||||||
|
* @returns Response if email was send successfully or not
|
||||||
*/
|
*/
|
||||||
sendMail(
|
async sendEmail(
|
||||||
to: string,
|
to: string,
|
||||||
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
||||||
) {
|
) {
|
||||||
return TE.tryCatch(
|
|
||||||
async () => {
|
|
||||||
await this.nestMailerService.sendMail({
|
|
||||||
to,
|
|
||||||
template: mailDesc.template,
|
|
||||||
subject: this.resolveSubjectForMailDesc(mailDesc),
|
|
||||||
context: mailDesc.variables,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => EMAIL_FAILED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param to Receiver's email id
|
|
||||||
* @param mailDesc Details of email to be sent for Magic-Link auth
|
|
||||||
* @returns Response if email was send successfully or not
|
|
||||||
*/
|
|
||||||
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
|
|
||||||
try {
|
try {
|
||||||
await this.nestMailerService.sendMail({
|
await this.nestMailerService.sendMail({
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||||
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class CreateTeamInvitationArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team ID to invite from',
|
||||||
|
})
|
||||||
|
teamID: string;
|
||||||
|
|
||||||
|
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
|
||||||
|
inviteeEmail: string;
|
||||||
|
|
||||||
|
@Field(() => TeamMemberRole, {
|
||||||
|
name: 'inviteeRole',
|
||||||
|
description: 'Role to be given to the user',
|
||||||
|
})
|
||||||
|
inviteeRole: TeamMemberRole;
|
||||||
|
}
|
||||||
@@ -12,15 +12,10 @@ import { TeamInvitation } from './team-invitation.model';
|
|||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe } from 'fp-ts/function';
|
import { pipe } from 'fp-ts/function';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
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 { EmailCodec } from 'src/types/Email';
|
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||||
import {
|
|
||||||
INVALID_EMAIL,
|
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
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';
|
||||||
@@ -36,6 +31,8 @@ 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';
|
||||||
|
import { CreateTeamInvitationArgs } from './input-type.args';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => TeamInvitation)
|
@Resolver(() => TeamInvitation)
|
||||||
@@ -79,8 +76,8 @@ export class TeamInvitationResolver {
|
|||||||
'Gets the Team Invitation with the given ID, or null if not exists',
|
'Gets the Team Invitation with the given ID, or null if not exists',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
||||||
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',
|
||||||
@@ -88,17 +85,11 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
return pipe(
|
const teamInvitation = await this.teamInvitationService.getInvitation(
|
||||||
this.teamInvitationService.getInvitation(inviteID),
|
inviteID,
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
);
|
||||||
TE.chainW(
|
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
TE.fromPredicate(
|
return teamInvitation.value;
|
||||||
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.getOrElse(throwErr),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamInvitation, {
|
@Mutation(() => TeamInvitation, {
|
||||||
@@ -106,56 +97,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
createTeamInvitation(
|
async createTeamInvitation(
|
||||||
@GqlUser()
|
@GqlUser() user: AuthUser,
|
||||||
user: User,
|
@Args() args: CreateTeamInvitationArgs,
|
||||||
|
|
||||||
@Args({
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team ID to invite from',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
teamID: string,
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeEmail',
|
|
||||||
description: 'Email of the user to invite',
|
|
||||||
})
|
|
||||||
inviteeEmail: string,
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeRole',
|
|
||||||
type: () => TeamMemberRole,
|
|
||||||
description: 'Role to be given to the user',
|
|
||||||
})
|
|
||||||
inviteeRole: TeamMemberRole,
|
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
return pipe(
|
const teamInvitation = await this.teamInvitationService.createInvitation(
|
||||||
TE.Do,
|
user,
|
||||||
|
args.teamID,
|
||||||
|
args.inviteeEmail,
|
||||||
|
args.inviteeRole,
|
||||||
|
);
|
||||||
|
|
||||||
// Validate email
|
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
|
||||||
TE.bindW('email', () =>
|
return teamInvitation.right;
|
||||||
pipe(
|
|
||||||
EmailCodec.decode(inviteeEmail),
|
|
||||||
TE.fromEither,
|
|
||||||
TE.mapLeft(() => INVALID_EMAIL),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validate and get Team
|
|
||||||
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
|
|
||||||
|
|
||||||
// Create team
|
|
||||||
TE.chainW(({ email, team }) =>
|
|
||||||
this.teamInvitationService.createInvitation(
|
|
||||||
user,
|
|
||||||
team,
|
|
||||||
email,
|
|
||||||
inviteeRole,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// If failed, throw err (so the message is passed) else return value
|
|
||||||
TE.getOrElse(throwErr),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -163,7 +117,7 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
revokeTeamInvitation(
|
async revokeTeamInvitation(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -171,19 +125,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<true> {
|
): Promise<true> {
|
||||||
return pipe(
|
const isRevoked = await this.teamInvitationService.revokeInvitation(
|
||||||
this.teamInvitationService.revokeInvitation(inviteID),
|
inviteID,
|
||||||
TE.map(() => true as const),
|
);
|
||||||
TE.getOrElse(throwErr),
|
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
|
||||||
)();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamMember, {
|
@Mutation(() => TeamMember, {
|
||||||
description: 'Accept an Invitation',
|
description: 'Accept an Invitation',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
||||||
acceptTeamInvitation(
|
async acceptTeamInvitation(
|
||||||
@GqlUser() user: User,
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -191,10 +145,12 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMember> {
|
||||||
return pipe(
|
const teamMember = await this.teamInvitationService.acceptInvitation(
|
||||||
this.teamInvitationService.acceptInvitation(inviteID, user),
|
inviteID,
|
||||||
TE.getOrElse(throwErr),
|
user,
|
||||||
)();
|
);
|
||||||
|
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||||
|
return teamMember.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as TO from 'fp-ts/TaskOption';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { pipe, flow, constVoid } from 'fp-ts/function';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { Team, TeamMemberRole } from 'src/team/team.model';
|
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
|
||||||
import { Email } from 'src/types/Email';
|
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import {
|
import {
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
|
TEAM_INVALID_ID,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_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,38 +30,37 @@ 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);
|
|
||||||
|
/**
|
||||||
|
* Cast a DBTeamInvitation to a TeamInvitation
|
||||||
|
* @param dbTeamInvitation database TeamInvitation
|
||||||
|
* @returns TeamInvitation model
|
||||||
|
*/
|
||||||
|
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
|
||||||
|
return {
|
||||||
|
...dbTeamInvitation,
|
||||||
|
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
|
/**
|
||||||
return pipe(
|
* Get the team invite
|
||||||
() =>
|
* @param inviteID invite id
|
||||||
this.prisma.teamInvitation.findUnique({
|
* @returns an Option of team invitation or none
|
||||||
where: {
|
*/
|
||||||
id: inviteID,
|
async getInvitation(inviteID: string) {
|
||||||
},
|
try {
|
||||||
}),
|
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||||
TO.fromTask,
|
where: {
|
||||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
id: inviteID,
|
||||||
TO.map((x) => x as TeamInvitation),
|
},
|
||||||
);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
getInvitationWithEmail(email: Email, team: Team) {
|
return O.some(this.cast(dbInvitation));
|
||||||
return pipe(
|
} catch (e) {
|
||||||
() =>
|
return O.none;
|
||||||
this.prisma.teamInvitation.findUnique({
|
}
|
||||||
where: {
|
|
||||||
teamID_inviteeEmail: {
|
|
||||||
inviteeEmail: email,
|
|
||||||
teamID: team.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
TO.fromTask,
|
|
||||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,213 +89,162 @@ export class TeamInvitationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createInvitation(
|
/**
|
||||||
creator: User,
|
* Create a team invitation
|
||||||
team: Team,
|
* @param creator creator of the invitation
|
||||||
inviteeEmail: Email,
|
* @param teamID team id
|
||||||
|
* @param inviteeEmail invitee email
|
||||||
|
* @param inviteeRole invitee role
|
||||||
|
* @returns an Either of team invitation or error message
|
||||||
|
*/
|
||||||
|
async createInvitation(
|
||||||
|
creator: AuthUser,
|
||||||
|
teamID: string,
|
||||||
|
inviteeEmail: string,
|
||||||
inviteeRole: TeamMemberRole,
|
inviteeRole: TeamMemberRole,
|
||||||
) {
|
) {
|
||||||
return pipe(
|
// validate email
|
||||||
// Perform all validation checks
|
const isEmailValid = validateEmail(inviteeEmail);
|
||||||
TE.sequenceArray([
|
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||||
// creator should be a TeamMember
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(team.id, creator.uid),
|
|
||||||
TE.map(constVoid),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Invitee should not be a team member
|
// team ID should valid
|
||||||
pipe(
|
const team = await this.teamService.getTeamWithID(teamID);
|
||||||
async () => await this.userService.findUserByEmail(inviteeEmail),
|
if (!team) return E.left(TEAM_INVALID_ID);
|
||||||
TO.foldW(
|
|
||||||
() => TE.right(undefined), // If no user, short circuit to completion
|
|
||||||
(user) =>
|
|
||||||
pipe(
|
|
||||||
// If user is found, check if team member
|
|
||||||
this.teamService.getTeamMemberTE(team.id, user.uid),
|
|
||||||
TE.foldW(
|
|
||||||
() => TE.right(undefined), // Not team-member, this is good
|
|
||||||
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(constVoid),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Should not have an existing invite
|
// invitation creator should be a TeamMember
|
||||||
pipe(
|
const isTeamMember = await this.teamService.getTeamMember(
|
||||||
this.getInvitationWithEmail(inviteeEmail, team),
|
team.id,
|
||||||
TE.fromTaskOption(() => null),
|
creator.uid,
|
||||||
TE.swap,
|
|
||||||
TE.map(constVoid),
|
|
||||||
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// Create the invitation
|
|
||||||
TE.chainTaskK(
|
|
||||||
() => () =>
|
|
||||||
this.prisma.teamInvitation.create({
|
|
||||||
data: {
|
|
||||||
teamID: team.id,
|
|
||||||
inviteeEmail,
|
|
||||||
inviteeRole,
|
|
||||||
creatorUid: creator.uid,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Send email, this is a side effect
|
|
||||||
TE.chainFirstTaskK((invitation) =>
|
|
||||||
pipe(
|
|
||||||
this.mailerService.sendMail(inviteeEmail, {
|
|
||||||
template: 'team-invitation',
|
|
||||||
variables: {
|
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
|
||||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
|
|
||||||
invite_team_name: team.name,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Send PubSub topic
|
|
||||||
TE.chainFirstTaskK((invitation) =>
|
|
||||||
TE.fromTask(async () => {
|
|
||||||
const inv: TeamInvitation = {
|
|
||||||
id: invitation.id,
|
|
||||||
teamID: invitation.teamID,
|
|
||||||
creatorUid: invitation.creatorUid,
|
|
||||||
inviteeEmail: invitation.inviteeEmail,
|
|
||||||
inviteeRole: TeamMemberRole[invitation.inviteeRole],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Map to model type
|
|
||||||
TE.map((x) => x as TeamInvitation),
|
|
||||||
);
|
);
|
||||||
}
|
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
|
||||||
revokeInvitation(inviteID: string) {
|
// Checking to see if the invitee is already part of the team or not
|
||||||
return pipe(
|
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
|
||||||
// Make sure invite exists
|
if (O.isSome(inviteeUser)) {
|
||||||
this.getInvitation(inviteID),
|
// invitee should not already a member
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
const isTeamMember = await this.teamService.getTeamMember(
|
||||||
|
team.id,
|
||||||
|
inviteeUser.value.uid,
|
||||||
|
);
|
||||||
|
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete team invitation
|
// check invitee already invited earlier or not
|
||||||
TE.chainTaskK(
|
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
|
||||||
() => () =>
|
inviteeEmail,
|
||||||
this.prisma.teamInvitation.delete({
|
team.id,
|
||||||
where: {
|
|
||||||
id: inviteID,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Emit Pubsub Event
|
|
||||||
TE.chainFirst((invitation) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team/${invitation.teamID}/invite_removed`,
|
|
||||||
invitation.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// We are not returning anything
|
|
||||||
TE.map(constVoid),
|
|
||||||
);
|
);
|
||||||
}
|
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
|
||||||
|
|
||||||
getAllInvitationsInTeam(team: Team) {
|
// create the invitation
|
||||||
return pipe(
|
const dbInvitation = await this.prisma.teamInvitation.create({
|
||||||
() =>
|
data: {
|
||||||
this.prisma.teamInvitation.findMany({
|
teamID: team.id,
|
||||||
where: {
|
inviteeEmail,
|
||||||
teamID: team.id,
|
inviteeRole,
|
||||||
},
|
creatorUid: creator.uid,
|
||||||
}),
|
},
|
||||||
T.map((x) => x as TeamInvitation[]),
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptInvitation(inviteID: string, acceptedBy: User) {
|
await this.mailerService.sendEmail(inviteeEmail, {
|
||||||
return pipe(
|
template: 'team-invitation',
|
||||||
TE.Do,
|
variables: {
|
||||||
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
|
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||||
|
invite_team_name: team.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// First get the invitation
|
const invitation = this.cast(dbInvitation);
|
||||||
TE.bindW('invitation', () =>
|
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
|
||||||
pipe(
|
|
||||||
this.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validation checks
|
return E.right(invitation);
|
||||||
TE.chainFirstW(({ invitation }) =>
|
|
||||||
TE.sequenceArray([
|
|
||||||
// Make sure the invited user is not part of the team
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
|
|
||||||
TE.swap,
|
|
||||||
TE.bimap(
|
|
||||||
() => TEAM_INVITE_ALREADY_MEMBER,
|
|
||||||
constVoid, // The return type is ignored
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Make sure the invited user and accepting user has the same email
|
|
||||||
pipe(
|
|
||||||
undefined,
|
|
||||||
TE.fromPredicate(
|
|
||||||
() =>
|
|
||||||
acceptedBy.email.toLowerCase() ===
|
|
||||||
invitation.inviteeEmail.toLowerCase(),
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Add the team member
|
|
||||||
// TODO: Somehow bring subscriptions to this ?
|
|
||||||
TE.bindW('teamMember', ({ invitation }) =>
|
|
||||||
pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.teamService.addMemberToTeam(
|
|
||||||
invitation.teamID,
|
|
||||||
acceptedBy.uid,
|
|
||||||
invitation.inviteeRole,
|
|
||||||
),
|
|
||||||
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
|
|
||||||
|
|
||||||
TE.map(({ teamMember }) => teamMember),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the count invitations for a given team.
|
* Revoke a team invitation
|
||||||
* @param teamID team id
|
* @param inviteID invite id
|
||||||
* @returns a count team invitations for a team
|
* @returns an Either of true or error message
|
||||||
*/
|
*/
|
||||||
async getAllTeamInvitations(teamID: string) {
|
async revokeInvitation(inviteID: string) {
|
||||||
const invitations = await this.prisma.teamInvitation.findMany({
|
// check if the invite exists
|
||||||
|
const invitation = await this.getInvitation(inviteID);
|
||||||
|
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
|
||||||
|
// delete the invite
|
||||||
|
await this.prisma.teamInvitation.delete({
|
||||||
|
where: {
|
||||||
|
id: inviteID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team/${invitation.value.teamID}/invite_removed`,
|
||||||
|
invitation.value.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(inviteID: string, acceptedBy: AuthUser) {
|
||||||
|
// check if the invite exists
|
||||||
|
const invitation = await this.getInvitation(inviteID);
|
||||||
|
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
|
||||||
|
// make sure the user is not already a member of the team
|
||||||
|
const teamMemberInvitee = await this.teamService.getTeamMember(
|
||||||
|
invitation.value.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
);
|
||||||
|
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
|
||||||
|
// make sure the user is the same as the invitee
|
||||||
|
if (
|
||||||
|
acceptedBy.email.toLowerCase() !==
|
||||||
|
invitation.value.inviteeEmail.toLowerCase()
|
||||||
|
)
|
||||||
|
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||||
|
|
||||||
|
// add the user to the team
|
||||||
|
let teamMember: TeamMember;
|
||||||
|
try {
|
||||||
|
teamMember = await this.teamService.addMemberToTeam(
|
||||||
|
invitation.value.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
invitation.value.inviteeRole,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the invite
|
||||||
|
await this.revokeInvitation(inviteID);
|
||||||
|
|
||||||
|
return E.right(teamMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all team invitations for a given team.
|
||||||
|
* @param teamID team id
|
||||||
|
* @returns array of team invitations for a team
|
||||||
|
*/
|
||||||
|
async getTeamInvitations(teamID: string) {
|
||||||
|
const dbInvitations = await this.prisma.teamInvitation.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
|
||||||
|
this.cast(dbInvitation),
|
||||||
|
);
|
||||||
|
|
||||||
return invitations;
|
return invitations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { pipe } from 'fp-ts/function';
|
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
TEAM_NOT_REQUIRED_ROLE,
|
TEAM_NOT_REQUIRED_ROLE,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
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(
|
||||||
@@ -24,48 +24,30 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
// Get user
|
||||||
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get the invite
|
// Get the invite
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW((inviteID) =>
|
|
||||||
pipe(
|
|
||||||
this.teamInviteService.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
pipe(
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
gqlCtx.getContext().req.user,
|
|
||||||
O.fromNullable,
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('userMember', ({ invite, user }) =>
|
// Fetch team member details of this user
|
||||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
),
|
invitation.value.teamID,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
TE.chainW(
|
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||||
TE.fromPredicate(
|
if (teamMember.role !== TeamMemberRole.OWNER)
|
||||||
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
|
throwErr(TEAM_NOT_REQUIRED_ROLE);
|
||||||
() => TEAM_NOT_REQUIRED_ROLE,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.fold(
|
return true;
|
||||||
(err) => throwErr(err),
|
|
||||||
() => T.of(true),
|
|
||||||
),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe, flow } from 'fp-ts/function';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NOT_VALID_VIEWER,
|
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
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(
|
||||||
@@ -23,50 +26,32 @@ export class TeamInviteViewerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
// Get GQL Context
|
// Get user
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get user
|
// Get the invite
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getContext().req.user),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Get the invite
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
pipe(
|
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW(
|
|
||||||
flow(
|
|
||||||
this.teamInviteService.getInvitation,
|
|
||||||
TE.fromTaskOption(() => 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 (
|
||||||
TE.chainW(({ user, invite }) =>
|
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||||
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
|
) {
|
||||||
? TE.of(true)
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
: pipe(
|
invitation.value.teamID,
|
||||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
user.uid,
|
||||||
TE.map(() => true),
|
);
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.mapLeft((e) =>
|
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||||
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
|
}
|
||||||
),
|
|
||||||
|
|
||||||
TE.fold(throwErr, () => T.of(true)),
|
return true;
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe, flow } from 'fp-ts/function';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
@@ -23,45 +20,26 @@ export class TeamInviteeGuard implements CanActivate {
|
|||||||
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL Context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
// Get execution context
|
// Get user
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get user
|
// Get the invite
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getContext().req.user),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Get invite
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
pipe(
|
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW(
|
|
||||||
flow(
|
|
||||||
this.teamInviteService.getInvitation,
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Check if the emails match
|
if (
|
||||||
TE.chainW(
|
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||||
TE.fromPredicate(
|
) {
|
||||||
({ user, invite }) =>
|
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||||
user.email.toLowerCase() === invite.inviteeEmail.toLowerCase(),
|
}
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Fold it to a promise
|
return true;
|
||||||
TE.fold(throwErr, () => T.of(true)),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
|
|||||||
complexity: 10,
|
complexity: 10,
|
||||||
})
|
})
|
||||||
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
||||||
return this.teamInviteService.getAllInvitationsInTeam(team)();
|
return this.teamInviteService.getTeamInvitations(team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user