Files
bigcapital/packages/server/src/services/InviteUsers/TenantInviteUser.ts
2023-05-05 00:28:57 +02:00

171 lines
4.7 KiB
TypeScript

import { Service, Inject } from 'typedi';
import uniqid from 'uniqid';
import moment from 'moment';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import {
ISystemUser,
IUserSendInviteDTO,
IInviteUserService,
ITenantUser,
IUserInvitedEventPayload,
IUserInviteResendEventPayload,
} from '@/interfaces';
import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import RolesService from '@/services/Roles/RolesService';
@Service()
export default class InviteTenantUserService implements IInviteUserService {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: TenancyService;
@Inject()
private rolesService: RolesService;
/**
* Sends invite mail to the given email from the given tenant and user.
* @param {number} tenantId -
* @param {string} email -
* @param {IUser} authorizedUser -
* @return {Promise<IUserInvite>}
*/
public async sendInvite(
tenantId: number,
sendInviteDTO: IUserSendInviteDTO,
authorizedUser: ISystemUser
): Promise<{
invitedUser: ITenantUser;
}> {
const { User } = this.tenancy.models(tenantId);
// Get the given role or throw not found service error.
const role = await this.rolesService.getRoleOrThrowError(
tenantId,
sendInviteDTO.roleId
);
// Validates the given email not exists on the storage.
await this.validateUserEmailNotExists(tenantId, sendInviteDTO.email);
// Generates a new invite token.
const inviteToken = uniqid();
// Creates and fetches a tenant user.
const user = await User.query().insertAndFetch({
email: sendInviteDTO.email,
roleId: sendInviteDTO.roleId,
active: true,
invitedAt: new Date(),
});
// Triggers `onUserSendInvite` event.
await this.eventPublisher.emitAsync(events.inviteUser.sendInvite, {
inviteToken,
authorizedUser,
tenantId,
user,
} as IUserInvitedEventPayload);
return { invitedUser: user };
}
/**
* Re-send user invite.
* @param {number} tenantId -
* @param {string} email -
* @return {Promise<{ invite: IUserInvite }>}
*/
public async resendInvite(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<{ user: ITenantUser }> {
// Retrieve the user by id or throw not found service error.
const user = await this.getUserByIdOrThrowError(tenantId, userId);
// Validate the user is not invited recently.
this.validateUserInviteThrottle(user);
// Validate the given user is not accepted yet.
this.validateInviteUserNotAccept(user);
// Generates a new invite token.
const inviteToken = uniqid();
// Triggers `onUserSendInvite` event.
await this.eventPublisher.emitAsync(events.inviteUser.resendInvite, {
authorizedUser,
tenantId,
user,
inviteToken,
} as IUserInviteResendEventPayload);
return { user };
}
/**
* Validate the given user has no active invite token.
* @param {number} tenantId
* @param {number} userId - User id.
*/
private validateInviteUserNotAccept = (user: ITenantUser) => {
// Throw the error if the one invite tokens is still active.
if (user.inviteAcceptedAt) {
throw new ServiceError(ERRORS.USER_RECENTLY_INVITED);
}
};
/**
* Validates user invite is not invited recently before specific time point.
* @param {ITenantUser} user
*/
private validateUserInviteThrottle = (user: ITenantUser) => {
const PARSE_FORMAT = 'M/D/YYYY, H:mm:ss A';
const beforeTime = moment().subtract(5, 'minutes');
if (moment(user.invitedAt, PARSE_FORMAT).isAfter(beforeTime)) {
throw new ServiceError(ERRORS.USER_RECENTLY_INVITED);
}
};
/**
* Retrieve the given user by id or throw not found service error.
* @param {number} userId - User id.
*/
private getUserByIdOrThrowError = async (
tenantId: number,
userId: number
): Promise<ITenantUser> => {
const { User } = this.tenancy.models(tenantId);
// Retrieve the tenant user.
const user = await User.query().findById(userId);
// Throw if the user not found.
if (!user) {
throw new ServiceError(ERRORS.USER_NOT_FOUND);
}
return user;
};
/**
* Throws error in case the given user email not exists on the storage.
* @param {string} email
* @throws {ServiceError}
*/
private async validateUserEmailNotExists(
tenantId: number,
email: string
): Promise<void> {
const { User } = this.tenancy.models(tenantId);
const foundUser = await User.query().findOne('email', email);
if (foundUser) {
throw new ServiceError(ERRORS.EMAIL_EXISTS);
}
}
}