mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
add server to monorepo.
This commit is contained in:
146
packages/server/src/services/InviteUsers/AcceptInviteUser.ts
Normal file
146
packages/server/src/services/InviteUsers/AcceptInviteUser.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Invite, SystemUser, Tenant } from '@/system/models';
|
||||
import { hashPassword } from 'utils';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InviteUsersMailMessages from '@/services/InviteUsers/InviteUsersMailMessages';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IAcceptInviteEventPayload,
|
||||
IInviteUserInput,
|
||||
ICheckInviteEventPayload,
|
||||
IUserInvite,
|
||||
} from '@/interfaces';
|
||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
||||
import { ERRORS } from './constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export default class AcceptInviteUserService {
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
mailMessages: InviteUsersMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManagerService;
|
||||
|
||||
/**
|
||||
* Accept the received invite.
|
||||
* @param {string} token
|
||||
* @param {IInviteUserInput} inviteUserInput
|
||||
* @throws {ServiceErrors}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async acceptInvite(
|
||||
token: string,
|
||||
inviteUserDTO: IInviteUserInput
|
||||
): Promise<void> {
|
||||
// Retrieve the invite token or throw not found error.
|
||||
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||
|
||||
// Validates the user phone number.
|
||||
await this.validateUserPhoneNumberNotExists(inviteUserDTO.phoneNumber);
|
||||
|
||||
// Hash the given password.
|
||||
const hashedPassword = await hashPassword(inviteUserDTO.password);
|
||||
|
||||
// Retrieve the system user.
|
||||
const user = await SystemUser.query().findOne('email', inviteToken.email);
|
||||
|
||||
// Sets the invited user details after invite accepting.
|
||||
const systemUser = await SystemUser.query().updateAndFetchById(
|
||||
inviteToken.userId,
|
||||
{
|
||||
...inviteUserDTO,
|
||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||
password: hashedPassword,
|
||||
}
|
||||
);
|
||||
// Clear invite token by the given user id.
|
||||
await this.clearInviteTokensByUserId(inviteToken.userId);
|
||||
|
||||
// Triggers `onUserAcceptInvite` event.
|
||||
await this.eventPublisher.emitAsync(events.inviteUser.acceptInvite, {
|
||||
inviteToken,
|
||||
user: systemUser,
|
||||
inviteUserDTO,
|
||||
} as IAcceptInviteEventPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given invite token.
|
||||
* @param {string} token - the given token string.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async checkInvite(
|
||||
token: string
|
||||
): Promise<{ inviteToken: IUserInvite; orgName: object }> {
|
||||
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query()
|
||||
.findById(inviteToken.tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Triggers `onUserCheckInvite` event.
|
||||
await this.eventPublisher.emitAsync(events.inviteUser.checkInvite, {
|
||||
inviteToken,
|
||||
tenant,
|
||||
} as ICheckInviteEventPayload);
|
||||
|
||||
return { inviteToken, orgName: tenant.metadata.name };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve invite model from the given token or throw error.
|
||||
* @param {string} token - Then given token string.
|
||||
* @throws {ServiceError}
|
||||
* @returns {Invite}
|
||||
*/
|
||||
private getInviteTokenOrThrowError = async (
|
||||
token: string
|
||||
): Promise<IUserInvite> => {
|
||||
const inviteToken = await Invite.query()
|
||||
.modify('notExpired')
|
||||
.findOne('token', token);
|
||||
|
||||
if (!inviteToken) {
|
||||
throw new ServiceError(ERRORS.INVITE_TOKEN_INVALID);
|
||||
}
|
||||
return inviteToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the given user email and phone number uniquine.
|
||||
* @param {IInviteUserInput} inviteUserInput
|
||||
*/
|
||||
private validateUserPhoneNumberNotExists = async (
|
||||
phoneNumber: string
|
||||
): Promise<void> => {
|
||||
const foundUser = await SystemUser.query().findOne({ phoneNumber });
|
||||
|
||||
if (foundUser) {
|
||||
throw new ServiceError(ERRORS.PHONE_NUMBER_EXISTS);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear invite tokens of the given user id.
|
||||
* @param {number} userId - User id.
|
||||
*/
|
||||
private clearInviteTokensByUserId = async (userId: number) => {
|
||||
await Invite.query().where('user_id', userId).delete();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
IUserInvitedEventPayload,
|
||||
IUserInviteTenantSyncedEventPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { Inject, Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export default class InviteSendMainNotificationSubscribe {
|
||||
@Inject('agenda')
|
||||
agenda: any;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.inviteUser.sendInviteTenantSynced,
|
||||
this.sendMailNotification
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends mail notification.
|
||||
* @param {IUserInvitedEventPayload} payload
|
||||
*/
|
||||
private sendMailNotification = (
|
||||
payload: IUserInviteTenantSyncedEventPayload
|
||||
) => {
|
||||
const { invite, authorizedUser, tenantId } = payload;
|
||||
|
||||
this.agenda.now('user-invite-mail', {
|
||||
invite,
|
||||
authorizedUser,
|
||||
tenantId,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ISystemUser } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Mail from '@/lib/Mail';
|
||||
import { Service, Container } from 'typedi';
|
||||
import config from '@/config';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersMailMessages {
|
||||
/**
|
||||
* Sends invite mail to the given email.
|
||||
* @param user
|
||||
* @param invite
|
||||
*/
|
||||
async sendInviteMail(tenantId: number, fromUser: ISystemUser, invite: any) {
|
||||
// Retreive tenant orgnaization name.
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const root = __dirname + '/../../../views/images/bigcapital.png';
|
||||
|
||||
const mail = new Mail()
|
||||
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)
|
||||
.setView('mail/UserInvite.html')
|
||||
.setTo(invite.email)
|
||||
.setAttachments([
|
||||
{
|
||||
filename: 'bigcapital.png',
|
||||
path: root,
|
||||
cid: 'bigcapital_logo',
|
||||
},
|
||||
])
|
||||
.setData({
|
||||
root,
|
||||
acceptUrl: `${config.baseURL}/auth/invite/${invite.token}/accept`,
|
||||
fullName: `${fromUser.firstName} ${fromUser.lastName}`,
|
||||
firstName: fromUser.firstName,
|
||||
lastName: fromUser.lastName,
|
||||
email: fromUser.email,
|
||||
organizationName: tenant.metadata.name,
|
||||
});
|
||||
|
||||
await mail.send();
|
||||
}
|
||||
}
|
||||
109
packages/server/src/services/InviteUsers/SyncSystemSendInvite.ts
Normal file
109
packages/server/src/services/InviteUsers/SyncSystemSendInvite.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
IUserInvitedEventPayload,
|
||||
IUserInviteResendEventPayload,
|
||||
IUserInviteTenantSyncedEventPayload,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import { Invite, SystemUser } from '@/system/models';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export default class SyncSystemSendInvite {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.inviteUser.sendInvite, this.syncSendInviteSystem);
|
||||
bus.subscribe(
|
||||
events.inviteUser.resendInvite,
|
||||
this.syncResendInviteSystemUser
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs send invite to system user.
|
||||
* @param {IUserInvitedEventPayload} payload -
|
||||
*/
|
||||
private syncSendInviteSystem = async ({
|
||||
inviteToken,
|
||||
user,
|
||||
tenantId,
|
||||
authorizedUser,
|
||||
}: IUserInvitedEventPayload) => {
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
|
||||
// Creates a new system user.
|
||||
const systemUser = await SystemUser.query().insert({
|
||||
email: user.email,
|
||||
active: user.active,
|
||||
tenantId,
|
||||
});
|
||||
// Creates a invite user token.
|
||||
const invite = await Invite.query().insert({
|
||||
email: user.email,
|
||||
tenantId,
|
||||
userId: systemUser.id,
|
||||
token: inviteToken,
|
||||
});
|
||||
// Links the tenant user with created system user.
|
||||
await User.query().findById(user.id).patch({
|
||||
systemUserId: systemUser.id,
|
||||
});
|
||||
// Triggers `onUserSendInviteTenantSynced` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inviteUser.sendInviteTenantSynced,
|
||||
{
|
||||
invite,
|
||||
tenantId,
|
||||
user,
|
||||
authorizedUser,
|
||||
} as IUserInviteTenantSyncedEventPayload
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Syncs resend invite to system user.
|
||||
* @param {IUserInviteResendEventPayload} payload -
|
||||
*/
|
||||
private syncResendInviteSystemUser = async ({
|
||||
inviteToken,
|
||||
authorizedUser,
|
||||
tenantId,
|
||||
user,
|
||||
}: IUserInviteResendEventPayload) => {
|
||||
// Clear all invite tokens of the given user id.
|
||||
await this.clearInviteTokensByUserId(user.systemUserId, tenantId);
|
||||
|
||||
const invite = await Invite.query().insert({
|
||||
email: user.email,
|
||||
tenantId,
|
||||
userId: user.systemUserId,
|
||||
token: inviteToken,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear invite tokens of the given user id.
|
||||
* @param {number} userId - User id.
|
||||
*/
|
||||
private clearInviteTokensByUserId = async (
|
||||
userId: number,
|
||||
tenantId: number
|
||||
) => {
|
||||
await Invite.query()
|
||||
.where({
|
||||
userId,
|
||||
tenantId,
|
||||
})
|
||||
.delete();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { IAcceptInviteEventPayload } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class SyncTenantAcceptInvite {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.inviteUser.acceptInvite, this.syncTenantAcceptInvite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs accept invite to tenant user.
|
||||
* @param {IAcceptInviteEventPayload} payload -
|
||||
*/
|
||||
private syncTenantAcceptInvite = async ({
|
||||
inviteToken,
|
||||
user,
|
||||
inviteUserDTO,
|
||||
}: IAcceptInviteEventPayload) => {
|
||||
const { User } = this.tenancy.models(inviteToken.tenantId);
|
||||
|
||||
await User.query()
|
||||
.where('systemUserId', inviteToken.userId)
|
||||
.update({
|
||||
...omit(inviteUserDTO, ['password']),
|
||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||
});
|
||||
};
|
||||
}
|
||||
188
packages/server/src/services/InviteUsers/TenantInviteUser.ts
Normal file
188
packages/server/src/services/InviteUsers/TenantInviteUser.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import uniqid from 'uniqid';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InviteUsersMailMessages from '@/services/InviteUsers/InviteUsersMailMessages';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ISystemUser,
|
||||
IUserSendInviteDTO,
|
||||
IInviteUserService,
|
||||
ITenantUser,
|
||||
IUserInvitedEventPayload,
|
||||
IUserInviteResendEventPayload,
|
||||
} from '@/interfaces';
|
||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
||||
import { ERRORS } from './constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import RolesService from '@/services/Roles/RolesService';
|
||||
|
||||
@Service()
|
||||
export default class InviteTenantUserService implements IInviteUserService {
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
mailMessages: InviteUsersMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManagerService;
|
||||
|
||||
@Inject()
|
||||
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;
|
||||
}> {
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/server/src/services/InviteUsers/constants.ts
Normal file
11
packages/server/src/services/InviteUsers/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export const ERRORS = {
|
||||
EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED',
|
||||
INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID',
|
||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||
USER_NOT_FOUND: 'USER_NOT_FOUND',
|
||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||
EMAIL_NOT_EXISTS: 'EMAIL_NOT_EXISTS',
|
||||
USER_RECENTLY_INVITED: 'USER_RECENTLY_INVITED',
|
||||
};
|
||||
Reference in New Issue
Block a user