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))
); );
@@ -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') {
@@ -143,7 +140,10 @@ export default class InviteUsersController extends BaseController {
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({

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(), ),
]) ];
]
} }
/** /**
@@ -146,7 +144,11 @@ export default class LicensesController extends BaseController {
* @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);
@@ -165,16 +167,21 @@ export default class LicensesController extends BaseController {
* @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);
@@ -216,7 +223,13 @@ export default class LicensesController extends BaseController {
* @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')
@@ -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,
@@ -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);
new SendLicenseViaEmailJob(agenda);
new SendLicenseViaPhoneJob(agenda);
// User invite mail.
agenda.define(
'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
): Promise<void> {
const filePath = path.join(global.__root, 'views/mail/Welcome.html'); const mail = new Mail()
const template = fs.readFileSync(filePath, 'utf8'); .setView('mail/Welcome.html')
const rendered = Mustache.render(template, { .setSubject('Welcome to Bigcapital')
email: user.email, .setTo(user.email)
firstName: user.firstName, .setData({
organizationName, firstName: user.firstName,
}); organizationId,
const mailOptions = { successPhoneNumber: config.customerSuccess.phoneNumber,
to: user.email, successEmail: config.customerSuccess.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Welcome to Bigcapital',
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 {
@@ -12,7 +12,7 @@ export default class AuthenticationSMSMessages {
* @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,
@@ -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[] = [];
@@ -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;
} }
@@ -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.
@@ -243,7 +269,11 @@ 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.');
} }
@@ -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
*/
async sendInviteMail(user: ISystemUser, invite) {
const mail = new Mail()
.setSubject(`${user.fullName} has invited you to join a Bigcapital`)
.setView('mail/UserInvite.html')
.setData({
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
fullName: `${user.firstName} ${user.lastName}`,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
organizationName: organizationOptions.getMeta('organization_name'),
});
const rendered = Mustache.render(template, { await mail.send();
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`, Logger.log('info', 'User has been sent invite user email successfuly.');
fullName: `${user.firstName} ${user.lastName}`,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
organizationName: organizationOptions.getMeta('organization_name'),
});
const mailOptions = {
to: user.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: `${user.fullName} has invited you to join a Bigcapital`,
html: rendered,
};
mail.sendMail(mailOptions, (error) => {
if (error) {
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 {
@@ -41,7 +38,10 @@ export default class InviteUserService {
* @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,
}); });
} }
@@ -80,7 +86,14 @@ export default class InviteUserService {
* *
* @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,
@@ -110,20 +125,24 @@ export default class InviteUserService {
* @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 };
} }
@@ -132,7 +151,9 @@ export default class InviteUserService {
* 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);
@@ -162,9 +183,13 @@ 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';
@@ -38,7 +38,7 @@ export default class OrganizationService {
* @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);
@@ -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);
@@ -97,7 +101,9 @@ export default class OrganizationService {
* @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);

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 filePath = path.join(global.__root, 'views/mail/LicenseReceive.html'); const mail = new Mail()
const template = fs.readFileSync(filePath, 'utf8'); .setView('mail/LicenseReceive.html')
const rendered = Mustache.render(template, { licenseCode }); .setSubject('Bigcapital - License code')
.setTo(email)
const mailOptions = { .setData({
to: email, licenseCode,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, successEmail: config.customerSuccess.email,
subject: 'Bigcapital License', successPhoneNumber: config.customerSuccess.phoneNumber,
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,10 +4,11 @@ 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>