feat: Introducing Admin Module to Backend (HBE-83) (#21)
* feat: introducing admin module, resolvers and service files as a module * feat: adding admin module in the app module * feat: introducing admin guard and decorator for allowing admin operations * feat: invited user model * chore: added user invitation mail description to mailer service * chore: added admin and user related error * feat: added invited users as a new model in prisma * chore: added admin related topics to pubsub * chore: added service method to fetch all users from user table * chore: added user deletion base implementation * Revert "chore: added user deletion base implementation" This reverts commit d1615ad83db2bae946e2d366a903d2f95051dabb. * feat: adding team related operations to admin * chore: adding admin related service methods to teams module service * chore: adding admin related service methods to team coll invitations requests envs * chore: added more module error messages * chore: added admin check service method * chore: added find individual user by UID in admin * HBE-106 feat: introduced code to handle first time admin login setup (#23) * test: wrote test cases for verifyAdmin route service method * chore: added comments to verifyAdmin service method * chore: deleted the prisma migration file * chore: added find admin users * feat: added user deletion into admin module * chore: admin user related errors * chore: fixed registry pattern in the shortcodes and teams to handle user deletion * chore: add subscription topic for user deletion * chore: updated user type in data handler * feat: implement and fix user deletion * feat: added make user admin mutation * chore: added unit tests for admin specific service methods in admin module * chore: added invitation not found error * chore: added admin specific operation test cases in specific modules * chore: added tests related to user deletion and admin related operation in user module * chore: updated to error constant when invitations not found * chore: fix rebase overwritten methods * feat: implement remove user as admin * chore: add new line * feat: introducing basic metrics into the self-hosted admin module (HBE-104) (#43) * feat: introducing admin module, resolvers and service files as a module * feat: adding admin module in the app module * feat: introducing admin guard and decorator for allowing admin operations * feat: invited user model * chore: added user invitation mail description to mailer service * chore: added admin and user related error * feat: added invited users as a new model in prisma * chore: added admin related topics to pubsub * chore: added service method to fetch all users from user table * chore: added user deletion base implementation * Revert "chore: added user deletion base implementation" This reverts commit d1615ad83db2bae946e2d366a903d2f95051dabb. * feat: adding team related operations to admin * chore: adding admin related service methods to teams module service * chore: adding admin related service methods to team coll invitations requests envs * chore: added more module error messages * chore: added admin check service method * chore: added find individual user by UID in admin * HBE-106 feat: introduced code to handle first time admin login setup (#23) * test: wrote test cases for verifyAdmin route service method * chore: added comments to verifyAdmin service method * chore: deleted the prisma migration file * chore: added find admin users * feat: added user deletion into admin module * chore: admin user related errors * chore: fixed registry pattern in the shortcodes and teams to handle user deletion * chore: add subscription topic for user deletion * chore: updated user type in data handler * feat: implement and fix user deletion * feat: added make user admin mutation * chore: added unit tests for admin specific service methods in admin module * chore: added invitation not found error * chore: added admin specific operation test cases in specific modules * chore: added tests related to user deletion and admin related operation in user module * chore: updated to error constant when invitations not found * chore: fix rebase overwritten methods * feat: implement remove user as admin * chore: add new line * chore: created new GQL return type for admin module * chore: created resolver and service method for method to fetch org metrics * chore: removed all entities relevant to seperate query for fetching admin metrics * chore: created all resolvers for metrics * feat: completed adding field resolves to query org metrics * test: wrote tests for all metrics related methods in admin module * test: added test cases for get count functions in multiple modules * chore: removed prisma migration folder * Delete backend-schema.gql * chore: resolved merge conflicts in team test file --------- Co-authored-by: ankitsridhar16 <ankit.sridhar16@gmail.com> * refactor: update mailer service to stop using postmark (#38) * refactor: update mailer service to stop using postmark * chore: remove postmark as a dep and move out postmark code * chore: remove postmark variables from .env.example * chore: add formal errors for mailer initialization errors * chore: add and update jsdoc comments in mailer service methods * chore: added user invitation mail description to mailer service * chore: updated with review changes requested for admin module * feat: adding admin resolver to gql schema * feat: adding input args for admin resolvers * chore: invited user renamed * chore: updated mailer service to be compatible with new mailer * chore: updated team service with review changes * chore: updated team collection service with review changes * chore: updated team environments service with review changes * chore: updated team requests service with review changes * chore: updated user service with review changes * refactor: invited user model * chore: review changes implemented * chore: implemented the review changes for admin, user and teams module * chore: removed error handling and implemented review changes * refactor: naming change for IsAdmin --------- Co-authored-by: Balu Babu <balub997@gmail.com> Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
4
packages/hoppscotch-backend/src/admin/admin.model.ts
Normal file
4
packages/hoppscotch-backend/src/admin/admin.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { ObjectType, ID, Field, ResolveField } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class Admin {}
|
||||
29
packages/hoppscotch-backend/src/admin/admin.module.ts
Normal file
29
packages/hoppscotch-backend/src/admin/admin.module.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminResolver } from './admin.resolver';
|
||||
import { AdminService } from './admin.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { MailerModule } from '../mailer/mailer.module';
|
||||
import { TeamModule } from '../team/team.module';
|
||||
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
|
||||
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
|
||||
import {TeamCollectionModule} from "../team-collection/team-collection.module";
|
||||
import {TeamRequestModule} from "../team-request/team-request.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
PubSubModule,
|
||||
UserModule,
|
||||
MailerModule,
|
||||
TeamModule,
|
||||
TeamInvitationModule,
|
||||
TeamEnvironmentsModule,
|
||||
TeamCollectionModule,
|
||||
TeamRequestModule,
|
||||
],
|
||||
providers: [AdminResolver, AdminService],
|
||||
exports: [AdminService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
402
packages/hoppscotch-backend/src/admin/admin.resolver.ts
Normal file
402
packages/hoppscotch-backend/src/admin/admin.resolver.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
import {
|
||||
Args,
|
||||
ID,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
Subscription,
|
||||
} from '@nestjs/graphql';
|
||||
import { Admin } from './admin.model';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlAuthGuard } from '../guards/gql-auth.guard';
|
||||
import { GqlAdminGuard } from './guards/gql-admin.guard';
|
||||
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
||||
import { AdminService } from './admin.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { throwErr } from '../utils';
|
||||
import { AuthUser } from '../types/AuthUser';
|
||||
import { InvitedUser } from './invited-user.model';
|
||||
import { GqlUser } from '../decorators/gql-user.decorator';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import { Team, TeamMember } from '../team/team.model';
|
||||
import { User } from '../user/user.model';
|
||||
import { TeamInvitation } from '../team-invitation/team-invitation.model';
|
||||
import { PaginationArgs } from '../types/input-types.args';
|
||||
import {
|
||||
AddUserToTeamArgs,
|
||||
ChangeUserRoleInTeamArgs,
|
||||
} from './input-types.args';
|
||||
|
||||
@Resolver(() => Admin)
|
||||
export class AdminResolver {
|
||||
constructor(
|
||||
private adminService: AdminService,
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
// Query
|
||||
@Query(() => Admin, {
|
||||
description: 'Gives details of the admin executing this query',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
admin(@GqlAdmin() admin: Admin) {
|
||||
return admin;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all admin users in infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async admins() {
|
||||
const admins = await this.adminService.fetchAdmins();
|
||||
return admins;
|
||||
}
|
||||
@ResolveField(() => User, {
|
||||
description: 'Returns a user info by UID',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async userInfo(
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
type: () => ID,
|
||||
description: 'The user UID',
|
||||
})
|
||||
userUid: string,
|
||||
): Promise<AuthUser> {
|
||||
const user = await this.adminService.fetchUserInfo(userUid);
|
||||
if (E.isLeft(user)) throwErr(user.left);
|
||||
return user.right;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(
|
||||
@Parent() admin: Admin,
|
||||
@Args() args: PaginationArgs,
|
||||
): Promise<AuthUser[]> {
|
||||
const users = await this.adminService.fetchUsers(args.cursor, args.take);
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [InvitedUser], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
})
|
||||
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [Team], {
|
||||
description: 'Returns a list of all the teams in the infra',
|
||||
})
|
||||
async allTeams(
|
||||
@Parent() admin: Admin,
|
||||
@Args() args: PaginationArgs,
|
||||
): Promise<Team[]> {
|
||||
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
|
||||
return teams;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the members in a team',
|
||||
})
|
||||
async membersCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
nullable: false,
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
|
||||
return teamMembersCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored collections in a team',
|
||||
})
|
||||
async collectionCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
|
||||
return teamCollCount;
|
||||
}
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored requests in a team',
|
||||
})
|
||||
async requestCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
|
||||
return teamReqCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored environments in a team',
|
||||
})
|
||||
async environmentCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const envsCount = await this.adminService.environmentCountInTeam(teamID);
|
||||
return envsCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => [TeamInvitation], {
|
||||
description: 'Return all the pending invitations in a team',
|
||||
})
|
||||
async pendingInvitationCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
const invitations = await this.adminService.pendingInvitationCountInTeam(
|
||||
teamID,
|
||||
);
|
||||
return invitations;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Users in organization',
|
||||
})
|
||||
async usersCount() {
|
||||
return this.adminService.getUsersCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Teams in organization',
|
||||
})
|
||||
async teamsCount() {
|
||||
return this.adminService.getTeamsCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Collections in organization',
|
||||
})
|
||||
async teamCollectionsCount() {
|
||||
return this.adminService.getTeamCollectionsCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Requests in organization',
|
||||
})
|
||||
async teamRequestsCount() {
|
||||
return this.adminService.getTeamRequestsCount();
|
||||
}
|
||||
|
||||
// Mutations
|
||||
@Mutation(() => InvitedUser, {
|
||||
description: 'Invite a user to the infra using email',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async inviteNewUser(
|
||||
@GqlUser() adminUser: AuthUser,
|
||||
@Args({
|
||||
name: 'inviteeEmail',
|
||||
description: 'invitee email',
|
||||
})
|
||||
inviteeEmail: string,
|
||||
): Promise<InvitedUser> {
|
||||
const invitedUser = await this.adminService.inviteUserToSignInViaEmail(
|
||||
adminUser.uid,
|
||||
adminUser.email,
|
||||
inviteeEmail,
|
||||
);
|
||||
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
||||
return invitedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete an user account from infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserByAdmin(
|
||||
@Args({
|
||||
name: 'userUID',
|
||||
description: 'users UID',
|
||||
type: () => ID,
|
||||
})
|
||||
userUID: string,
|
||||
): Promise<boolean> {
|
||||
const invitedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
||||
return invitedUser.right;
|
||||
}
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Make user an admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUserAdmin(
|
||||
@Args({
|
||||
name: 'userUID',
|
||||
description: 'users UID',
|
||||
type: () => ID,
|
||||
})
|
||||
userUID: string,
|
||||
): Promise<boolean> {
|
||||
const admin = await this.adminService.makeUserAdmin(userUID);
|
||||
if (E.isLeft(admin)) throwErr(admin.left);
|
||||
return admin.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove user as admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserAsAdmin(
|
||||
@Args({
|
||||
name: 'userUID',
|
||||
description: 'users UID',
|
||||
type: () => ID,
|
||||
})
|
||||
userUID: string,
|
||||
): Promise<boolean> {
|
||||
const admin = await this.adminService.removeUserAsAdmin(userUID);
|
||||
if (E.isLeft(admin)) throwErr(admin.left);
|
||||
return admin.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Team, {
|
||||
description:
|
||||
'Create a new team by providing the user uid to nominate as Team owner',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async createTeamByAdmin(
|
||||
@GqlAdmin() adminUser: Admin,
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
description: 'users uid to make team owner',
|
||||
type: () => ID,
|
||||
})
|
||||
userUid: string,
|
||||
@Args({ name: 'name', description: 'Displayed name of the team' })
|
||||
name: string,
|
||||
): Promise<Team> {
|
||||
const createdTeam = await this.adminService.createATeam(userUid, name);
|
||||
if (E.isLeft(createdTeam)) throwErr(createdTeam.left);
|
||||
return createdTeam.right;
|
||||
}
|
||||
@Mutation(() => TeamMember, {
|
||||
description: 'Change the role of a user in a team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async changeUserRoleInTeamByAdmin(
|
||||
@GqlAdmin() adminUser: Admin,
|
||||
@Args() args: ChangeUserRoleInTeamArgs,
|
||||
): Promise<TeamMember> {
|
||||
const updatedRole = await this.adminService.changeRoleOfUserTeam(
|
||||
args.userUID,
|
||||
args.teamID,
|
||||
args.newRole,
|
||||
);
|
||||
if (E.isLeft(updatedRole)) throwErr(updatedRole.left);
|
||||
return updatedRole.right;
|
||||
}
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove the user from a team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserFromTeamByAdmin(
|
||||
@GqlAdmin() adminUser: Admin,
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
description: 'users UID',
|
||||
type: () => ID,
|
||||
})
|
||||
userUid: string,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'team ID',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<boolean> {
|
||||
const removedUser = await this.adminService.removeUserFromTeam(
|
||||
userUid,
|
||||
teamID,
|
||||
);
|
||||
if (E.isLeft(removedUser)) throwErr(removedUser.left);
|
||||
return removedUser.right;
|
||||
}
|
||||
@Mutation(() => TeamMember, {
|
||||
description: 'Add a user to a team with email and team member role',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async addUserToTeamByAdmin(
|
||||
@GqlAdmin() adminUser: Admin,
|
||||
@Args() args: AddUserToTeamArgs,
|
||||
): Promise<TeamMember> {
|
||||
const addedUser = await this.adminService.addUserToTeam(
|
||||
args.teamID,
|
||||
args.userEmail,
|
||||
args.role,
|
||||
);
|
||||
if (E.isLeft(addedUser)) throwErr(addedUser.left);
|
||||
return addedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Team, {
|
||||
description: 'Change a team name',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async renameTeamByAdmin(
|
||||
@GqlAdmin() adminUser: Admin,
|
||||
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
||||
teamID: string,
|
||||
@Args({ name: 'newName', description: 'The updated name of the team' })
|
||||
newName: string,
|
||||
): Promise<Team> {
|
||||
const renamedTeam = await this.adminService.renameATeam(teamID, newName);
|
||||
if (E.isLeft(renamedTeam)) throwErr(renamedTeam.left);
|
||||
return renamedTeam.right;
|
||||
}
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete a team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async deleteTeamByAdmin(
|
||||
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
||||
teamID: string,
|
||||
): Promise<boolean> {
|
||||
const deletedTeam = await this.adminService.deleteATeam(teamID);
|
||||
if (E.isLeft(deletedTeam)) throwErr(deletedTeam.left);
|
||||
return deletedTeam.right;
|
||||
}
|
||||
|
||||
/* Subscriptions */
|
||||
|
||||
@Subscription(() => InvitedUser, {
|
||||
description: 'Listen for User Invitation',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
userInvited(@GqlUser() admin: AuthUser) {
|
||||
return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`);
|
||||
}
|
||||
}
|
||||
168
packages/hoppscotch-backend/src/admin/admin.service.spec.ts
Normal file
168
packages/hoppscotch-backend/src/admin/admin.service.spec.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { AdminService } from './admin.service';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { InvitedUsers } from '@prisma/client';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { TeamService } from '../team/team.service';
|
||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||
import { TeamRequestService } from '../team-request/team-request.service';
|
||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
||||
import { MailerService } from '../mailer/mailer.service';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import {
|
||||
DUPLICATE_EMAIL,
|
||||
INVALID_EMAIL,
|
||||
USER_ALREADY_INVITED,
|
||||
} from '../errors';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
const mockUserService = mockDeep<UserService>();
|
||||
const mockTeamService = mockDeep<TeamService>();
|
||||
const mockTeamEnvironmentsService = mockDeep<TeamEnvironmentsService>();
|
||||
const mockTeamRequestService = mockDeep<TeamRequestService>();
|
||||
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||
const mockMailerService = mockDeep<MailerService>();
|
||||
|
||||
const adminService = new AdminService(
|
||||
mockUserService,
|
||||
mockTeamService,
|
||||
mockTeamCollectionService,
|
||||
mockTeamRequestService,
|
||||
mockTeamEnvironmentsService,
|
||||
mockTeamInvitationService,
|
||||
mockPubSub as any,
|
||||
mockPrisma as any,
|
||||
mockMailerService,
|
||||
);
|
||||
|
||||
const invitedUsers: InvitedUsers[] = [
|
||||
{
|
||||
adminUid: 'uid1',
|
||||
adminEmail: 'admin1@example.com',
|
||||
inviteeEmail: 'i@example.com',
|
||||
invitedOn: new Date(),
|
||||
},
|
||||
{
|
||||
adminUid: 'uid2',
|
||||
adminEmail: 'admin2@example.com',
|
||||
inviteeEmail: 'u@example.com',
|
||||
invitedOn: new Date(),
|
||||
},
|
||||
];
|
||||
describe('AdminService', () => {
|
||||
describe('fetchInvitedUsers', () => {
|
||||
test('should resolve right and return an array of invited users', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
expect(results).toEqual(invitedUsers);
|
||||
});
|
||||
test('should resolve left and return an error if invited users not found', async () => {
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inviteUserToSignInViaEmail', () => {
|
||||
test('should resolve right and create a invited user', async () => {
|
||||
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);
|
||||
mockPrisma.invitedUsers.create.mockResolvedValueOnce(invitedUsers[0]);
|
||||
const result = await adminService.inviteUserToSignInViaEmail(
|
||||
invitedUsers[0].adminUid,
|
||||
invitedUsers[0].adminEmail,
|
||||
invitedUsers[0].inviteeEmail,
|
||||
);
|
||||
expect(mockPrisma.invitedUsers.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
adminUid: invitedUsers[0].adminUid,
|
||||
adminEmail: invitedUsers[0].adminEmail,
|
||||
inviteeEmail: invitedUsers[0].inviteeEmail,
|
||||
},
|
||||
});
|
||||
return expect(result).toEqualRight(invitedUsers[0]);
|
||||
});
|
||||
test('should resolve right, create a invited user and publish a subscription', async () => {
|
||||
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);
|
||||
mockPrisma.invitedUsers.create.mockResolvedValueOnce(invitedUsers[0]);
|
||||
await adminService.inviteUserToSignInViaEmail(
|
||||
invitedUsers[0].adminUid,
|
||||
invitedUsers[0].adminEmail,
|
||||
invitedUsers[0].inviteeEmail,
|
||||
);
|
||||
return expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`admin/${invitedUsers[0].adminUid}/invited`,
|
||||
invitedUsers[0],
|
||||
);
|
||||
});
|
||||
test('should resolve left and return an error when invalid invitee email is passed', async () => {
|
||||
const result = await adminService.inviteUserToSignInViaEmail(
|
||||
invitedUsers[0].adminUid,
|
||||
invitedUsers[0].adminEmail,
|
||||
'invalidemail',
|
||||
);
|
||||
return expect(result).toEqualLeft(INVALID_EMAIL);
|
||||
});
|
||||
test('should resolve left and return an error when user already invited', async () => {
|
||||
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(invitedUsers[0]);
|
||||
const result = await adminService.inviteUserToSignInViaEmail(
|
||||
invitedUsers[0].adminUid,
|
||||
invitedUsers[0].adminEmail,
|
||||
invitedUsers[0].inviteeEmail,
|
||||
);
|
||||
return expect(result).toEqualLeft(USER_ALREADY_INVITED);
|
||||
});
|
||||
test('should resolve left and return an error when invitee and admin email is same', async () => {
|
||||
const result = await adminService.inviteUserToSignInViaEmail(
|
||||
invitedUsers[0].adminUid,
|
||||
invitedUsers[0].inviteeEmail,
|
||||
invitedUsers[0].inviteeEmail,
|
||||
);
|
||||
return expect(result).toEqualLeft(DUPLICATE_EMAIL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsersCount', () => {
|
||||
test('should return count of all users in the organization', async () => {
|
||||
mockUserService.getUsersCount.mockResolvedValueOnce(10);
|
||||
|
||||
const result = await adminService.getUsersCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamsCount', () => {
|
||||
test('should return count of all teams in the organization', async () => {
|
||||
mockTeamService.getTeamsCount.mockResolvedValueOnce(10);
|
||||
|
||||
const result = await adminService.getTeamsCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamCollectionsCount', () => {
|
||||
test('should return count of all Team Collections in the organization', async () => {
|
||||
mockTeamCollectionService.getTeamCollectionsCount.mockResolvedValueOnce(
|
||||
10,
|
||||
);
|
||||
|
||||
const result = await adminService.getTeamCollectionsCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamRequestsCount', () => {
|
||||
test('should return count of all Team Collections in the organization', async () => {
|
||||
mockTeamRequestService.getTeamRequestsCount.mockResolvedValueOnce(10);
|
||||
|
||||
const result = await adminService.getTeamRequestsCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
392
packages/hoppscotch-backend/src/admin/admin.service.ts
Normal file
392
packages/hoppscotch-backend/src/admin/admin.service.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { validateEmail } from '../utils';
|
||||
import {
|
||||
DUPLICATE_EMAIL,
|
||||
EMAIL_FAILED,
|
||||
INVALID_EMAIL,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_IS_ADMIN,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
import { MailerService } from '../mailer/mailer.service';
|
||||
import { InvitedUser } from './invited-user.model';
|
||||
import { TeamService } from '../team/team.service';
|
||||
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
||||
import { TeamRequestService } from '../team-request/team-request.service';
|
||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly teamService: TeamService,
|
||||
private readonly teamCollectionService: TeamCollectionService,
|
||||
private readonly teamRequestService: TeamRequestService,
|
||||
private readonly teamEnvironmentsService: TeamEnvironmentsService,
|
||||
private readonly teamInvitationService: TeamInvitationService,
|
||||
private readonly pubsub: PubSubService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mailerService: MailerService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetch all the users in the infra.
|
||||
* @param cursorID Users uid
|
||||
* @param take number of users to fetch
|
||||
* @returns an Either of array of user or error
|
||||
*/
|
||||
async fetchUsers(cursorID: string, take: number) {
|
||||
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to join the infra.
|
||||
* @param adminUID Admin's UID
|
||||
* @param adminEmail Admin's email
|
||||
* @param inviteeEmail Invitee's email
|
||||
* @returns an Either of `InvitedUser` object or error
|
||||
*/
|
||||
async inviteUserToSignInViaEmail(
|
||||
adminUID: string,
|
||||
adminEmail: string,
|
||||
inviteeEmail: string,
|
||||
) {
|
||||
if (inviteeEmail == adminEmail) return E.left(DUPLICATE_EMAIL);
|
||||
if (!validateEmail(inviteeEmail)) return E.left(INVALID_EMAIL);
|
||||
|
||||
const alreadyInvitedUser = await this.prisma.invitedUsers.findFirst({
|
||||
where: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
},
|
||||
});
|
||||
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
|
||||
|
||||
try {
|
||||
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
|
||||
template: 'code-your-own',
|
||||
variables: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
magicLink: `${process.env.APP_DOMAIN}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return E.left(EMAIL_FAILED);
|
||||
}
|
||||
|
||||
// Add invitee email to the list of invited users by admin
|
||||
const dbInvitedUser = await this.prisma.invitedUsers.create({
|
||||
data: {
|
||||
adminUid: adminUID,
|
||||
adminEmail: adminEmail,
|
||||
inviteeEmail: inviteeEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const invitedUser = <InvitedUser>{
|
||||
adminEmail: dbInvitedUser.adminEmail,
|
||||
adminUid: dbInvitedUser.adminUid,
|
||||
inviteeEmail: dbInvitedUser.inviteeEmail,
|
||||
invitedOn: dbInvitedUser.invitedOn,
|
||||
};
|
||||
|
||||
// Publish invited user subscription
|
||||
await this.pubsub.publish(`admin/${adminUID}/invited`, invitedUser);
|
||||
|
||||
return E.right(invitedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 users: InvitedUser[] = invitedUsers.map(
|
||||
(user) => <InvitedUser>{ ...user },
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the teams in the infra.
|
||||
* @param cursorID team id
|
||||
* @param take number of items to fetch
|
||||
* @returns an array of teams
|
||||
*/
|
||||
async fetchAllTeams(cursorID: string, take: number) {
|
||||
const allTeams = await this.teamService.fetchAllTeams(cursorID, take);
|
||||
return allTeams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the count of all the members in a team.
|
||||
* @param teamID team id
|
||||
* @returns a count of team members
|
||||
*/
|
||||
async membersCountInTeam(teamID: string) {
|
||||
const teamMembersCount = await this.teamService.getCountOfMembersInTeam(
|
||||
teamID,
|
||||
);
|
||||
return teamMembersCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch count of all the collections in a team.
|
||||
* @param teamID team id
|
||||
* @returns a of count of collections
|
||||
*/
|
||||
async collectionCountInTeam(teamID: string) {
|
||||
const teamCollectionsCount =
|
||||
await this.teamCollectionService.totalCollectionsInTeam(teamID);
|
||||
return teamCollectionsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the count of all the requests in a team.
|
||||
* @param teamID team id
|
||||
* @returns a count of total requests in a team
|
||||
*/
|
||||
async requestCountInTeam(teamID: string) {
|
||||
const teamRequestsCount =
|
||||
await this.teamRequestService.totalRequestsInATeam(teamID);
|
||||
|
||||
return teamRequestsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the count of all the environments in a team.
|
||||
* @param teamID team id
|
||||
* @returns a count of environments in a team
|
||||
*/
|
||||
async environmentCountInTeam(teamID: string) {
|
||||
const envCount = await this.teamEnvironmentsService.totalEnvsInTeam(teamID);
|
||||
return envCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the invitations for a given team.
|
||||
* @param teamID team id
|
||||
* @returns an array team invitations
|
||||
*/
|
||||
async pendingInvitationCountInTeam(teamID: string) {
|
||||
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
||||
teamID,
|
||||
);
|
||||
|
||||
return invitations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the role of a user in a team
|
||||
* @param userUid users uid
|
||||
* @param teamID team id
|
||||
* @returns an Either of updated `TeamMember` object or error
|
||||
*/
|
||||
async changeRoleOfUserTeam(
|
||||
userUid: string,
|
||||
teamID: string,
|
||||
newRole: TeamMemberRole,
|
||||
) {
|
||||
const updatedTeamMember = await this.teamService.updateTeamMemberRole(
|
||||
teamID,
|
||||
userUid,
|
||||
newRole,
|
||||
);
|
||||
|
||||
if (E.isLeft(updatedTeamMember)) return E.left(updatedTeamMember.left);
|
||||
|
||||
return E.right(updatedTeamMember.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the user from a team
|
||||
* @param userUid users uid
|
||||
* @param teamID team id
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUserFromTeam(userUid: string, teamID: string) {
|
||||
const removedUser = await this.teamService.leaveTeam(teamID, userUid);
|
||||
if (E.isLeft(removedUser)) return E.left(removedUser.left);
|
||||
|
||||
return E.right(removedUser.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the user to a team
|
||||
* @param teamID team id
|
||||
* @param userEmail users email
|
||||
* @param role team member role for the user
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async addUserToTeam(teamID: string, userEmail: string, role: TeamMemberRole) {
|
||||
if (!validateEmail(userEmail)) return E.left(INVALID_EMAIL);
|
||||
|
||||
const user = await this.userService.findUserByEmail(userEmail);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
|
||||
teamID,
|
||||
user.value.uid,
|
||||
)();
|
||||
if (E.left(isUserAlreadyMember)) {
|
||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||
teamID,
|
||||
userEmail,
|
||||
role,
|
||||
);
|
||||
if (E.isLeft(addedUser)) return E.left(addedUser.left);
|
||||
|
||||
return E.right(addedUser.right);
|
||||
}
|
||||
|
||||
return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new team
|
||||
* @param userUid user uid
|
||||
* @param name team name
|
||||
* @returns an Either of `Team` object or error
|
||||
*/
|
||||
async createATeam(userUid: string, name: string) {
|
||||
const validUser = await this.userService.findUserById(userUid);
|
||||
if (O.isNone(validUser)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
const createdTeam = await this.teamService.createTeam(name, userUid);
|
||||
if (E.isLeft(createdTeam)) return E.left(createdTeam.left);
|
||||
|
||||
return E.right(createdTeam.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a team
|
||||
* @param teamID team ID
|
||||
* @param newName new team name
|
||||
* @returns an Either of `Team` object or error
|
||||
*/
|
||||
async renameATeam(teamID: string, newName: string) {
|
||||
const renamedTeam = await this.teamService.renameTeam(teamID, newName);
|
||||
if (E.isLeft(renamedTeam)) return E.left(renamedTeam.left);
|
||||
|
||||
return E.right(renamedTeam.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a team
|
||||
* @param teamID team ID
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async deleteATeam(teamID: string) {
|
||||
const deleteTeam = await this.teamService.deleteTeam(teamID);
|
||||
if (E.isLeft(deleteTeam)) return E.left(deleteTeam.left);
|
||||
|
||||
return E.right(deleteTeam.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all admin accounts
|
||||
* @returns an array of admin users
|
||||
*/
|
||||
async fetchAdmins() {
|
||||
const admins = this.userService.fetchAdminUsers();
|
||||
return admins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a user by UID
|
||||
* @param userUid User UID
|
||||
* @returns an Either of `User` obj or error
|
||||
*/
|
||||
async fetchUserInfo(userUid: string) {
|
||||
const user = await this.userService.findUserById(userUid);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
return E.right(user.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user account by UID
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUserAccount(userUid: string) {
|
||||
const user = await this.userService.findUserById(userUid);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
if (user.value.isAdmin) return E.left(USER_IS_ADMIN);
|
||||
|
||||
const delUser = await this.userService.deleteUserByUID(user.value)();
|
||||
if (E.isLeft(delUser)) return E.left(delUser.left);
|
||||
return E.right(delUser.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a user an admin
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async makeUserAdmin(userUID: string) {
|
||||
const admin = await this.userService.makeAdmin(userUID);
|
||||
if (E.isLeft(admin)) return E.left(admin.left);
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user as admin
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUserAsAdmin(userUID: string) {
|
||||
const admin = await this.userService.removeUserAsAdmin(userUID);
|
||||
if (E.isLeft(admin)) return E.left(admin.left);
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of all the Users in org
|
||||
* @returns number of users in the org
|
||||
*/
|
||||
async getUsersCount() {
|
||||
const usersCount = this.userService.getUsersCount();
|
||||
return usersCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of all the Teams in org
|
||||
* @returns number of users in the org
|
||||
*/
|
||||
async getTeamsCount() {
|
||||
const teamsCount = this.teamService.getTeamsCount();
|
||||
return teamsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of all the Team Collections in org
|
||||
* @returns number of users in the org
|
||||
*/
|
||||
async getTeamCollectionsCount() {
|
||||
const teamCollectionCount =
|
||||
this.teamCollectionService.getTeamCollectionsCount();
|
||||
return teamCollectionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of all the Team Requests in org
|
||||
* @returns number of users in the org
|
||||
*/
|
||||
async getTeamRequestsCount() {
|
||||
const teamRequestCount = this.teamRequestService.getTeamRequestsCount();
|
||||
return teamRequestCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
export const GqlAdmin = createParamDecorator(
|
||||
(data: unknown, context: ExecutionContext) => {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
return ctx.getContext().req.user;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
@Injectable()
|
||||
export class GqlAdminGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
const { req, headers } = ctx.getContext();
|
||||
const request = headers ? headers : req;
|
||||
const user = request.user;
|
||||
if (user.isAdmin) return true;
|
||||
else return false;
|
||||
}
|
||||
}
|
||||
42
packages/hoppscotch-backend/src/admin/input-types.args.ts
Normal file
42
packages/hoppscotch-backend/src/admin/input-types.args.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Field, ID, ArgsType } from '@nestjs/graphql';
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
|
||||
@ArgsType()
|
||||
export class ChangeUserRoleInTeamArgs {
|
||||
@Field(() => ID, {
|
||||
name: 'userUID',
|
||||
description: 'users UID',
|
||||
})
|
||||
userUID: string;
|
||||
@Field(() => ID, {
|
||||
name: 'teamID',
|
||||
description: 'team ID',
|
||||
})
|
||||
teamID: string;
|
||||
|
||||
@Field(() => TeamMemberRole, {
|
||||
name: 'newRole',
|
||||
description: 'updated team role',
|
||||
})
|
||||
newRole: TeamMemberRole;
|
||||
}
|
||||
@ArgsType()
|
||||
export class AddUserToTeamArgs {
|
||||
@Field(() => ID, {
|
||||
name: 'teamID',
|
||||
description: 'team ID',
|
||||
})
|
||||
teamID: string;
|
||||
|
||||
@Field(() => TeamMemberRole, {
|
||||
name: 'role',
|
||||
description: 'The role of the user to add in the team',
|
||||
})
|
||||
role: TeamMemberRole;
|
||||
|
||||
@Field({
|
||||
name: 'userEmail',
|
||||
description: 'Email of the user to add to team',
|
||||
})
|
||||
userEmail: string;
|
||||
}
|
||||
24
packages/hoppscotch-backend/src/admin/invited-user.model.ts
Normal file
24
packages/hoppscotch-backend/src/admin/invited-user.model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ObjectType, ID, Field } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class InvitedUser {
|
||||
@Field(() => ID, {
|
||||
description: 'Admin UID',
|
||||
})
|
||||
adminUid: string;
|
||||
|
||||
@Field({
|
||||
description: 'Admin email',
|
||||
})
|
||||
adminEmail: string;
|
||||
|
||||
@Field({
|
||||
description: 'Invitee email',
|
||||
})
|
||||
inviteeEmail: string;
|
||||
|
||||
@Field({
|
||||
description: 'Date when the user invitation was sent',
|
||||
})
|
||||
invitedOn: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user