632 lines
18 KiB
TypeScript
632 lines
18 KiB
TypeScript
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 {
|
|
ADMIN_CAN_NOT_BE_DELETED,
|
|
DUPLICATE_EMAIL,
|
|
EMAIL_FAILED,
|
|
INVALID_EMAIL,
|
|
ONLY_ONE_ADMIN_ACCOUNT,
|
|
TEAM_INVITE_ALREADY_MEMBER,
|
|
TEAM_INVITE_NO_INVITE_FOUND,
|
|
USERS_NOT_FOUND,
|
|
USER_ALREADY_INVITED,
|
|
USER_INVITATION_NOT_FOUND,
|
|
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';
|
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
|
import { UserDeletionResult } from 'src/user/user.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,
|
|
private readonly shortcodeService: ShortcodeService,
|
|
private readonly configService: ConfigService,
|
|
) {}
|
|
|
|
/**
|
|
* 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
|
|
* @deprecated use fetchUsersV2 instead
|
|
*/
|
|
async fetchUsers(cursorID: string, take: number) {
|
|
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
|
return allUsers;
|
|
}
|
|
|
|
/**
|
|
* Fetch all the users in the infra.
|
|
* @param searchString search on users displayName or email
|
|
* @param paginationOption pagination options
|
|
* @returns an Either of array of user or error
|
|
*/
|
|
async fetchUsersV2(
|
|
searchString: string,
|
|
paginationOption: OffsetPaginationArgs,
|
|
) {
|
|
const allUsers = await this.userService.fetchAllUsersV2(
|
|
searchString,
|
|
paginationOption,
|
|
);
|
|
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: 'user-invitation',
|
|
variables: {
|
|
inviteeEmail: inviteeEmail,
|
|
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
|
},
|
|
});
|
|
} 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);
|
|
}
|
|
|
|
/**
|
|
* Update the display name of a user
|
|
* @param userUid Who's display name is being updated
|
|
* @param displayName New display name of the user
|
|
* @returns an Either of boolean or error
|
|
*/
|
|
async updateUserDisplayName(userUid: string, displayName: string) {
|
|
const updatedUser = await this.userService.updateUserDisplayName(
|
|
userUid,
|
|
displayName,
|
|
);
|
|
if (E.isLeft(updatedUser)) return E.left(updatedUser.left);
|
|
|
|
return E.right(true);
|
|
}
|
|
|
|
/**
|
|
* Revoke infra level user invitations
|
|
* @param inviteeEmails Invitee's emails
|
|
* @param adminUid Admin Uid
|
|
* @returns an Either of boolean or error string
|
|
*/
|
|
async revokeUserInvites(inviteeEmails: string[], adminUid: string) {
|
|
try {
|
|
await this.prisma.invitedUsers.deleteMany({
|
|
where: {
|
|
inviteeEmail: { in: inviteeEmails },
|
|
},
|
|
});
|
|
return E.right(true);
|
|
} catch (error) {
|
|
return E.left(USER_INVITATION_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the list of invited users by the admin.
|
|
* @returns an Either of array of `InvitedUser` object or error
|
|
*/
|
|
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
|
|
const userEmailObjs = await this.prisma.user.findMany({
|
|
select: {
|
|
email: true,
|
|
},
|
|
});
|
|
|
|
const pendingInvitedUsers = await this.prisma.invitedUsers.findMany({
|
|
take: paginationOption.take,
|
|
skip: paginationOption.skip,
|
|
orderBy: {
|
|
invitedOn: 'desc',
|
|
},
|
|
where: {
|
|
NOT: {
|
|
inviteeEmail: {
|
|
in: userEmailObjs.map((user) => user.email),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const users: InvitedUser[] = pendingInvitedUsers.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.getTeamInvitations(
|
|
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 teamMember = await this.teamService.getTeamMemberTE(
|
|
teamID,
|
|
user.value.uid,
|
|
)();
|
|
if (E.isLeft(teamMember)) {
|
|
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
|
teamID,
|
|
userEmail,
|
|
role,
|
|
);
|
|
if (E.isLeft(addedUser)) return E.left(addedUser.left);
|
|
|
|
const userInvitation =
|
|
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
|
|
userEmail,
|
|
teamID,
|
|
);
|
|
|
|
if (E.isRight(userInvitation)) {
|
|
await this.teamInvitationService.revokeInvitation(
|
|
userInvitation.right.id,
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Remove user (not Admin) accounts by UIDs
|
|
* @param userUIDs User UIDs
|
|
* @returns an Either of boolean or error
|
|
*/
|
|
async removeUserAccounts(userUIDs: string[]) {
|
|
const allUsersList = await this.userService.findUsersByIds(userUIDs);
|
|
if (allUsersList.length === 0) return E.left(USERS_NOT_FOUND);
|
|
|
|
const userDeleteResult: UserDeletionResult[] = [];
|
|
|
|
// Admin user can not be deleted without removing admin status/role
|
|
allUsersList.forEach((user) => {
|
|
if (user.isAdmin) {
|
|
userDeleteResult.push({
|
|
userUID: user.uid,
|
|
isDeleted: false,
|
|
errorMessage: ADMIN_CAN_NOT_BE_DELETED,
|
|
});
|
|
}
|
|
});
|
|
|
|
const nonAdminUsers = allUsersList.filter((user) => !user.isAdmin);
|
|
|
|
const deletionPromises = nonAdminUsers.map((user) => {
|
|
return this.userService
|
|
.deleteUserByUID(user)()
|
|
.then((res) => {
|
|
if (E.isLeft(res)) {
|
|
return {
|
|
userUID: user.uid,
|
|
isDeleted: false,
|
|
errorMessage: res.left,
|
|
} as UserDeletionResult;
|
|
}
|
|
return {
|
|
userUID: user.uid,
|
|
isDeleted: true,
|
|
errorMessage: null,
|
|
} as UserDeletionResult;
|
|
});
|
|
});
|
|
const promiseResult = await Promise.allSettled(deletionPromises);
|
|
|
|
promiseResult.forEach((result) => {
|
|
if (result.status === 'fulfilled') {
|
|
userDeleteResult.push(result.value);
|
|
}
|
|
});
|
|
|
|
return E.right(userDeleteResult);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Make users to admin
|
|
* @param userUid User UIDs
|
|
* @returns an Either of boolean or error
|
|
*/
|
|
async makeUsersAdmin(userUIDs: string[]) {
|
|
const isUpdated = await this.userService.makeAdmins(userUIDs);
|
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.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 adminUsers = await this.userService.fetchAdminUsers();
|
|
if (adminUsers.length === 1) return E.left(ONLY_ONE_ADMIN_ACCOUNT);
|
|
|
|
const admin = await this.userService.removeUserAsAdmin(userUID);
|
|
if (E.isLeft(admin)) return E.left(admin.left);
|
|
return E.right(true);
|
|
}
|
|
|
|
/**
|
|
* Remove users as admin
|
|
* @param userUIDs User UIDs
|
|
* @returns an Either of boolean or error
|
|
*/
|
|
async demoteUsersByAdmin(userUIDs: string[]) {
|
|
const adminUsers = await this.userService.fetchAdminUsers();
|
|
|
|
const remainingAdmins = adminUsers.filter(
|
|
(adminUser) => !userUIDs.includes(adminUser.uid),
|
|
);
|
|
if (remainingAdmins.length < 1) {
|
|
return E.left(ONLY_ONE_ADMIN_ACCOUNT);
|
|
}
|
|
|
|
const isUpdated = await this.userService.removeUsersAsAdmin(userUIDs);
|
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.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;
|
|
}
|
|
|
|
/**
|
|
* Get team info by ID
|
|
* @param teamID Team ID
|
|
* @returns an Either of `Team` or error
|
|
*/
|
|
async getTeamInfo(teamID: string) {
|
|
const team = await this.teamService.getTeamWithIDTE(teamID)();
|
|
if (E.isLeft(team)) return E.left(team.left);
|
|
return E.right(team.right);
|
|
}
|
|
|
|
/**
|
|
* Revoke a team invite by ID
|
|
* @param inviteID Team Invite ID
|
|
* @returns an Either of boolean or error
|
|
*/
|
|
async revokeTeamInviteByID(inviteID: string) {
|
|
const teamInvite = await this.teamInvitationService.revokeInvitation(
|
|
inviteID,
|
|
);
|
|
|
|
if (E.isLeft(teamInvite)) return E.left(teamInvite.left);
|
|
|
|
return E.right(teamInvite.right);
|
|
}
|
|
|
|
/**
|
|
* Fetch all created ShortCodes
|
|
*
|
|
* @param args Pagination arguments
|
|
* @param userEmail User email
|
|
* @returns ShortcodeWithUserEmail
|
|
*/
|
|
async fetchAllShortcodes(
|
|
cursorID: string,
|
|
take: number,
|
|
userEmail: string = null,
|
|
) {
|
|
return this.shortcodeService.fetchAllShortcodes(
|
|
{ cursor: cursorID, take },
|
|
userEmail,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete a Shortcode
|
|
*
|
|
* @param shortcodeID ID of Shortcode being deleted
|
|
* @returns Boolean on successful deletion
|
|
*/
|
|
async deleteShortcode(shortcodeID: string) {
|
|
const result = await this.shortcodeService.deleteShortcode(shortcodeID);
|
|
|
|
if (E.isLeft(result)) return E.left(result.left);
|
|
return E.right(result.right);
|
|
}
|
|
}
|