diff --git a/packages/hoppscotch-backend/src/admin/admin.resolver.ts b/packages/hoppscotch-backend/src/admin/admin.resolver.ts index deeb90dbe..1ead2f4b8 100644 --- a/packages/hoppscotch-backend/src/admin/admin.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/admin.resolver.ts @@ -269,6 +269,25 @@ export class AdminResolver { return invitedUser.right; } + @Mutation(() => Boolean, { description: 'Revoke a user invite by ID' }) + @UseGuards(GqlAuthGuard, GqlAdminGuard) + async revokeUserInviteByAdmin( + @GqlAdmin() adminUser: Admin, + @Args({ + name: 'inviteeEmail', + description: 'Invite Email', + type: () => ID, + }) + inviteeEmail: string, + ): Promise { + const invite = await this.adminService.revokeUserInvite( + inviteeEmail, + adminUser, + ); + if (E.isLeft(invite)) throwErr(invite.left); + return invite.right; + } + @Mutation(() => Boolean, { description: 'Delete an user account from infra', }) @@ -471,4 +490,14 @@ export class AdminResolver { userInvited(@GqlUser() admin: AuthUser) { return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`); } + + @Subscription(() => InvitedUser, { + description: 'Listen for User Revocation', + resolve: (value) => value, + }) + @SkipThrottle() + @UseGuards(GqlAuthGuard, GqlAdminGuard) + userRevoked(@GqlUser() admin: AuthUser) { + return this.pubsub.asyncIterator(`admin/${admin.uid}/revoked`); + } } diff --git a/packages/hoppscotch-backend/src/admin/admin.service.ts b/packages/hoppscotch-backend/src/admin/admin.service.ts index 077d46ab3..80792011f 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.ts @@ -11,10 +11,10 @@ import { INVALID_EMAIL, ONLY_ONE_ADMIN_ACCOUNT, TEAM_INVITE_ALREADY_MEMBER, - TEAM_INVITE_NO_INVITE_FOUND, USER_ALREADY_INVITED, USER_IS_ADMIN, USER_NOT_FOUND, + USER_NOT_INVITED, } from '../errors'; import { MailerService } from '../mailer/mailer.service'; import { InvitedUser } from './invited-user.model'; @@ -26,6 +26,7 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic import { TeamMemberRole } from '../team/team.model'; import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ConfigService } from '@nestjs/config'; +import { Admin } from './admin.model'; @Injectable() export class AdminService { @@ -110,18 +111,69 @@ export class AdminService { return E.right(invitedUser); } + /** + * Revoke the invitation of a user to join infra. + * @param inviteeEmail Invitee's email + * @param adminUser Admin object + * @returns an Either of array of `InvitedUser` object or error string + */ + async revokeUserInvite(inviteeEmail: string, adminUser: Admin) { + try { + const deletedInvitee = await this.prisma.invitedUsers.delete({ + where: { + inviteeEmail, + }, + }); + + const invitedUser = { + adminEmail: deletedInvitee.adminEmail, + adminUid: deletedInvitee.adminUid, + inviteeEmail: deletedInvitee.inviteeEmail, + invitedOn: deletedInvitee.invitedOn, + }; + + this.pubsub.publish(`admin/${adminUser.uid}/revoked`, invitedUser); + + return E.right(true); + } catch (error) { + return E.left(USER_NOT_INVITED); + } + } + /** * Fetch the list of invited users by the admin. * @returns an Either of array of `InvitedUser` object or error */ async fetchInvitedUsers() { - const invitedUsers = await this.prisma.invitedUsers.findMany(); + const dbInvitedUsers = await this.prisma.invitedUsers.findMany(); - const users: InvitedUser[] = invitedUsers.map( - (user) => { ...user }, - ); + const invitationAcceptedUsers = await this.prisma.user.findMany({ + where: { + email: { + in: dbInvitedUsers.map((user) => user.inviteeEmail), + }, + }, + }); - return users; + let invitedUsers: InvitedUser[] = []; + + dbInvitedUsers.forEach((dbInvitedUser) => { + const isUserAccepts = invitationAcceptedUsers.find( + (user) => user.email === dbInvitedUser.inviteeEmail, + ); + + const invitedUser: InvitedUser = { + adminEmail: dbInvitedUser.adminEmail, + adminUid: dbInvitedUser.adminUid, + inviteeEmail: dbInvitedUser.inviteeEmail, + invitedOn: dbInvitedUser.invitedOn, + isInvitationAccepted: isUserAccepts ? true : false, + }; + + invitedUsers.push(invitedUser); + }); + + return invitedUsers; } /** diff --git a/packages/hoppscotch-backend/src/admin/invited-user.model.ts b/packages/hoppscotch-backend/src/admin/invited-user.model.ts index f61564bb3..e9810f664 100644 --- a/packages/hoppscotch-backend/src/admin/invited-user.model.ts +++ b/packages/hoppscotch-backend/src/admin/invited-user.model.ts @@ -21,4 +21,10 @@ export class InvitedUser { description: 'Date when the user invitation was sent', }) invitedOn: Date; + + @Field({ + description: 'Boolean status if invitation was accepted or not', + defaultValue: false, + }) + isInvitationAccepted: boolean; } diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 3297be8f8..a85a06e42 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -65,6 +65,11 @@ export const USER_FB_DOCUMENT_DELETION_FAILED = */ export const USER_NOT_FOUND = 'user/not_found' as const; +/** + * User is not invited by admin + */ +export const USER_NOT_INVITED = 'admin/user_not_invited' as const; + /** * User is already invited by admin */ diff --git a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts index 49506c67e..43119a1ab 100644 --- a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts @@ -31,7 +31,7 @@ import { Shortcode } from 'src/shortcode/shortcode.model'; // A custom message type that defines the topic and the corresponding payload. // For every module that publishes a subscription add its type def and the possible subscription type. export type TopicDef = { - [topic: `admin/${string}/${'invited'}`]: InvitedUser; + [topic: `admin/${string}/${'invited' | 'revoked'}`]: InvitedUser; [topic: `user/${string}/${'updated' | 'deleted'}`]: User; [topic: `user_settings/${string}/${'created' | 'updated'}`]: UserSettings; [