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:
Ankit Sridhar
2023-03-21 16:42:30 +05:30
committed by GitHub
parent 8b1d8e6a90
commit f78354a377
34 changed files with 1986 additions and 35 deletions

View 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;
}
}