add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

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

View File

@@ -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,
});
};
}

View File

@@ -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();
}
}

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

View File

@@ -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'),
});
};
}

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

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