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:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user