fix: issue in mail sender.

This commit is contained in:
a.bouhuolia
2020-12-17 01:16:08 +02:00
parent 3ac6d8897e
commit 46d06bd591
32 changed files with 538 additions and 334 deletions

View File

@@ -4,9 +4,6 @@ MAIL_PASSWORD=172f97b34f1a17
MAIL_PORT=587 MAIL_PORT=587
MAIL_SECURE=false MAIL_SECURE=false
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
SYSTEM_DB_CLIENT=mysql SYSTEM_DB_CLIENT=mysql
SYSTEM_DB_HOST=127.0.0.1 SYSTEM_DB_HOST=127.0.0.1
SYSTEM_DB_USER=root SYSTEM_DB_USER=root

View File

@@ -6,7 +6,7 @@ import parsePhoneNumber from 'libphonenumber-js';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AuthenticationService from 'services/Authentication'; import AuthenticationService from 'services/Authentication';
import { ILoginDTO, ISystemUser, IRegisterOTD } from 'interfaces'; import { ILoginDTO, ISystemUser, IRegisterDTO } from 'interfaces';
import { ServiceError, ServiceErrors } from "exceptions"; import { ServiceError, ServiceErrors } from "exceptions";
import { DATATYPES_LENGTH } from 'data/DataTypes'; import { DATATYPES_LENGTH } from 'data/DataTypes';
import LoginThrottlerMiddleware from 'api/middleware/LoginThrottlerMiddleware'; import LoginThrottlerMiddleware from 'api/middleware/LoginThrottlerMiddleware';
@@ -206,7 +206,7 @@ export default class AuthenticationController extends BaseController{
* @param {Response} res * @param {Response} res
*/ */
async register(req: Request, res: Response, next: Function) { async register(req: Request, res: Response, next: Function) {
const registerDTO: IRegisterOTD = this.matchedBodyData(req); const registerDTO: IRegisterDTO = this.matchedBodyData(req);
try { try {
const registeredUser: ISystemUser = await this.authService.register(registerDTO); const registeredUser: ISystemUser = await this.authService.register(registerDTO);

View File

@@ -1,10 +1,6 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { import { check, body, param } from 'express-validator';
check,
body,
param,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import InviteUserService from 'services/InviteUsers'; import InviteUserService from 'services/InviteUsers';
import { ServiceErrors, ServiceError } from 'exceptions'; import { ServiceErrors, ServiceError } from 'exceptions';
@@ -21,11 +17,11 @@ export default class InviteUsersController extends BaseController {
authRouter() { authRouter() {
const router = Router(); const router = Router();
router.post('/send', [ router.post(
body('email').exists().trim().escape(), '/send',
], [body('email').exists().trim().escape()],
this.validationResult, this.validationResult,
asyncMiddleware(this.sendInvite.bind(this)), asyncMiddleware(this.sendInvite.bind(this))
); );
return router; return router;
} }
@@ -36,15 +32,15 @@ export default class InviteUsersController extends BaseController {
nonAuthRouter() { nonAuthRouter() {
const router = Router(); const router = Router();
router.post('/accept/:token', [ router.post(
...this.inviteUserDTO, '/accept/:token',
], [...this.inviteUserDTO],
this.validationResult, this.validationResult,
asyncMiddleware(this.accept.bind(this)) asyncMiddleware(this.accept.bind(this))
); );
router.get('/invited/:token', [ router.get(
param('token').exists().trim().escape(), '/invited/:token',
], [param('token').exists().trim().escape()],
this.validationResult, this.validationResult,
asyncMiddleware(this.invited.bind(this)) asyncMiddleware(this.invited.bind(this))
); );
@@ -67,9 +63,9 @@ export default class InviteUsersController extends BaseController {
/** /**
* Invite a user to the authorized user organization. * Invite a user to the authorized user organization.
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
async sendInvite(req: Request, res: Response, next: Function) { async sendInvite(req: Request, res: Response, next: Function) {
const { email } = req.body; const { email } = req.body;
@@ -78,11 +74,12 @@ export default class InviteUsersController extends BaseController {
try { try {
await this.inviteUsersService.sendInvite(tenantId, email, user); await this.inviteUsersService.sendInvite(tenantId, email, user);
return res.status(200).send({ return res.status(200).send({
type: 'success', type: 'success',
code: 'INVITE.SENT.SUCCESSFULLY', code: 'INVITE.SENT.SUCCESSFULLY',
message: 'The invite has been sent to the given email.', message: 'The invite has been sent to the given email.',
}) });
} catch (error) { } catch (error) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'email_already_invited') { if (error.errorType === 'email_already_invited') {
@@ -98,9 +95,9 @@ export default class InviteUsersController extends BaseController {
/** /**
* Accept the inviation. * Accept the inviation.
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
async accept(req: Request, res: Response, next: Function) { async accept(req: Request, res: Response, next: Function) {
const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, { const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, {
@@ -135,15 +132,18 @@ export default class InviteUsersController extends BaseController {
/** /**
* Check if the invite token is valid. * Check if the invite token is valid.
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
async invited(req: Request, res: Response, next: Function) { async invited(req: Request, res: Response, next: Function) {
const { token } = req.params; const { token } = req.params;
try { try {
const { inviteToken, orgName } = await this.inviteUsersService.checkInvite(token); const {
inviteToken,
orgName,
} = await this.inviteUsersService.checkInvite(token);
return res.status(200).send({ return res.status(200).send({
inviteToken: inviteToken.token, inviteToken: inviteToken.token,
@@ -151,7 +151,6 @@ export default class InviteUsersController extends BaseController {
organizationName: orgName?.value, organizationName: orgName?.value,
}); });
} catch (error) { } catch (error) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'invite_token_invalid') { if (error.errorType === 'invite_token_invalid') {
return res.status(400).send({ return res.status(400).send({
@@ -162,4 +161,4 @@ export default class InviteUsersController extends BaseController {
next(error); next(error);
} }
} }
} }

View File

@@ -218,7 +218,11 @@ export default class ItemsController extends BaseController {
try { try {
const storedItem = await this.itemsService.newItem(tenantId, itemDTO); const storedItem = await this.itemsService.newItem(tenantId, itemDTO);
return res.status(200).send({ id: storedItem.id });
return res.status(200).send({
id: storedItem.id,
message: 'Item has been created successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -58,9 +58,10 @@ export default class OrganizationController extends BaseController{
*/ */
async build(req: Request, res: Response, next: Function) { async build(req: Request, res: Response, next: Function) {
const { organizationId } = req.tenant; const { organizationId } = req.tenant;
const { user } = req;
try { try {
await this.organizationService.build(organizationId); await this.organizationService.build(organizationId, user);
return res.status(200).send({ return res.status(200).send({
type: 'success', type: 'success',

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response } from 'express' import { Router, Request, Response } from 'express';
import { check, oneOf, ValidationChain } from 'express-validator'; import { check, oneOf, ValidationChain } from 'express-validator';
import basicAuth from 'express-basic-auth'; import basicAuth from 'express-basic-auth';
import config from 'config'; import config from 'config';
@@ -20,42 +20,41 @@ export default class LicensesController extends BaseController {
router() { router() {
const router = Router(); const router = Router();
router.use(basicAuth({ router.use(
users: { basicAuth({
[config.licensesAuth.user]: config.licensesAuth.password, users: {
}, [config.licensesAuth.user]: config.licensesAuth.password,
challenge: true, },
})); challenge: true,
})
);
router.post( router.post(
'/generate', '/generate',
this.generateLicenseSchema, this.generateLicenseSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.validatePlanExistance.bind(this)), asyncMiddleware(this.validatePlanExistance.bind(this)),
asyncMiddleware(this.generateLicense.bind(this)), asyncMiddleware(this.generateLicense.bind(this))
); );
router.post( router.post(
'/disable/:licenseId', '/disable/:licenseId',
this.validationResult, this.validationResult,
asyncMiddleware(this.validateLicenseExistance.bind(this)), asyncMiddleware(this.validateLicenseExistance.bind(this)),
asyncMiddleware(this.validateNotDisabledLicense.bind(this)), asyncMiddleware(this.validateNotDisabledLicense.bind(this)),
asyncMiddleware(this.disableLicense.bind(this)), asyncMiddleware(this.disableLicense.bind(this))
); );
router.post( router.post(
'/send', '/send',
this.sendLicenseSchemaValidation, this.sendLicenseSchemaValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.sendLicense.bind(this)), asyncMiddleware(this.sendLicense.bind(this))
); );
router.delete( router.delete(
'/:licenseId', '/:licenseId',
asyncMiddleware(this.validateLicenseExistance.bind(this)), asyncMiddleware(this.validateLicenseExistance.bind(this)),
asyncMiddleware(this.deleteLicense.bind(this)), asyncMiddleware(this.deleteLicense.bind(this))
);
router.get(
'/',
asyncMiddleware(this.listLicenses.bind(this)),
); );
router.get('/', asyncMiddleware(this.listLicenses.bind(this)));
return router; return router;
} }
@@ -66,9 +65,9 @@ export default class LicensesController extends BaseController {
return [ return [
check('loop').exists().isNumeric().toInt(), check('loop').exists().isNumeric().toInt(),
check('period').exists().isNumeric().toInt(), check('period').exists().isNumeric().toInt(),
check('period_interval').exists().isIn([ check('period_interval')
'month', 'months', 'year', 'years', 'day', 'days' .exists()
]), .isIn(['month', 'months', 'year', 'years', 'day', 'days']),
check('plan_id').exists().isNumeric().toInt(), check('plan_id').exists().isNumeric().toInt(),
]; ];
} }
@@ -78,12 +77,11 @@ export default class LicensesController extends BaseController {
*/ */
get specificLicenseSchema(): ValidationChain[] { get specificLicenseSchema(): ValidationChain[] {
return [ return [
oneOf([ oneOf(
check('license_id').exists().isNumeric().toInt(), [check('license_id').exists().isNumeric().toInt()],
], [ [check('license_code').exists().isNumeric().toInt()]
check('license_code').exists().isNumeric().toInt(), ),
]) ];
]
} }
/** /**
@@ -103,9 +101,9 @@ export default class LicensesController extends BaseController {
/** /**
* Validate the plan existance on the storage. * Validate the plan existance on the storage.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async validatePlanExistance(req: Request, res: Response, next: Function) { async validatePlanExistance(req: Request, res: Response, next: Function) {
const body = this.matchedBodyData(req); const body = this.matchedBodyData(req);
@@ -122,8 +120,8 @@ export default class LicensesController extends BaseController {
/** /**
* Valdiate the license existance on the storage. * Valdiate the license existance on the storage.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {Function} * @param {Function}
*/ */
async validateLicenseExistance(req: Request, res: Response, next: Function) { async validateLicenseExistance(req: Request, res: Response, next: Function) {
@@ -142,11 +140,15 @@ export default class LicensesController extends BaseController {
/** /**
* Validates whether the license id is disabled. * Validates whether the license id is disabled.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async validateNotDisabledLicense(req: Request, res: Response, next: Function) { async validateNotDisabledLicense(
req: Request,
res: Response,
next: Function
) {
const licenseId = req.params.licenseId || req.query.licenseId; const licenseId = req.params.licenseId || req.query.licenseId;
const foundLicense = await License.query().findById(licenseId); const foundLicense = await License.query().findById(licenseId);
@@ -160,31 +162,36 @@ export default class LicensesController extends BaseController {
/** /**
* Generate licenses codes with given period in bulk. * Generate licenses codes with given period in bulk.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async generateLicense(req: Request, res: Response, next: Function) { async generateLicense(req: Request, res: Response, next: Function) {
const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(req); const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(
req
);
try { try {
await this.licenseService.generateLicenses( await this.licenseService.generateLicenses(
loop, period, periodInterval, planId, loop,
period,
periodInterval,
planId
); );
return res.status(200).send({ return res.status(200).send({
code: 100, code: 100,
type: 'LICENSEES.GENERATED.SUCCESSFULLY', type: 'LICENSEES.GENERATED.SUCCESSFULLY',
message: 'The licenses have been generated successfully.' message: 'The licenses have been generated successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
} }
} }
/** /**
* Disable the given license on the storage. * Disable the given license on the storage.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async disableLicense(req: Request, res: Response) { async disableLicense(req: Request, res: Response) {
@@ -197,8 +204,8 @@ export default class LicensesController extends BaseController {
/** /**
* Deletes the given license code on the storage. * Deletes the given license code on the storage.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async deleteLicense(req: Request, res: Response) { async deleteLicense(req: Request, res: Response) {
@@ -211,13 +218,19 @@ export default class LicensesController extends BaseController {
/** /**
* Send license code in the given period to the customer via email or phone number * Send license code in the given period to the customer via email or phone number
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async sendLicense(req: Request, res: Response) { async sendLicense(req: Request, res: Response) {
const { phoneNumber, email, period, periodInterval, planId } = this.matchedBodyData(req); const {
phoneNumber,
email,
period,
periodInterval,
planId,
} = this.matchedBodyData(req);
const license = await License.query() const license = await License.query()
.modify('filterActiveLicense') .modify('filterActiveLicense')
.where('license_period', period) .where('license_period', period)
@@ -228,12 +241,15 @@ export default class LicensesController extends BaseController {
if (!license) { if (!license) {
return res.status(400).send({ return res.status(400).send({
status: 110, status: 110,
message: 'There is no licenses availiable right now with the given period and plan.', message:
'There is no licenses availiable right now with the given period and plan.',
code: 'NO.AVALIABLE.LICENSE.CODE', code: 'NO.AVALIABLE.LICENSE.CODE',
}); });
} }
await this.licenseService.sendLicenseToCustomer( await this.licenseService.sendLicenseToCustomer(
license.licenseCode, phoneNumber, email, license.licenseCode,
phoneNumber,
email
); );
return res.status(200).send({ return res.status(200).send({
status: 100, status: 100,
@@ -244,8 +260,8 @@ export default class LicensesController extends BaseController {
/** /**
* Listing licenses. * Listing licenses.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async listLicenses(req: Request, res: Response) { async listLicenses(req: Request, res: Response) {
const filter: ILicensesFilter = { const filter: ILicensesFilter = {
@@ -255,11 +271,10 @@ export default class LicensesController extends BaseController {
active: false, active: false,
...req.query, ...req.query,
}; };
const licenses = await License.query() const licenses = await License.query().onBuild((builder) => {
.onBuild((builder) => { builder.modify('filter', filter);
builder.modify('filter', filter); builder.orderBy('createdAt', 'ASC');
builder.orderBy('createdAt', 'ASC'); });
});
return res.status(200).send({ licenses }); return res.status(200).send({ licenses });
} }
} }

View File

@@ -57,7 +57,7 @@ export default {
mail: { mail: {
host: process.env.MAIL_HOST, host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT, port: process.env.MAIL_PORT,
secure: process.env.MAIL_SECURE, secure: !!parseInt(process.env.MAIL_SECURE, 10),
username: process.env.MAIL_USERNAME, username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD, password: process.env.MAIL_PASSWORD,
}, },
@@ -105,7 +105,11 @@ export default {
/** /**
* *
*/ */
contactUsMail: process.env.CONTACT_US_MAIL, customerSuccess: {
email: 'success@bigcapital.ly',
phoneNumber: '(218) 92 791 8381'
},
baseURL: process.env.BASE_URL, baseURL: process.env.BASE_URL,
/** /**

View File

@@ -0,0 +1,16 @@
export interface IMailable {
constructor(
view: string,
data?: { [key: string]: string | number },
);
send(): Promise<any>;
build(): void;
setData(data: { [key: string]: string | number }): IMailable;
setTo(to: string): IMailable;
setFrom(from: string): IMailable;
setSubject(subject: string): IMailable;
setView(view: string): IMailable;
render(data?: { [key: string]: string | number }): string;
getViewContent(): string;
}

View File

@@ -35,5 +35,5 @@ export * from './TrialBalanceSheet';
export * from './GeneralLedgerSheet' export * from './GeneralLedgerSheet'
export * from './ProfitLossSheet'; export * from './ProfitLossSheet';
export * from './JournalReport'; export * from './JournalReport';
export * from './ARAgingSummaryReport';
export * from './ARAgingSummaryReport'; export * from './Mailable';

View File

@@ -2,19 +2,31 @@ import { Container } from 'typedi';
import LicenseService from 'services/Payment/License'; import LicenseService from 'services/Payment/License';
export default class SendLicenseViaEmailJob { export default class SendLicenseViaEmailJob {
/**
* Constructor method.
* @param agenda
*/
constructor(agenda) {
agenda.define(
'send-license-via-email',
{ priority: 'high', concurrency: 1, },
this.handler,
);
}
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const licenseService = Container.get(LicenseService); const licenseService = Container.get(LicenseService);
const { email, licenseCode } = job.attrs.data; const { email, licenseCode } = job.attrs.data;
Logger.debug(`Send license via email - started: ${job.attrs.data}`); Logger.info(`[send_license_via_mail] started: ${job.attrs.data}`);
try { try {
await licenseService.mailMessages.sendMailLicense(licenseCode, email); await licenseService.mailMessages.sendMailLicense(licenseCode, email);
Logger.debug(`Send license via email - completed: ${job.attrs.data}`); Logger.info(`[send_license_via_mail] completed: ${job.attrs.data}`);
done(); done();
} catch(e) { } catch(e) {
Logger.error(`Send license via email: ${job.attrs.data}, error: ${e}`); Logger.error(`[send_license_via_mail] ${job.attrs.data}, error: ${e}`);
done(e); done(e);
} }
} }

View File

@@ -2,6 +2,17 @@ import { Container } from 'typedi';
import LicenseService from 'services/Payment/License'; import LicenseService from 'services/Payment/License';
export default class SendLicenseViaPhoneJob { export default class SendLicenseViaPhoneJob {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define(
'send-license-via-phone',
{ priority: 'high', concurrency: 1, },
this.handler,
);
}
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const { phoneNumber, licenseCode } = job.attrs.data; const { phoneNumber, licenseCode } = job.attrs.data;

View File

@@ -5,6 +5,18 @@ export default class UserInviteMailJob {
@Inject() @Inject()
inviteUsersService: InviteUserService; inviteUsersService: InviteUserService;
/**
* Constructor method.
* @param {Agenda} agenda
*/
constructor(agenda) {
agenda.define(
'user-invite-mail',
{ priority: 'high' },
this.handler.bind(this),
);
}
/** /**
* Handle invite user job. * Handle invite user job.
* @param {Job} job * @param {Job} job

View File

@@ -1,4 +1,4 @@
import { Container, Inject } from 'typedi'; import { Container } from 'typedi';
import AuthenticationService from 'services/Authentication'; import AuthenticationService from 'services/Authentication';
export default class WelcomeEmailJob { export default class WelcomeEmailJob {
@@ -21,18 +21,18 @@ export default class WelcomeEmailJob {
* @param {Function} done * @param {Function} done
*/ */
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const { organizationName, user } = job.attrs.data; const { organizationId, user } = job.attrs.data;
const Logger: any = Container.get('logger'); const Logger: any = Container.get('logger');
const authService = Container.get(AuthenticationService); const authService = Container.get(AuthenticationService);
Logger.info(`[welcome_mail] send welcome mail message - started: ${job.attrs.data}`); Logger.info(`[welcome_mail] started: ${job.attrs.data}`);
try { try {
await authService.mailMessages.sendWelcomeMessage(user, organizationName); await authService.mailMessages.sendWelcomeMessage(user, organizationId);
Logger.info(`[welcome_mail] send welcome mail message - finished: ${job.attrs.data}`); Logger.info(`[welcome_mail] finished: ${job.attrs.data}`);
done(); done();
} catch (error) { } catch (error) {
Logger.info(`[welcome_mail] send welcome mail message - error: ${job.attrs.data}, error: ${error}`); Logger.error(`[welcome_mail] error: ${job.attrs.data}, error: ${error}`);
done(error); done(error);
} }
} }

View File

@@ -0,0 +1,102 @@
import fs from 'fs';
import Mustache from 'mustache';
import { Container } from 'typedi';
import path from 'path';
import { IMailable } from 'interfaces';
export default class Mail{
view: string;
subject: string;
to: string;
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
data: { [key: string]: string | number };
/**
* Mail options.
*/
private get mailOptions() {
return {
to: this.to,
from: this.from,
subject: this.subject,
html: this.render(this.data),
};
}
/**
* Sends the given mail to the target address.
*/
public send() {
return new Promise((resolve, reject) => {
const Mail = Container.get('mail');
Mail.sendMail(this.mailOptions, (error) => {
if (error) {
reject(error);
return;
}
resolve(true);
});
});
}
/**
* Set send mail to address.
* @param {string} to -
*/
setTo(to: string) {
this.to = to;
return this;
}
/**
* Sets from address to the mail.
* @param {string} from
* @return {}
*/
private setFrom(from: string) {
this.from = from;
return this;
}
/**
* Set mail subject.
* @param {string} subject
*/
setSubject(subject: string) {
this.subject = subject;
return this;
}
/**
* Set view directory.
* @param {string} view
*/
setView(view: string) {
this.view = view;
return this;
}
setData(data) {
this.data = data;
return this;
}
/**
* Renders the view template with the given data.
* @param {object} data
* @return {string}
*/
render(data): string {
const viewContent = this.getViewContent();
return Mustache.render(viewContent, data);
}
/**
* Retrieve view content from the view directory.
*/
private getViewContent(): string {
const filePath = path.join(global.__root, `../views/${this.view}`);
return fs.readFileSync(filePath, 'utf8');
}
}

View File

@@ -16,13 +16,10 @@ export default ({ agenda }: { agenda: Agenda }) => {
new WelcomeEmailJob(agenda); new WelcomeEmailJob(agenda);
new ResetPasswordMailJob(agenda); new ResetPasswordMailJob(agenda);
new WelcomeSMSJob(agenda); new WelcomeSMSJob(agenda);
new UserInviteMailJob(agenda);
// User invite mail. new SendLicenseViaEmailJob(agenda);
agenda.define( new SendLicenseViaPhoneJob(agenda);
'user-invite-mail',
{ priority: 'high' },
new UserInviteMailJob().handler,
)
agenda.define( agenda.define(
'compute-item-cost', 'compute-item-cost',
{ priority: 'high', concurrency: 20 }, { priority: 'high', concurrency: 20 },
@@ -33,16 +30,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
{ priority: 'normal', concurrency: 1, }, { priority: 'normal', concurrency: 1, },
new RewriteInvoicesJournalEntries().handler, new RewriteInvoicesJournalEntries().handler,
); );
agenda.define(
'send-license-via-phone',
{ priority: 'high', concurrency: 1, },
new SendLicenseViaPhoneJob().handler,
);
agenda.define(
'send-license-via-email',
{ priority: 'high', concurrency: 1, },
new SendLicenseViaEmailJob().handler,
);
agenda.define( agenda.define(
'send-sms-notification-subscribe-end', 'send-sms-notification-subscribe-end',
{ priority: 'nromal', concurrency: 1, }, { priority: 'nromal', concurrency: 1, },

View File

@@ -1,9 +1,8 @@
import fs from 'fs';
import { Service, Container } from "typedi"; import { Service } from "typedi";
import Mustache from 'mustache';
import path from 'path';
import { ISystemUser } from 'interfaces'; import { ISystemUser } from 'interfaces';
import config from 'config'; import config from 'config';
import Mail from "lib/Mail";
@Service() @Service()
export default class AuthenticationMailMesssages { export default class AuthenticationMailMesssages {
@@ -13,31 +12,23 @@ export default class AuthenticationMailMesssages {
* @param {string} organizationName - * @param {string} organizationName -
* @return {Promise<void>} * @return {Promise<void>}
*/ */
sendWelcomeMessage(user: ISystemUser, organizationName: string): Promise<void> { async sendWelcomeMessage(
const Mail = Container.get('mail'); user: ISystemUser,
organizationId: string
const filePath = path.join(global.__root, 'views/mail/Welcome.html'); ): Promise<void> {
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, { const mail = new Mail()
email: user.email, .setView('mail/Welcome.html')
firstName: user.firstName, .setSubject('Welcome to Bigcapital')
organizationName, .setTo(user.email)
}); .setData({
const mailOptions = { firstName: user.firstName,
to: user.email, organizationId,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, successPhoneNumber: config.customerSuccess.phoneNumber,
subject: 'Welcome to Bigcapital', successEmail: config.customerSuccess.email,
html: rendered,
};
return new Promise((resolve, reject) => {
Mail.sendMail(mailOptions, (error) => {
if (error) {
resolve(error);
return;
}
reject();
}); });
});
await mail.send();
} }
/** /**
@@ -46,31 +37,22 @@ export default class AuthenticationMailMesssages {
* @param {string} token - Reset password token. * @param {string} token - Reset password token.
* @return {Promise<void>} * @return {Promise<void>}
*/ */
sendResetPasswordMessage(user: ISystemUser, token: string): Promise<void> { async sendResetPasswordMessage(
const Mail = Container.get('mail'); user: ISystemUser,
token: string
): Promise<void> {
const filePath = path.join(global.__root, 'views/mail/ResetPassword.html'); const mail = new Mail()
const template = fs.readFileSync(filePath, 'utf8'); .setSubject('Bigcapital - Password Reset')
const rendered = Mustache.render(template, { .setView('mail/ResetPassword.html')
resetPasswordUrl: `${config.baseURL}/reset/${token}`, .setTo(user.email)
first_name: user.firstName, .setData({
last_name: user.lastName, resetPasswordUrl: `${config.baseURL}/reset/${token}`,
contact_us_email: config.contactUsMail, first_name: user.firstName,
}); last_name: user.lastName,
const mailOptions = { contact_us_email: config.contactUsMail,
to: user.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Bigcapital - Password Reset',
html: rendered,
};
return new Promise((resolve, reject) => {
Mail.sendMail(mailOptions, (error) => {
if (error) {
reject(error);
return;
}
resolve();
}); });
});
await mail.send();
} }
} }

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from 'typedi';
import { ISystemUser, ITenant } from "interfaces"; import { ISystemUser, ITenant } from 'interfaces';
@Service() @Service()
export default class AuthenticationSMSMessages { export default class AuthenticationSMSMessages {
@@ -8,12 +8,12 @@ export default class AuthenticationSMSMessages {
/** /**
* Sends welcome sms message. * Sends welcome sms message.
* @param {ITenant} tenant * @param {ITenant} tenant
* @param {ISystemUser} user * @param {ISystemUser} user
*/ */
sendWelcomeMessage(tenant: ITenant, user: ISystemUser) { sendWelcomeMessage(tenant: ITenant, user: ISystemUser) {
const message: string = `Hi ${user.firstName}, Welcome to Bigcapital, You've joined the new workspace, if you need any help please don't hesitate to contact us.` const message: string = `Hi ${user.firstName}, Welcome to Bigcapital, You've joined the new workspace, if you need any help please don't hesitate to contact us.`;
return this.smsClient.sendMessage(user.phoneNumber, message); return this.smsClient.sendMessage(user.phoneNumber, message);
} }
} }

View File

@@ -1,8 +1,8 @@
import { Service, Inject, Container } from "typedi"; import { Service, Inject, Container } from 'typedi';
import JWT from 'jsonwebtoken'; import JWT from 'jsonwebtoken';
import uniqid from 'uniqid'; import uniqid from 'uniqid';
import { omit } from 'lodash'; import { omit } from 'lodash';
import moment from "moment"; import moment from 'moment';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -46,7 +46,7 @@ export default class AuthenticationService implements IAuthenticationService {
/** /**
* Signin and generates JWT token. * Signin and generates JWT token.
* @throws {ServiceError} * @throws {ServiceError}
* @param {string} emailOrPhone - Email or phone number. * @param {string} emailOrPhone - Email or phone number.
* @param {string} password - Password. * @param {string} password - Password.
* @return {Promise<{user: IUser, token: string}>} * @return {Promise<{user: IUser, token: string}>}
*/ */
@@ -54,11 +54,14 @@ export default class AuthenticationService implements IAuthenticationService {
emailOrPhone: string, emailOrPhone: string,
password: string password: string
): Promise<{ ): Promise<{
user: ISystemUser, user: ISystemUser;
token: string, token: string;
tenant: ITenant tenant: ITenant;
}> { }> {
this.logger.info('[login] Someone trying to login.', { emailOrPhone, password }); this.logger.info('[login] Someone trying to login.', {
emailOrPhone,
password,
});
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const loginThrottler = Container.get('rateLimiter.login'); const loginThrottler = Container.get('rateLimiter.login');
@@ -72,7 +75,10 @@ export default class AuthenticationService implements IAuthenticationService {
throw new ServiceError('invalid_details'); throw new ServiceError('invalid_details');
} }
this.logger.info('[login] check password validation.', { emailOrPhone, password }); this.logger.info('[login] check password validation.', {
emailOrPhone,
password,
});
if (!user.verifyPassword(password)) { if (!user.verifyPassword(password)) {
await loginThrottler.hit(emailOrPhone); await loginThrottler.hit(emailOrPhone);
@@ -87,14 +93,18 @@ export default class AuthenticationService implements IAuthenticationService {
this.logger.info('[login] generating JWT token.', { userId: user.id }); this.logger.info('[login] generating JWT token.', { userId: user.id });
const token = this.generateToken(user); const token = this.generateToken(user);
this.logger.info('[login] updating user last login at.', { userId: user.id }); this.logger.info('[login] updating user last login at.', {
userId: user.id,
});
await systemUserRepository.patchLastLoginAt(user.id); await systemUserRepository.patchLastLoginAt(user.id);
this.logger.info('[login] Logging success.', { user, token }); this.logger.info('[login] Logging success.', { user, token });
// Triggers `onLogin` event. // Triggers `onLogin` event.
this.eventDispatcher.dispatch(events.auth.login, { this.eventDispatcher.dispatch(events.auth.login, {
emailOrPhone, password, user, emailOrPhone,
password,
user,
}); });
const tenant = await user.$relatedQuery('tenant'); const tenant = await user.$relatedQuery('tenant');
@@ -111,8 +121,12 @@ export default class AuthenticationService implements IAuthenticationService {
*/ */
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) { private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.getByEmail(registerDTO.email); const isEmailExists = await systemUserRepository.getByEmail(
const isPhoneExists = await systemUserRepository.getByPhoneNumber(registerDTO.phoneNumber); registerDTO.email
);
const isPhoneExists = await systemUserRepository.getByPhoneNumber(
registerDTO.phoneNumber
);
const errorReasons: ServiceError[] = []; const errorReasons: ServiceError[] = [];
@@ -132,7 +146,7 @@ export default class AuthenticationService implements IAuthenticationService {
/** /**
* Registers a new tenant with user from user input. * Registers a new tenant with user from user input.
* @throws {ServiceErrors} * @throws {ServiceErrors}
* @param {IUserDTO} user * @param {IUserDTO} user
*/ */
public async register(registerDTO: IRegisterDTO): Promise<ISystemUser> { public async register(registerDTO: IRegisterDTO): Promise<ISystemUser> {
this.logger.info('[register] Someone trying to register.'); this.logger.info('[register] Someone trying to register.');
@@ -141,7 +155,7 @@ export default class AuthenticationService implements IAuthenticationService {
this.logger.info('[register] Creating a new tenant organization.'); this.logger.info('[register] Creating a new tenant organization.');
const tenant = await this.newTenantOrganization(); const tenant = await this.newTenantOrganization();
this.logger.info('[register] Trying hashing the password.') this.logger.info('[register] Trying hashing the password.');
const hashedPassword = await hashPassword(registerDTO.password); const hashedPassword = await hashPassword(registerDTO.password);
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
@@ -153,7 +167,9 @@ export default class AuthenticationService implements IAuthenticationService {
}); });
// Triggers `onRegister` event. // Triggers `onRegister` event.
this.eventDispatcher.dispatch(events.auth.register, { this.eventDispatcher.dispatch(events.auth.register, {
registerDTO, user: registeredUser registerDTO,
tenant,
user: registeredUser,
}); });
return registeredUser; return registeredUser;
} }
@@ -170,14 +186,14 @@ export default class AuthenticationService implements IAuthenticationService {
/** /**
* Validate the given email existance on the storage. * Validate the given email existance on the storage.
* @throws {ServiceError} * @throws {ServiceError}
* @param {string} email - email address. * @param {string} email - email address.
*/ */
private async validateEmailExistance(email: string): Promise<ISystemUser> { private async validateEmailExistance(email: string): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.getByEmail(email); const userByEmail = await systemUserRepository.getByEmail(email);
if (!userByEmail) { if (!userByEmail) {
this.logger.info('[send_reset_password] The given email not found.'); this.logger.info('[send_reset_password] The given email not found.');
throw new ServiceError('email_not_found'); throw new ServiceError('email_not_found');
} }
return userByEmail; return userByEmail;
@@ -185,7 +201,7 @@ export default class AuthenticationService implements IAuthenticationService {
/** /**
* Generates and retrieve password reset token for the given user email. * Generates and retrieve password reset token for the given user email.
* @param {string} email * @param {string} email
* @return {<Promise<IPasswordReset>} * @return {<Promise<IPasswordReset>}
*/ */
public async sendResetPassword(email: string): Promise<IPasswordReset> { public async sendResetPassword(email: string): Promise<IPasswordReset> {
@@ -193,7 +209,9 @@ export default class AuthenticationService implements IAuthenticationService {
const user = await this.validateEmailExistance(email); const user = await this.validateEmailExistance(email);
// Delete all stored tokens of reset password that associate to the give email. // Delete all stored tokens of reset password that associate to the give email.
this.logger.info('[send_reset_password] trying to delete all tokens by email.'); this.logger.info(
'[send_reset_password] trying to delete all tokens by email.'
);
this.deletePasswordResetToken(email); this.deletePasswordResetToken(email);
const token: string = uniqid(); const token: string = uniqid();
@@ -202,8 +220,10 @@ export default class AuthenticationService implements IAuthenticationService {
const passwordReset = await PasswordReset.query().insert({ email, token }); const passwordReset = await PasswordReset.query().insert({ email, token });
// Triggers `onSendResetPassword` event. // Triggers `onSendResetPassword` event.
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token }); this.eventDispatcher.dispatch(events.auth.sendResetPassword, {
user,
token,
});
return passwordReset; return passwordReset;
} }
@@ -215,14 +235,20 @@ export default class AuthenticationService implements IAuthenticationService {
*/ */
public async resetPassword(token: string, password: string): Promise<void> { public async resetPassword(token: string, password: string): Promise<void> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const tokenModel: IPasswordReset = await PasswordReset.query().findOne('token', token); const tokenModel: IPasswordReset = await PasswordReset.query().findOne(
'token',
token
);
if (!tokenModel) { if (!tokenModel) {
this.logger.info('[reset_password] token invalid.'); this.logger.info('[reset_password] token invalid.');
throw new ServiceError('token_invalid'); throw new ServiceError('token_invalid');
} }
// Different between tokne creation datetime and current time. // Different between tokne creation datetime and current time.
if (moment().diff(tokenModel.createdAt, 'seconds') > config.resetPasswordSeconds) { if (
moment().diff(tokenModel.createdAt, 'seconds') >
config.resetPasswordSeconds
) {
this.logger.info('[reset_password] token expired.'); this.logger.info('[reset_password] token expired.');
// Deletes the expired token by expired token email. // Deletes the expired token by expired token email.
@@ -235,7 +261,7 @@ export default class AuthenticationService implements IAuthenticationService {
throw new ServiceError('user_not_found'); throw new ServiceError('user_not_found');
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
this.logger.info('[reset_password] saving a new hashed password.'); this.logger.info('[reset_password] saving a new hashed password.');
await systemUserRepository.edit(user.id, { password: hashedPassword }); await systemUserRepository.edit(user.id, { password: hashedPassword });
@@ -243,13 +269,17 @@ export default class AuthenticationService implements IAuthenticationService {
await this.deletePasswordResetToken(tokenModel.email); await this.deletePasswordResetToken(tokenModel.email);
// Triggers `onResetPassword` event. // Triggers `onResetPassword` event.
this.eventDispatcher.dispatch(events.auth.resetPassword, { user, token, password }); this.eventDispatcher.dispatch(events.auth.resetPassword, {
user,
token,
password,
});
this.logger.info('[reset_password] reset password success.'); this.logger.info('[reset_password] reset password success.');
} }
/** /**
* Deletes the password reset token by the given email. * Deletes the password reset token by the given email.
* @param {string} email * @param {string} email
* @returns {Promise} * @returns {Promise}
*/ */
private async deletePasswordResetToken(email: string) { private async deletePasswordResetToken(email: string) {
@@ -259,7 +289,7 @@ export default class AuthenticationService implements IAuthenticationService {
/** /**
* Generates JWT token for the given user. * Generates JWT token for the given user.
* @param {ISystemUser} user * @param {ISystemUser} user
* @return {string} token * @return {string} token
*/ */
generateToken(user: ISystemUser): string { generateToken(user: ISystemUser): string {
@@ -273,7 +303,7 @@ export default class AuthenticationService implements IAuthenticationService {
id: user.id, // We are gonna use this in the middleware 'isAuth' id: user.id, // We are gonna use this in the middleware 'isAuth'
exp: exp.getTime() / 1000, exp: exp.getTime() / 1000,
}, },
config.jwtSecret, config.jwtSecret
); );
} }
} }

View File

@@ -1,31 +1,29 @@
import { IInviteUserInput, ISystemUser } from "interfaces";
import Mail from "lib/Mail";
import { Service } from "typedi"; import { Service } from "typedi";
@Service() @Service()
export default class InviteUsersMailMessages { export default class InviteUsersMailMessages {
sendInviteMail() { /**
const filePath = path.join(global.__root, 'views/mail/UserInvite.html'); * Sends invite mail to the given email.
const template = fs.readFileSync(filePath, 'utf8'); * @param user
* @param invite
const rendered = Mustache.render(template, { */
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`, async sendInviteMail(user: ISystemUser, invite) {
fullName: `${user.firstName} ${user.lastName}`, const mail = new Mail()
firstName: user.firstName, .setSubject(`${user.fullName} has invited you to join a Bigcapital`)
lastName: user.lastName, .setView('mail/UserInvite.html')
email: user.email, .setData({
organizationName: organizationOptions.getMeta('organization_name'), acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
}); fullName: `${user.firstName} ${user.lastName}`,
const mailOptions = { firstName: user.firstName,
to: user.email, lastName: user.lastName,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, email: user.email,
subject: `${user.fullName} has invited you to join a Bigcapital`, organizationName: organizationOptions.getMeta('organization_name'),
html: rendered, });
};
mail.sendMail(mailOptions, (error) => { await mail.send();
if (error) { Logger.log('info', 'User has been sent invite user email successfuly.');
Logger.log('error', 'Failed send user invite mail', { error, form });
}
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
});
} }
} }

View File

@@ -1,21 +1,18 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from 'typedi';
import uniqid from 'uniqid'; import uniqid from 'uniqid';
import moment from 'moment'; import moment from 'moment';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
} from 'decorators/eventDispatcher'; } from 'decorators/eventDispatcher';
import { ServiceError } from "exceptions"; import { ServiceError } from 'exceptions';
import { Invite, Tenant } from "system/models"; import { Invite, Tenant } from 'system/models';
import { Option } from 'models'; import { Option } from 'models';
import { hashPassword } from 'utils'; import { hashPassword } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import InviteUsersMailMessages from "services/InviteUsers/InviteUsersMailMessages"; import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages';
import events from 'subscribers/events'; import events from 'subscribers/events';
import { import { ISystemUser, IInviteUserInput } from 'interfaces';
ISystemUser,
IInviteUserInput,
} from 'interfaces';
@Service() @Service()
export default class InviteUserService { export default class InviteUserService {
@@ -36,12 +33,15 @@ export default class InviteUserService {
/** /**
* Accept the received invite. * Accept the received invite.
* @param {string} token * @param {string} token
* @param {IInviteUserInput} inviteUserInput * @param {IInviteUserInput} inviteUserInput
* @throws {ServiceErrors} * @throws {ServiceErrors}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void> { async acceptInvite(
token: string,
inviteUserInput: IInviteUserInput
): Promise<void> {
const inviteToken = await this.getInviteOrThrowError(token); const inviteToken = await this.getInviteOrThrowError(token);
await this.validateUserPhoneNumber(inviteUserInput); await this.validateUserPhoneNumber(inviteUserInput);
@@ -61,14 +61,20 @@ export default class InviteUserService {
}); });
this.logger.info('[accept_invite] trying to delete the given token.'); this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete(); const deleteInviteTokenOper = Invite.query()
.where('token', inviteToken.token)
.delete();
// Await all async operations. // Await all async operations.
const [updatedUser] = await Promise.all([updateUserOper, deleteInviteTokenOper]); const [updatedUser] = await Promise.all([
updateUserOper,
deleteInviteTokenOper,
]);
// Triggers `onUserAcceptInvite` event. // Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, { this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken, user: updatedUser, inviteToken,
user: updatedUser,
}); });
} }
@@ -77,10 +83,17 @@ export default class InviteUserService {
* @param {number} tenantId - * @param {number} tenantId -
* @param {string} email - * @param {string} email -
* @param {IUser} authorizedUser - * @param {IUser} authorizedUser -
* *
* @return {Promise<IInvite>} * @return {Promise<IInvite>}
*/ */
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<{ invite: IInvite, user: ISystemUser }> { public async sendInvite(
tenantId: number,
email: string,
authorizedUser: ISystemUser
): Promise<{
invite: IInvite,
user: ISystemUser
}> {
await this.throwErrorIfUserEmailExists(email); await this.throwErrorIfUserEmailExists(email);
this.logger.info('[send_invite] trying to store invite token.'); this.logger.info('[send_invite] trying to store invite token.');
@@ -90,7 +103,9 @@ export default class InviteUserService {
token: uniqid(), token: uniqid(),
}); });
this.logger.info('[send_invite] trying to store user with email and tenant.'); this.logger.info(
'[send_invite] trying to store user with email and tenant.'
);
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.create({ const user = await systemUserRepository.create({
email, email,
@@ -100,39 +115,45 @@ export default class InviteUserService {
// Triggers `onUserSendInvite` event. // Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, { this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
invite, invite,
}); });
return { invite, user }; return { invite, user };
} }
/** /**
* Validate the given invite token. * Validate the given invite token.
* @param {string} token - the given token string. * @param {string} token - the given token string.
* @throws {ServiceError} * @throws {ServiceError}
*/ */
public async checkInvite(token: string): Promise<{ inviteToken: string, orgName: object}> { public async checkInvite(
const inviteToken = await this.getInviteOrThrowError(token) token: string
): Promise<{ inviteToken: string; orgName: object }> {
const inviteToken = await this.getInviteOrThrowError(token);
// Find the tenant that associated to the given token. // Find the tenant that associated to the given token.
const tenant = await Tenant.query().findOne('id', inviteToken.tenantId); const tenant = await Tenant.query().findOne('id', inviteToken.tenantId);
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId); const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
const orgName = await Option.bindKnex(tenantDb).query() const orgName = await Option.bindKnex(tenantDb)
.findOne('key', 'organization_name') .query()
.findOne('key', 'organization_name');
// Triggers `onUserCheckInvite` event. // Triggers `onUserCheckInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.checkInvite, { this.eventDispatcher.dispatch(events.inviteUser.checkInvite, {
inviteToken, orgName, inviteToken,
orgName,
}); });
return { inviteToken, orgName }; return { inviteToken, orgName };
} }
/** /**
* Throws error in case the given user email not exists on the storage. * Throws error in case the given user email not exists on the storage.
* @param {string} email * @param {string} email
*/ */
private async throwErrorIfUserEmailExists(email: string): Promise<ISystemUser> { private async throwErrorIfUserEmailExists(
email: string
): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByEmail(email); const foundUser = await systemUserRepository.getByEmail(email);
@@ -160,14 +181,18 @@ export default class InviteUserService {
/** /**
* Validate the given user email and phone number uniquine. * Validate the given user email and phone number uniquine.
* @param {IInviteUserInput} inviteUserInput * @param {IInviteUserInput} inviteUserInput
*/ */
private async validateUserPhoneNumber(inviteUserInput: IInviteUserInput): Promise<ISystemUser> { private async validateUserPhoneNumber(
inviteUserInput: IInviteUserInput
): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByPhoneNumber(inviteUserInput.phoneNumber) const foundUser = await systemUserRepository.getByPhoneNumber(
inviteUserInput.phoneNumber
);
if (foundUser) { if (foundUser) {
throw new ServiceError('phone_number_exists'); throw new ServiceError('phone_number_exists');
} }
} }
} }

View File

@@ -9,7 +9,7 @@ import events from 'subscribers/events';
import { import {
TenantAlreadyInitialized, TenantAlreadyInitialized,
TenantAlreadySeeded, TenantAlreadySeeded,
TenantDatabaseNotBuilt TenantDatabaseNotBuilt,
} from 'exceptions'; } from 'exceptions';
import TenantsManager from 'services/Tenancy/TenantsManager'; import TenantsManager from 'services/Tenancy/TenantsManager';
@@ -35,10 +35,10 @@ export default class OrganizationService {
/** /**
* Builds the database schema and seed data of the given organization id. * Builds the database schema and seed data of the given organization id.
* @param {srting} organizationId * @param {srting} organizationId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
public async build(organizationId: string): Promise<void> { public async build(organizationId: string, user: ISystemUser): Promise<void> {
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId); const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
this.throwIfTenantInitizalized(tenant); this.throwIfTenantInitizalized(tenant);
@@ -46,15 +46,18 @@ export default class OrganizationService {
try { try {
if (!tenantHasDB) { if (!tenantHasDB) {
this.logger.info('[organization] trying to create tenant database.', { organizationId }); this.logger.info('[organization] trying to create tenant database.', {
organizationId, userId: user.id,
});
await this.tenantsManager.createDatabase(tenant); await this.tenantsManager.createDatabase(tenant);
} }
this.logger.info('[organization] trying to migrate tenant database.', { organizationId }); this.logger.info('[organization] trying to migrate tenant database.', {
organizationId, userId: user.id,
});
await this.tenantsManager.migrateTenant(tenant); await this.tenantsManager.migrateTenant(tenant);
// Throws `onOrganizationBuild` event. // Throws `onOrganizationBuild` event.
this.eventDispatcher.dispatch(events.organization.build, { tenant }); this.eventDispatcher.dispatch(events.organization.build, { tenant, user });
} catch (error) { } catch (error) {
if (error instanceof TenantAlreadyInitialized) { if (error instanceof TenantAlreadyInitialized) {
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED); throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
@@ -66,7 +69,7 @@ export default class OrganizationService {
/** /**
* Seeds initial core data to the given organization tenant. * Seeds initial core data to the given organization tenant.
* @param {number} organizationId * @param {number} organizationId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
public async seed(organizationId: string): Promise<void> { public async seed(organizationId: string): Promise<void> {
@@ -74,12 +77,13 @@ export default class OrganizationService {
this.throwIfTenantSeeded(tenant); this.throwIfTenantSeeded(tenant);
try { try {
this.logger.info('[organization] trying to seed tenant database.', { organizationId }); this.logger.info('[organization] trying to seed tenant database.', {
organizationId,
});
await this.tenantsManager.seedTenant(tenant); await this.tenantsManager.seedTenant(tenant);
// Throws `onOrganizationBuild` event. // Throws `onOrganizationBuild` event.
this.eventDispatcher.dispatch(events.organization.seeded, { tenant }); this.eventDispatcher.dispatch(events.organization.seeded, { tenant });
} catch (error) { } catch (error) {
if (error instanceof TenantAlreadySeeded) { if (error instanceof TenantAlreadySeeded) {
throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED); throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED);
@@ -93,11 +97,13 @@ export default class OrganizationService {
/** /**
* Listing all associated organizations to the given user. * Listing all associated organizations to the given user.
* @param {ISystemUser} user - * @param {ISystemUser} user -
* @return {Promise<void>} * @return {Promise<void>}
*/ */
public async listOrganizations(user: ISystemUser): Promise<ITenant[]> { public async listOrganizations(user: ISystemUser): Promise<ITenant[]> {
this.logger.info('[organization] trying to list all organizations.', { user }); this.logger.info('[organization] trying to list all organizations.', {
user,
});
const { tenantRepository } = this.sysRepositories; const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getById(user.tenantId); const tenant = await tenantRepository.getById(user.tenantId);
@@ -107,7 +113,7 @@ export default class OrganizationService {
/** /**
* Throws error in case the given tenant is undefined. * Throws error in case the given tenant is undefined.
* @param {ITenant} tenant * @param {ITenant} tenant
*/ */
private throwIfTenantNotExists(tenant: ITenant) { private throwIfTenantNotExists(tenant: ITenant) {
if (!tenant) { if (!tenant) {
@@ -118,7 +124,7 @@ export default class OrganizationService {
/** /**
* Throws error in case the given tenant is already initialized. * Throws error in case the given tenant is already initialized.
* @param {ITenant} tenant * @param {ITenant} tenant
*/ */
private throwIfTenantInitizalized(tenant: ITenant) { private throwIfTenantInitizalized(tenant: ITenant) {
if (tenant.initializedAt) { if (tenant.initializedAt) {
@@ -128,7 +134,7 @@ export default class OrganizationService {
/** /**
* Throws service if the tenant already seeded. * Throws service if the tenant already seeded.
* @param {ITenant} tenant * @param {ITenant} tenant
*/ */
private throwIfTenantSeeded(tenant: ITenant) { private throwIfTenantSeeded(tenant: ITenant) {
if (tenant.seededAt) { if (tenant.seededAt) {
@@ -137,9 +143,9 @@ export default class OrganizationService {
} }
/** /**
* Retrieve tenant model by the given organization id or throw not found * Retrieve tenant model by the given organization id or throw not found
* error if the tenant not exists on the storage. * error if the tenant not exists on the storage.
* @param {string} organizationId * @param {string} organizationId
* @return {ITenant} * @return {ITenant}
*/ */
private async getTenantByOrgIdOrThrowError(organizationId: string) { private async getTenantByOrgIdOrThrowError(organizationId: string) {
@@ -149,4 +155,4 @@ export default class OrganizationService {
return tenant; return tenant;
} }
} }

View File

@@ -1,8 +1,6 @@
import fs from 'fs';
import path from 'path';
import Mustache from 'mustache';
import { Container } from 'typedi'; import { Container } from 'typedi';
import Mail from 'lib/Mail';
import config from 'config';
export default class SubscriptionMailMessages { export default class SubscriptionMailMessages {
/** /**
* Send license code to the given mail address. * Send license code to the given mail address.
@@ -11,26 +9,18 @@ export default class SubscriptionMailMessages {
*/ */
public async sendMailLicense(licenseCode: string, email: string) { public async sendMailLicense(licenseCode: string, email: string) {
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const Mail = Container.get('mail');
const mail = new Mail()
const filePath = path.join(global.__root, 'views/mail/LicenseReceive.html'); .setView('mail/LicenseReceive.html')
const template = fs.readFileSync(filePath, 'utf8'); .setSubject('Bigcapital - License code')
const rendered = Mustache.render(template, { licenseCode }); .setTo(email)
.setData({
const mailOptions = { licenseCode,
to: email, successEmail: config.customerSuccess.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, successPhoneNumber: config.customerSuccess.phoneNumber,
subject: 'Bigcapital License',
html: rendered,
};
return new Promise((resolve, reject) => {
Mail.sendMail(mailOptions, (error) => {
if (error) {
reject(error);
return;
}
resolve();
}); });
});
await mail.send();
Logger.info('[license_mail] sent successfully.');
} }
} }

View File

@@ -11,7 +11,7 @@ export default class SubscriptionSMSMessages {
* @param {string} licenseCode * @param {string} licenseCode
*/ */
public async sendLicenseSMSMessage(phoneNumber: string, licenseCode: string) { public async sendLicenseSMSMessage(phoneNumber: string, licenseCode: string) {
const message: string = `Your license card number: ${licenseCode}.`; const message: string = `Your license card number: ${licenseCode}. If you need any help please contact us. Bigcapital.`;
return this.smsClient.sendMessage(phoneNumber, message); return this.smsClient.sendMessage(phoneNumber, message);
} }
} }

View File

@@ -12,7 +12,8 @@ export default class EasySMSClient implements SMSClientInterface {
*/ */
send(to: string, message: string) { send(to: string, message: string) {
const API_KEY = config.easySMSGateway.api_key; const API_KEY = config.easySMSGateway.api_key;
const params = `action=send-sms&api_key=${API_KEY}=&to=${to}&sms=${message}&unicode=1`; const parsedTo = to.indexOf('218') === 0 ? to.replace('218', '') : to;
const params = `action=send-sms&api_key=${API_KEY}=&to=${parsedTo}&sms=${message}&unicode=1`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get(`https://easysms.devs.ly/sms/api?${params}`) axios.get(`https://easysms.devs.ly/sms/api?${params}`)

View File

@@ -8,7 +8,7 @@ export default class SMSAPI {
} }
/** /**
* * Sends the message to the target via the client.
* @param {string} to * @param {string} to
* @param {string} message * @param {string} message
* @param {array} extraParams * @param {array} extraParams

View File

@@ -5,9 +5,11 @@ import events from 'subscribers/events';
@EventSubscriber() @EventSubscriber()
export class AuthenticationSubscriber { export class AuthenticationSubscriber {
/**
* Resets the login throttle once the login success.
*/
@On(events.auth.login) @On(events.auth.login)
public async onLogin(payload) { public async resetLoginThrottleOnceSuccessLogin(payload) {
const { emailOrPhone, password, user } = payload; const { emailOrPhone, password, user } = payload;
const loginThrottler = Container.get('rateLimiter.login'); const loginThrottler = Container.get('rateLimiter.login');
@@ -15,26 +17,28 @@ export class AuthenticationSubscriber {
// Reset the login throttle by the given email and phone number. // Reset the login throttle by the given email and phone number.
await loginThrottler.reset(user.email); await loginThrottler.reset(user.email);
await loginThrottler.reset(user.phoneNumber); await loginThrottler.reset(user.phoneNumber);
await loginThrottler.reset(emailOrPhone);
} }
/**
* Sends welcome email once the user register.
*/
@On(events.auth.register) @On(events.auth.register)
public onRegister(payload) { public async sendWelcomeEmail(payload) {
const { registerDTO, user } = payload; const { registerDTO, tenant, user } = payload;
const agenda = Container.get('agenda'); const agenda = Container.get('agenda');
// Send welcome mail to the user. // Send welcome mail to the user.
agenda.now('welcome-email', { await agenda.now('welcome-email', {
...pick(registerDTO, ['organizationName']), organizationId: tenant.organizationId,
user, user,
}); });
} }
@On(events.auth.resetPassword) /**
public onResetPassword(payload) { * Sends reset password mail once the reset password success.
*/
}
@On(events.auth.sendResetPassword) @On(events.auth.sendResetPassword)
public onSendResetPassword (payload) { public onSendResetPassword (payload) {
const { user, token } = payload; const { user, token } = payload;

View File

@@ -24,6 +24,5 @@ export class InviteUserSubscriber {
const { invite } = payload; const { invite } = payload;
const agenda = Container.get('agenda'); const agenda = Container.get('agenda');
} }
} }

View File

@@ -4,12 +4,13 @@ import events from 'subscribers/events';
@EventSubscriber() @EventSubscriber()
export class OrganizationSubscriber { export class OrganizationSubscriber {
/**
* Sends welcome SMS once the organization build completed.
*/
@On(events.organization.build) @On(events.organization.build)
public async onBuild(payload) { public async onBuild({ tenant, user }) {
const { tenant, user } = payload;
const agenda = Container.get('agenda'); const agenda = Container.get('agenda');
await agenda.now('welcome-sms', { tenant, user }); await agenda.now('welcome-sms', { tenant, user });
} }
} }

View File

@@ -14,6 +14,7 @@ export default class SubscriptionRepository extends SystemRepository{
*/ */
getBySlugInTenant(slug: string, tenantId: number) { getBySlugInTenant(slug: string, tenantId: number) {
const key = `subscription.slug.${slug}.tenant.${tenantId}`; const key = `subscription.slug.${slug}.tenant.${tenantId}`;
return this.cache.get(key, () => { return this.cache.get(key, () => {
return PlanSubscription.query().findOne('slug', slug).where('tenant_id', tenantId); return PlanSubscription.query().findOne('slug', slug).where('tenant_id', tenantId);
}); });

View File

@@ -245,6 +245,7 @@ function defaultToTransform(
: _transfromedValue; : _transfromedValue;
} }
export { export {
hashPassword, hashPassword,
origin, origin,
@@ -265,5 +266,5 @@ export {
convertEmptyStringToNull, convertEmptyStringToNull,
formatNumber, formatNumber,
isBlank, isBlank,
defaultToTransform defaultToTransform,
}; };

View File

@@ -374,12 +374,12 @@
<p class="align-center"> <p class="align-center">
<h3>License Code</h3> <h3>License Code</h3>
</p> </p>
<p class="mgb-1x">License {{ licenseCode }},</p> <p class="mgb-1x">
<p class="mgb-2-5x">Click On The link blow to reset your password.</p> <h1>{{ licenseCode }}</h1>
</p>
<p class="email-note"> <p class="email-note">
This is an automatically generated email please do not reply to This is an automatically generated email please do not reply to
this email. If you face any issues, please contact us at {{ contact_us_email }}</p> this email. If you face any issues, please contact us at <a href="mailto:{{ successEmail }}">{{ successEmail }}</a> or call <u>{{ successPhoneNumber }}</u></p>
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -371,10 +371,15 @@
<hr /> <hr />
<p class="align-center"> <p class="align-center">
<h3>Hi {{ firstName }}, Welcome to Bigcapital</h3> <h3>Hi {{ firstName }}, Welcome to Bigcapital, </h3>
</p> </p>
<p class="mgb-1x">Youve joined the new Bigcapital workspace {{ organizationName }}.</p>
<p class="mgb-2-5x">If you need any help to get started please don't hesitate to contact us to help you via phone number or email.</p> <p class="mgb-1x">
Your organization Id: <strong>{{ organizationId }}</strong>
</p>
<p class="mgb-1x">We are available to help you get started and answer any questions you may have. You can also email <a href="mailto:{{ successEmail }}">{{ successEmail }}</a> or call <u>{{ successPhoneNumber }}</u> about your set-up questions.</p>
<p class="mgb-2-5x">Thank you for trusting Bigcapital Software for your business needs. We look forward to serving you!</p>
</td> </td>
</tr> </tr>
</table> </table>