From 46d06bd591919fc096d8183f5550b83e6e39b7fe Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Thu, 17 Dec 2020 01:16:08 +0200 Subject: [PATCH] fix: issue in mail sender. --- server/.env.example | 3 - server/src/api/controllers/Authentication.ts | 4 +- server/src/api/controllers/InviteUsers.ts | 55 ++++---- server/src/api/controllers/Items.ts | 6 +- server/src/api/controllers/Organization.ts | 3 +- .../api/controllers/Subscription/Licenses.ts | 129 ++++++++++-------- server/src/config/index.js | 8 +- server/src/interfaces/Mailable.ts | 16 +++ server/src/interfaces/index.ts | 4 +- server/src/jobs/SendLicenseEmail.ts | 18 ++- server/src/jobs/SendLicensePhone.ts | 11 ++ server/src/jobs/UserInviteMail.ts | 12 ++ server/src/jobs/welcomeEmail.ts | 12 +- server/src/lib/Mail/index.ts | 102 ++++++++++++++ server/src/loaders/jobs.ts | 22 +-- .../AuthenticationMailMessages.ts | 86 +++++------- .../AuthenticationSMSMessages.ts | 12 +- server/src/services/Authentication/index.ts | 88 ++++++++---- .../InviteUsers/InviteUsersMailMessages.ts | 46 +++---- server/src/services/InviteUsers/index.ts | 87 +++++++----- server/src/services/Organization/index.ts | 42 +++--- .../services/Payment/LicenseMailMessages.ts | 38 ++---- .../services/Payment/LicenseSMSMessages.ts | 2 +- .../src/services/SMSClient/EasySmsClient.ts | 3 +- server/src/services/SMSClient/SMSAPI.ts | 2 +- server/src/subscribers/authentication.ts | 26 ++-- server/src/subscribers/inviteUser.ts | 1 - server/src/subscribers/organization.ts | 11 +- .../repositories/SubscriptionRepository.ts | 1 + server/src/utils/index.js | 3 +- server/views/mail/LicenseReceive.html | 8 +- server/views/mail/Welcome.html | 11 +- 32 files changed, 538 insertions(+), 334 deletions(-) create mode 100644 server/src/interfaces/Mailable.ts create mode 100644 server/src/lib/Mail/index.ts diff --git a/server/.env.example b/server/.env.example index 437019d76..31287ed5d 100644 --- a/server/.env.example +++ b/server/.env.example @@ -4,9 +4,6 @@ MAIL_PASSWORD=172f97b34f1a17 MAIL_PORT=587 MAIL_SECURE=false -MAIL_FROM_ADDRESS= -MAIL_FROM_NAME= - SYSTEM_DB_CLIENT=mysql SYSTEM_DB_HOST=127.0.0.1 SYSTEM_DB_USER=root diff --git a/server/src/api/controllers/Authentication.ts b/server/src/api/controllers/Authentication.ts index a63880c5b..8fb5cd530 100644 --- a/server/src/api/controllers/Authentication.ts +++ b/server/src/api/controllers/Authentication.ts @@ -6,7 +6,7 @@ import parsePhoneNumber from 'libphonenumber-js'; import BaseController from 'api/controllers/BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import AuthenticationService from 'services/Authentication'; -import { ILoginDTO, ISystemUser, IRegisterOTD } from 'interfaces'; +import { ILoginDTO, ISystemUser, IRegisterDTO } from 'interfaces'; import { ServiceError, ServiceErrors } from "exceptions"; import { DATATYPES_LENGTH } from 'data/DataTypes'; import LoginThrottlerMiddleware from 'api/middleware/LoginThrottlerMiddleware'; @@ -206,7 +206,7 @@ export default class AuthenticationController extends BaseController{ * @param {Response} res */ async register(req: Request, res: Response, next: Function) { - const registerDTO: IRegisterOTD = this.matchedBodyData(req); + const registerDTO: IRegisterDTO = this.matchedBodyData(req); try { const registeredUser: ISystemUser = await this.authService.register(registerDTO); diff --git a/server/src/api/controllers/InviteUsers.ts b/server/src/api/controllers/InviteUsers.ts index 3f9d4ab10..55a83113c 100644 --- a/server/src/api/controllers/InviteUsers.ts +++ b/server/src/api/controllers/InviteUsers.ts @@ -1,10 +1,6 @@ import { Service, Inject } from 'typedi'; import { Router, Request, Response } from 'express'; -import { - check, - body, - param, -} from 'express-validator'; +import { check, body, param } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import InviteUserService from 'services/InviteUsers'; import { ServiceErrors, ServiceError } from 'exceptions'; @@ -21,11 +17,11 @@ export default class InviteUsersController extends BaseController { authRouter() { const router = Router(); - router.post('/send', [ - body('email').exists().trim().escape(), - ], + router.post( + '/send', + [body('email').exists().trim().escape()], this.validationResult, - asyncMiddleware(this.sendInvite.bind(this)), + asyncMiddleware(this.sendInvite.bind(this)) ); return router; } @@ -36,15 +32,15 @@ export default class InviteUsersController extends BaseController { nonAuthRouter() { const router = Router(); - router.post('/accept/:token', [ - ...this.inviteUserDTO, - ], + router.post( + '/accept/:token', + [...this.inviteUserDTO], this.validationResult, asyncMiddleware(this.accept.bind(this)) ); - router.get('/invited/:token', [ - param('token').exists().trim().escape(), - ], + router.get( + '/invited/:token', + [param('token').exists().trim().escape()], this.validationResult, asyncMiddleware(this.invited.bind(this)) ); @@ -67,9 +63,9 @@ export default class InviteUsersController extends BaseController { /** * Invite a user to the authorized user organization. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - */ async sendInvite(req: Request, res: Response, next: Function) { const { email } = req.body; @@ -78,11 +74,12 @@ export default class InviteUsersController extends BaseController { try { await this.inviteUsersService.sendInvite(tenantId, email, user); + return res.status(200).send({ type: 'success', code: 'INVITE.SENT.SUCCESSFULLY', message: 'The invite has been sent to the given email.', - }) + }); } catch (error) { if (error instanceof ServiceError) { if (error.errorType === 'email_already_invited') { @@ -98,9 +95,9 @@ export default class InviteUsersController extends BaseController { /** * Accept the inviation. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - */ async accept(req: Request, res: Response, next: Function) { const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, { @@ -135,15 +132,18 @@ export default class InviteUsersController extends BaseController { /** * Check if the invite token is valid. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - */ async invited(req: Request, res: Response, next: Function) { const { token } = req.params; try { - const { inviteToken, orgName } = await this.inviteUsersService.checkInvite(token); + const { + inviteToken, + orgName, + } = await this.inviteUsersService.checkInvite(token); return res.status(200).send({ inviteToken: inviteToken.token, @@ -151,7 +151,6 @@ export default class InviteUsersController extends BaseController { organizationName: orgName?.value, }); } catch (error) { - if (error instanceof ServiceError) { if (error.errorType === 'invite_token_invalid') { return res.status(400).send({ @@ -162,4 +161,4 @@ export default class InviteUsersController extends BaseController { next(error); } } -} \ No newline at end of file +} diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 5faa275ef..58f828584 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -218,7 +218,11 @@ export default class ItemsController extends BaseController { try { 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) { next(error); } diff --git a/server/src/api/controllers/Organization.ts b/server/src/api/controllers/Organization.ts index 5c298d0c1..9f93a4fc7 100644 --- a/server/src/api/controllers/Organization.ts +++ b/server/src/api/controllers/Organization.ts @@ -58,9 +58,10 @@ export default class OrganizationController extends BaseController{ */ async build(req: Request, res: Response, next: Function) { const { organizationId } = req.tenant; + const { user } = req; try { - await this.organizationService.build(organizationId); + await this.organizationService.build(organizationId, user); return res.status(200).send({ type: 'success', diff --git a/server/src/api/controllers/Subscription/Licenses.ts b/server/src/api/controllers/Subscription/Licenses.ts index 5639d9b7c..ace83a13a 100644 --- a/server/src/api/controllers/Subscription/Licenses.ts +++ b/server/src/api/controllers/Subscription/Licenses.ts @@ -1,5 +1,5 @@ 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 basicAuth from 'express-basic-auth'; import config from 'config'; @@ -20,42 +20,41 @@ export default class LicensesController extends BaseController { router() { const router = Router(); - router.use(basicAuth({ - users: { - [config.licensesAuth.user]: config.licensesAuth.password, - }, - challenge: true, - })); + router.use( + basicAuth({ + users: { + [config.licensesAuth.user]: config.licensesAuth.password, + }, + challenge: true, + }) + ); router.post( '/generate', this.generateLicenseSchema, this.validationResult, asyncMiddleware(this.validatePlanExistance.bind(this)), - asyncMiddleware(this.generateLicense.bind(this)), + asyncMiddleware(this.generateLicense.bind(this)) ); router.post( '/disable/:licenseId', this.validationResult, asyncMiddleware(this.validateLicenseExistance.bind(this)), asyncMiddleware(this.validateNotDisabledLicense.bind(this)), - asyncMiddleware(this.disableLicense.bind(this)), + asyncMiddleware(this.disableLicense.bind(this)) ); router.post( '/send', this.sendLicenseSchemaValidation, this.validationResult, - asyncMiddleware(this.sendLicense.bind(this)), + asyncMiddleware(this.sendLicense.bind(this)) ); router.delete( '/:licenseId', asyncMiddleware(this.validateLicenseExistance.bind(this)), - asyncMiddleware(this.deleteLicense.bind(this)), - ); - router.get( - '/', - asyncMiddleware(this.listLicenses.bind(this)), + asyncMiddleware(this.deleteLicense.bind(this)) ); + router.get('/', asyncMiddleware(this.listLicenses.bind(this))); return router; } @@ -66,9 +65,9 @@ export default class LicensesController extends BaseController { return [ check('loop').exists().isNumeric().toInt(), check('period').exists().isNumeric().toInt(), - check('period_interval').exists().isIn([ - 'month', 'months', 'year', 'years', 'day', 'days' - ]), + check('period_interval') + .exists() + .isIn(['month', 'months', 'year', 'years', 'day', 'days']), check('plan_id').exists().isNumeric().toInt(), ]; } @@ -78,12 +77,11 @@ export default class LicensesController extends BaseController { */ get specificLicenseSchema(): ValidationChain[] { return [ - oneOf([ - check('license_id').exists().isNumeric().toInt(), - ], [ - check('license_code').exists().isNumeric().toInt(), - ]) - ] + oneOf( + [check('license_id').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. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {Request} req + * @param {Response} res + * @param {Function} next */ async validatePlanExistance(req: Request, res: Response, next: Function) { const body = this.matchedBodyData(req); @@ -122,8 +120,8 @@ export default class LicensesController extends BaseController { /** * Valdiate the license existance on the storage. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res * @param {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. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {Request} req + * @param {Response} res + * @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 foundLicense = await License.query().findById(licenseId); @@ -160,31 +162,36 @@ export default class LicensesController extends BaseController { /** * Generate licenses codes with given period in bulk. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res * @return {Response} */ 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 { await this.licenseService.generateLicenses( - loop, period, periodInterval, planId, + loop, + period, + periodInterval, + planId ); return res.status(200).send({ code: 100, type: 'LICENSEES.GENERATED.SUCCESSFULLY', - message: 'The licenses have been generated successfully.' + message: 'The licenses have been generated successfully.', }); } catch (error) { next(error); - } + } } /** * Disable the given license on the storage. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res * @return {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. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res * @return {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 - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res * @return {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() .modify('filterActiveLicense') .where('license_period', period) @@ -228,12 +241,15 @@ export default class LicensesController extends BaseController { if (!license) { return res.status(400).send({ 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', }); } await this.licenseService.sendLicenseToCustomer( - license.licenseCode, phoneNumber, email, + license.licenseCode, + phoneNumber, + email ); return res.status(200).send({ status: 100, @@ -244,8 +260,8 @@ export default class LicensesController extends BaseController { /** * Listing licenses. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ async listLicenses(req: Request, res: Response) { const filter: ILicensesFilter = { @@ -255,11 +271,10 @@ export default class LicensesController extends BaseController { active: false, ...req.query, }; - const licenses = await License.query() - .onBuild((builder) => { - builder.modify('filter', filter); - builder.orderBy('createdAt', 'ASC'); - }); + const licenses = await License.query().onBuild((builder) => { + builder.modify('filter', filter); + builder.orderBy('createdAt', 'ASC'); + }); return res.status(200).send({ licenses }); } -} \ No newline at end of file +} diff --git a/server/src/config/index.js b/server/src/config/index.js index 23f014317..49f25ed0e 100644 --- a/server/src/config/index.js +++ b/server/src/config/index.js @@ -57,7 +57,7 @@ export default { mail: { host: process.env.MAIL_HOST, port: process.env.MAIL_PORT, - secure: process.env.MAIL_SECURE, + secure: !!parseInt(process.env.MAIL_SECURE, 10), username: process.env.MAIL_USERNAME, 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, /** diff --git a/server/src/interfaces/Mailable.ts b/server/src/interfaces/Mailable.ts new file mode 100644 index 000000000..36cc3c81f --- /dev/null +++ b/server/src/interfaces/Mailable.ts @@ -0,0 +1,16 @@ + +export interface IMailable { + constructor( + view: string, + data?: { [key: string]: string | number }, + ); + send(): Promise; + 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; +} \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 8c4c9f2e7..48aecded5 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -35,5 +35,5 @@ export * from './TrialBalanceSheet'; export * from './GeneralLedgerSheet' export * from './ProfitLossSheet'; export * from './JournalReport'; - -export * from './ARAgingSummaryReport'; \ No newline at end of file +export * from './ARAgingSummaryReport'; +export * from './Mailable'; \ No newline at end of file diff --git a/server/src/jobs/SendLicenseEmail.ts b/server/src/jobs/SendLicenseEmail.ts index 6de2e2366..91569a91d 100644 --- a/server/src/jobs/SendLicenseEmail.ts +++ b/server/src/jobs/SendLicenseEmail.ts @@ -2,19 +2,31 @@ import { Container } from 'typedi'; import LicenseService from 'services/Payment/License'; 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 { const Logger = Container.get('logger'); const licenseService = Container.get(LicenseService); 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 { 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(); } 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); } } diff --git a/server/src/jobs/SendLicensePhone.ts b/server/src/jobs/SendLicensePhone.ts index 797a427fa..6301c7058 100644 --- a/server/src/jobs/SendLicensePhone.ts +++ b/server/src/jobs/SendLicensePhone.ts @@ -2,6 +2,17 @@ import { Container } from 'typedi'; import LicenseService from 'services/Payment/License'; 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 { const { phoneNumber, licenseCode } = job.attrs.data; diff --git a/server/src/jobs/UserInviteMail.ts b/server/src/jobs/UserInviteMail.ts index 7b261c407..ce1eea239 100644 --- a/server/src/jobs/UserInviteMail.ts +++ b/server/src/jobs/UserInviteMail.ts @@ -5,6 +5,18 @@ export default class UserInviteMailJob { @Inject() inviteUsersService: InviteUserService; + /** + * Constructor method. + * @param {Agenda} agenda + */ + constructor(agenda) { + agenda.define( + 'user-invite-mail', + { priority: 'high' }, + this.handler.bind(this), + ); + } + /** * Handle invite user job. * @param {Job} job diff --git a/server/src/jobs/welcomeEmail.ts b/server/src/jobs/welcomeEmail.ts index 8b37baaf2..1bc038b2f 100644 --- a/server/src/jobs/welcomeEmail.ts +++ b/server/src/jobs/welcomeEmail.ts @@ -1,4 +1,4 @@ -import { Container, Inject } from 'typedi'; +import { Container } from 'typedi'; import AuthenticationService from 'services/Authentication'; export default class WelcomeEmailJob { @@ -21,18 +21,18 @@ export default class WelcomeEmailJob { * @param {Function} done */ public async handler(job, done: Function): Promise { - const { organizationName, user } = job.attrs.data; + const { organizationId, user } = job.attrs.data; const Logger: any = Container.get('logger'); 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 { - await authService.mailMessages.sendWelcomeMessage(user, organizationName); - Logger.info(`[welcome_mail] send welcome mail message - finished: ${job.attrs.data}`); + await authService.mailMessages.sendWelcomeMessage(user, organizationId); + Logger.info(`[welcome_mail] finished: ${job.attrs.data}`); done(); } 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); } } diff --git a/server/src/lib/Mail/index.ts b/server/src/lib/Mail/index.ts new file mode 100644 index 000000000..424b5f298 --- /dev/null +++ b/server/src/lib/Mail/index.ts @@ -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'); + } +} \ No newline at end of file diff --git a/server/src/loaders/jobs.ts b/server/src/loaders/jobs.ts index 324cfb7cc..5596c4eb6 100644 --- a/server/src/loaders/jobs.ts +++ b/server/src/loaders/jobs.ts @@ -16,13 +16,10 @@ export default ({ agenda }: { agenda: Agenda }) => { new WelcomeEmailJob(agenda); new ResetPasswordMailJob(agenda); new WelcomeSMSJob(agenda); - - // User invite mail. - agenda.define( - 'user-invite-mail', - { priority: 'high' }, - new UserInviteMailJob().handler, - ) + new UserInviteMailJob(agenda); + new SendLicenseViaEmailJob(agenda); + new SendLicenseViaPhoneJob(agenda); + agenda.define( 'compute-item-cost', { priority: 'high', concurrency: 20 }, @@ -33,16 +30,7 @@ export default ({ agenda }: { agenda: Agenda }) => { { priority: 'normal', concurrency: 1, }, 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( 'send-sms-notification-subscribe-end', { priority: 'nromal', concurrency: 1, }, diff --git a/server/src/services/Authentication/AuthenticationMailMessages.ts b/server/src/services/Authentication/AuthenticationMailMessages.ts index 39890c39d..a99e67a7c 100644 --- a/server/src/services/Authentication/AuthenticationMailMessages.ts +++ b/server/src/services/Authentication/AuthenticationMailMessages.ts @@ -1,9 +1,8 @@ -import fs from 'fs'; -import { Service, Container } from "typedi"; -import Mustache from 'mustache'; -import path from 'path'; + +import { Service } from "typedi"; import { ISystemUser } from 'interfaces'; import config from 'config'; +import Mail from "lib/Mail"; @Service() export default class AuthenticationMailMesssages { @@ -13,31 +12,23 @@ export default class AuthenticationMailMesssages { * @param {string} organizationName - * @return {Promise} */ - sendWelcomeMessage(user: ISystemUser, organizationName: string): Promise { - const Mail = Container.get('mail'); - - const filePath = path.join(global.__root, 'views/mail/Welcome.html'); - const template = fs.readFileSync(filePath, 'utf8'); - const rendered = Mustache.render(template, { - email: user.email, - firstName: user.firstName, - organizationName, - }); - const mailOptions = { - to: user.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(); + async sendWelcomeMessage( + user: ISystemUser, + organizationId: string + ): Promise { + + const mail = new Mail() + .setView('mail/Welcome.html') + .setSubject('Welcome to Bigcapital') + .setTo(user.email) + .setData({ + firstName: user.firstName, + organizationId, + successPhoneNumber: config.customerSuccess.phoneNumber, + successEmail: config.customerSuccess.email, }); - }); + + await mail.send(); } /** @@ -46,31 +37,22 @@ export default class AuthenticationMailMesssages { * @param {string} token - Reset password token. * @return {Promise} */ - sendResetPasswordMessage(user: ISystemUser, token: string): Promise { - const Mail = Container.get('mail'); + async sendResetPasswordMessage( + user: ISystemUser, + token: string + ): Promise { - const filePath = path.join(global.__root, 'views/mail/ResetPassword.html'); - const template = fs.readFileSync(filePath, 'utf8'); - const rendered = Mustache.render(template, { - resetPasswordUrl: `${config.baseURL}/reset/${token}`, - first_name: user.firstName, - last_name: user.lastName, - contact_us_email: config.contactUsMail, - }); - const mailOptions = { - 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(); + const mail = new Mail() + .setSubject('Bigcapital - Password Reset') + .setView('mail/ResetPassword.html') + .setTo(user.email) + .setData({ + resetPasswordUrl: `${config.baseURL}/reset/${token}`, + first_name: user.firstName, + last_name: user.lastName, + contact_us_email: config.contactUsMail, }); - }); + + await mail.send(); } } \ No newline at end of file diff --git a/server/src/services/Authentication/AuthenticationSMSMessages.ts b/server/src/services/Authentication/AuthenticationSMSMessages.ts index c9a062ffb..81d3ba7e6 100644 --- a/server/src/services/Authentication/AuthenticationSMSMessages.ts +++ b/server/src/services/Authentication/AuthenticationSMSMessages.ts @@ -1,5 +1,5 @@ -import { Service, Inject } from "typedi"; -import { ISystemUser, ITenant } from "interfaces"; +import { Service, Inject } from 'typedi'; +import { ISystemUser, ITenant } from 'interfaces'; @Service() export default class AuthenticationSMSMessages { @@ -8,12 +8,12 @@ export default class AuthenticationSMSMessages { /** * Sends welcome sms message. - * @param {ITenant} tenant - * @param {ISystemUser} user + * @param {ITenant} tenant + * @param {ISystemUser} user */ 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); } -} \ No newline at end of file +} diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index d3441dc42..342c6aa31 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -1,8 +1,8 @@ -import { Service, Inject, Container } from "typedi"; +import { Service, Inject, Container } from 'typedi'; import JWT from 'jsonwebtoken'; import uniqid from 'uniqid'; import { omit } from 'lodash'; -import moment from "moment"; +import moment from 'moment'; import { EventDispatcher, EventDispatcherInterface, @@ -46,7 +46,7 @@ export default class AuthenticationService implements IAuthenticationService { /** * Signin and generates JWT token. * @throws {ServiceError} - * @param {string} emailOrPhone - Email or phone number. + * @param {string} emailOrPhone - Email or phone number. * @param {string} password - Password. * @return {Promise<{user: IUser, token: string}>} */ @@ -54,11 +54,14 @@ export default class AuthenticationService implements IAuthenticationService { emailOrPhone: string, password: string ): Promise<{ - user: ISystemUser, - token: string, - tenant: ITenant + user: ISystemUser; + token: string; + 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 loginThrottler = Container.get('rateLimiter.login'); @@ -72,7 +75,10 @@ export default class AuthenticationService implements IAuthenticationService { 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)) { await loginThrottler.hit(emailOrPhone); @@ -87,14 +93,18 @@ export default class AuthenticationService implements IAuthenticationService { this.logger.info('[login] generating JWT token.', { userId: user.id }); 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); this.logger.info('[login] Logging success.', { user, token }); // Triggers `onLogin` event. this.eventDispatcher.dispatch(events.auth.login, { - emailOrPhone, password, user, + emailOrPhone, + password, + user, }); const tenant = await user.$relatedQuery('tenant'); @@ -111,8 +121,12 @@ export default class AuthenticationService implements IAuthenticationService { */ private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) { const { systemUserRepository } = this.sysRepositories; - const isEmailExists = await systemUserRepository.getByEmail(registerDTO.email); - const isPhoneExists = await systemUserRepository.getByPhoneNumber(registerDTO.phoneNumber); + const isEmailExists = await systemUserRepository.getByEmail( + registerDTO.email + ); + const isPhoneExists = await systemUserRepository.getByPhoneNumber( + registerDTO.phoneNumber + ); const errorReasons: ServiceError[] = []; @@ -132,7 +146,7 @@ export default class AuthenticationService implements IAuthenticationService { /** * Registers a new tenant with user from user input. * @throws {ServiceErrors} - * @param {IUserDTO} user + * @param {IUserDTO} user */ public async register(registerDTO: IRegisterDTO): Promise { 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.'); 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 { systemUserRepository } = this.sysRepositories; @@ -153,7 +167,9 @@ export default class AuthenticationService implements IAuthenticationService { }); // Triggers `onRegister` event. this.eventDispatcher.dispatch(events.auth.register, { - registerDTO, user: registeredUser + registerDTO, + tenant, + user: registeredUser, }); return registeredUser; } @@ -170,14 +186,14 @@ export default class AuthenticationService implements IAuthenticationService { /** * Validate the given email existance on the storage. * @throws {ServiceError} - * @param {string} email - email address. + * @param {string} email - email address. */ private async validateEmailExistance(email: string): Promise { const { systemUserRepository } = this.sysRepositories; const userByEmail = await systemUserRepository.getByEmail(email); 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'); } return userByEmail; @@ -185,7 +201,7 @@ export default class AuthenticationService implements IAuthenticationService { /** * Generates and retrieve password reset token for the given user email. - * @param {string} email + * @param {string} email * @return {} */ public async sendResetPassword(email: string): Promise { @@ -193,7 +209,9 @@ export default class AuthenticationService implements IAuthenticationService { const user = await this.validateEmailExistance(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); const token: string = uniqid(); @@ -202,8 +220,10 @@ export default class AuthenticationService implements IAuthenticationService { const passwordReset = await PasswordReset.query().insert({ email, token }); // Triggers `onSendResetPassword` event. - this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token }); - + this.eventDispatcher.dispatch(events.auth.sendResetPassword, { + user, + token, + }); return passwordReset; } @@ -215,14 +235,20 @@ export default class AuthenticationService implements IAuthenticationService { */ public async resetPassword(token: string, password: string): Promise { const { systemUserRepository } = this.sysRepositories; - const tokenModel: IPasswordReset = await PasswordReset.query().findOne('token', token); + const tokenModel: IPasswordReset = await PasswordReset.query().findOne( + 'token', + token + ); if (!tokenModel) { this.logger.info('[reset_password] token invalid.'); throw new ServiceError('token_invalid'); } // 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.'); // Deletes the expired token by expired token email. @@ -235,7 +261,7 @@ export default class AuthenticationService implements IAuthenticationService { throw new ServiceError('user_not_found'); } const hashedPassword = await hashPassword(password); - + this.logger.info('[reset_password] saving a new hashed password.'); await systemUserRepository.edit(user.id, { password: hashedPassword }); @@ -243,13 +269,17 @@ export default class AuthenticationService implements IAuthenticationService { await this.deletePasswordResetToken(tokenModel.email); // 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.'); } /** * Deletes the password reset token by the given email. - * @param {string} email + * @param {string} email * @returns {Promise} */ private async deletePasswordResetToken(email: string) { @@ -259,7 +289,7 @@ export default class AuthenticationService implements IAuthenticationService { /** * Generates JWT token for the given user. - * @param {ISystemUser} user + * @param {ISystemUser} user * @return {string} token */ 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' exp: exp.getTime() / 1000, }, - config.jwtSecret, + config.jwtSecret ); } -} \ No newline at end of file +} diff --git a/server/src/services/InviteUsers/InviteUsersMailMessages.ts b/server/src/services/InviteUsers/InviteUsersMailMessages.ts index 9730c813e..5ec6853d6 100644 --- a/server/src/services/InviteUsers/InviteUsersMailMessages.ts +++ b/server/src/services/InviteUsers/InviteUsersMailMessages.ts @@ -1,31 +1,29 @@ +import { IInviteUserInput, ISystemUser } from "interfaces"; +import Mail from "lib/Mail"; import { Service } from "typedi"; @Service() export default class InviteUsersMailMessages { - sendInviteMail() { - const filePath = path.join(global.__root, 'views/mail/UserInvite.html'); - const template = fs.readFileSync(filePath, 'utf8'); - - const rendered = Mustache.render(template, { - 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 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 }); - }); + /** + * Sends invite mail to the given email. + * @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'), + }); + + await mail.send(); + Logger.log('info', 'User has been sent invite user email successfuly.'); } } \ No newline at end of file diff --git a/server/src/services/InviteUsers/index.ts b/server/src/services/InviteUsers/index.ts index be2ea602b..2509037dd 100644 --- a/server/src/services/InviteUsers/index.ts +++ b/server/src/services/InviteUsers/index.ts @@ -1,21 +1,18 @@ -import { Service, Inject } from "typedi"; +import { Service, Inject } from 'typedi'; import uniqid from 'uniqid'; import moment from 'moment'; import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; -import { ServiceError } from "exceptions"; -import { Invite, Tenant } from "system/models"; +import { ServiceError } from 'exceptions'; +import { Invite, Tenant } from 'system/models'; import { Option } from 'models'; import { hashPassword } from 'utils'; import TenancyService from 'services/Tenancy/TenancyService'; -import InviteUsersMailMessages from "services/InviteUsers/InviteUsersMailMessages"; +import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages'; import events from 'subscribers/events'; -import { - ISystemUser, - IInviteUserInput, -} from 'interfaces'; +import { ISystemUser, IInviteUserInput } from 'interfaces'; @Service() export default class InviteUserService { @@ -36,12 +33,15 @@ export default class InviteUserService { /** * Accept the received invite. - * @param {string} token - * @param {IInviteUserInput} inviteUserInput + * @param {string} token + * @param {IInviteUserInput} inviteUserInput * @throws {ServiceErrors} * @returns {Promise} */ - async acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise { + async acceptInvite( + token: string, + inviteUserInput: IInviteUserInput + ): Promise { const inviteToken = await this.getInviteOrThrowError(token); await this.validateUserPhoneNumber(inviteUserInput); @@ -61,14 +61,20 @@ export default class InviteUserService { }); 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. - const [updatedUser] = await Promise.all([updateUserOper, deleteInviteTokenOper]); + const [updatedUser] = await Promise.all([ + updateUserOper, + deleteInviteTokenOper, + ]); // Triggers `onUserAcceptInvite` event. this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, { - inviteToken, user: updatedUser, + inviteToken, + user: updatedUser, }); } @@ -77,10 +83,17 @@ export default class InviteUserService { * @param {number} tenantId - * @param {string} email - * @param {IUser} authorizedUser - - * + * * @return {Promise} */ - 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); this.logger.info('[send_invite] trying to store invite token.'); @@ -90,7 +103,9 @@ export default class InviteUserService { 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 user = await systemUserRepository.create({ email, @@ -100,39 +115,45 @@ export default class InviteUserService { // Triggers `onUserSendInvite` event. this.eventDispatcher.dispatch(events.inviteUser.sendInvite, { - invite, + invite, }); return { invite, user }; } /** * Validate the given invite token. - * @param {string} token - the given token string. + * @param {string} token - the given token string. * @throws {ServiceError} */ - public async checkInvite(token: string): Promise<{ inviteToken: string, orgName: object}> { - const inviteToken = await this.getInviteOrThrowError(token) + public async checkInvite( + token: string + ): Promise<{ inviteToken: string; orgName: object }> { + const inviteToken = await this.getInviteOrThrowError(token); // Find the tenant that associated to the given token. const tenant = await Tenant.query().findOne('id', inviteToken.tenantId); const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId); - const orgName = await Option.bindKnex(tenantDb).query() - .findOne('key', 'organization_name') + const orgName = await Option.bindKnex(tenantDb) + .query() + .findOne('key', 'organization_name'); // Triggers `onUserCheckInvite` event. this.eventDispatcher.dispatch(events.inviteUser.checkInvite, { - inviteToken, orgName, + inviteToken, + orgName, }); return { inviteToken, orgName }; } /** * 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 { + private async throwErrorIfUserEmailExists( + email: string + ): Promise { const { systemUserRepository } = this.sysRepositories; const foundUser = await systemUserRepository.getByEmail(email); @@ -160,14 +181,18 @@ export default class InviteUserService { /** * Validate the given user email and phone number uniquine. - * @param {IInviteUserInput} inviteUserInput + * @param {IInviteUserInput} inviteUserInput */ - private async validateUserPhoneNumber(inviteUserInput: IInviteUserInput): Promise { + private async validateUserPhoneNumber( + inviteUserInput: IInviteUserInput + ): Promise { const { systemUserRepository } = this.sysRepositories; - const foundUser = await systemUserRepository.getByPhoneNumber(inviteUserInput.phoneNumber) - + const foundUser = await systemUserRepository.getByPhoneNumber( + inviteUserInput.phoneNumber + ); + if (foundUser) { throw new ServiceError('phone_number_exists'); } } -} \ No newline at end of file +} diff --git a/server/src/services/Organization/index.ts b/server/src/services/Organization/index.ts index a50d67930..8bbbf48d8 100644 --- a/server/src/services/Organization/index.ts +++ b/server/src/services/Organization/index.ts @@ -9,7 +9,7 @@ import events from 'subscribers/events'; import { TenantAlreadyInitialized, TenantAlreadySeeded, - TenantDatabaseNotBuilt + TenantDatabaseNotBuilt, } from 'exceptions'; 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. - * @param {srting} organizationId + * @param {srting} organizationId * @return {Promise} */ - public async build(organizationId: string): Promise { + public async build(organizationId: string, user: ISystemUser): Promise { const tenant = await this.getTenantByOrgIdOrThrowError(organizationId); this.throwIfTenantInitizalized(tenant); @@ -46,15 +46,18 @@ export default class OrganizationService { try { 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); } - 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); // Throws `onOrganizationBuild` event. - this.eventDispatcher.dispatch(events.organization.build, { tenant }); - + this.eventDispatcher.dispatch(events.organization.build, { tenant, user }); } catch (error) { if (error instanceof TenantAlreadyInitialized) { throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED); @@ -66,7 +69,7 @@ export default class OrganizationService { /** * Seeds initial core data to the given organization tenant. - * @param {number} organizationId + * @param {number} organizationId * @return {Promise} */ public async seed(organizationId: string): Promise { @@ -74,12 +77,13 @@ export default class OrganizationService { this.throwIfTenantSeeded(tenant); 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); // Throws `onOrganizationBuild` event. this.eventDispatcher.dispatch(events.organization.seeded, { tenant }); - } catch (error) { if (error instanceof TenantAlreadySeeded) { throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED); @@ -93,11 +97,13 @@ export default class OrganizationService { /** * Listing all associated organizations to the given user. - * @param {ISystemUser} user - + * @param {ISystemUser} user - * @return {Promise} */ public async listOrganizations(user: ISystemUser): Promise { - 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 tenant = await tenantRepository.getById(user.tenantId); @@ -107,7 +113,7 @@ export default class OrganizationService { /** * Throws error in case the given tenant is undefined. - * @param {ITenant} tenant + * @param {ITenant} tenant */ private throwIfTenantNotExists(tenant: ITenant) { if (!tenant) { @@ -118,7 +124,7 @@ export default class OrganizationService { /** * Throws error in case the given tenant is already initialized. - * @param {ITenant} tenant + * @param {ITenant} tenant */ private throwIfTenantInitizalized(tenant: ITenant) { if (tenant.initializedAt) { @@ -128,7 +134,7 @@ export default class OrganizationService { /** * Throws service if the tenant already seeded. - * @param {ITenant} tenant + * @param {ITenant} tenant */ private throwIfTenantSeeded(tenant: ITenant) { 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. - * @param {string} organizationId + * @param {string} organizationId * @return {ITenant} */ private async getTenantByOrgIdOrThrowError(organizationId: string) { @@ -149,4 +155,4 @@ export default class OrganizationService { return tenant; } -} \ No newline at end of file +} diff --git a/server/src/services/Payment/LicenseMailMessages.ts b/server/src/services/Payment/LicenseMailMessages.ts index fce87b5b6..82088d0aa 100644 --- a/server/src/services/Payment/LicenseMailMessages.ts +++ b/server/src/services/Payment/LicenseMailMessages.ts @@ -1,8 +1,6 @@ -import fs from 'fs'; -import path from 'path'; -import Mustache from 'mustache'; import { Container } from 'typedi'; - +import Mail from 'lib/Mail'; +import config from 'config'; export default class SubscriptionMailMessages { /** * Send license code to the given mail address. @@ -11,26 +9,18 @@ export default class SubscriptionMailMessages { */ public async sendMailLicense(licenseCode: string, email: string) { const Logger = Container.get('logger'); - const Mail = Container.get('mail'); - - const filePath = path.join(global.__root, 'views/mail/LicenseReceive.html'); - const template = fs.readFileSync(filePath, 'utf8'); - const rendered = Mustache.render(template, { licenseCode }); - - const mailOptions = { - to: email, - from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, - subject: 'Bigcapital License', - html: rendered, - }; - return new Promise((resolve, reject) => { - Mail.sendMail(mailOptions, (error) => { - if (error) { - reject(error); - return; - } - resolve(); + + const mail = new Mail() + .setView('mail/LicenseReceive.html') + .setSubject('Bigcapital - License code') + .setTo(email) + .setData({ + licenseCode, + successEmail: config.customerSuccess.email, + successPhoneNumber: config.customerSuccess.phoneNumber, }); - }); + + await mail.send(); + Logger.info('[license_mail] sent successfully.'); } } \ No newline at end of file diff --git a/server/src/services/Payment/LicenseSMSMessages.ts b/server/src/services/Payment/LicenseSMSMessages.ts index 70ed40a9e..022ded1b4 100644 --- a/server/src/services/Payment/LicenseSMSMessages.ts +++ b/server/src/services/Payment/LicenseSMSMessages.ts @@ -11,7 +11,7 @@ export default class SubscriptionSMSMessages { * @param {string} licenseCode */ 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); } } \ No newline at end of file diff --git a/server/src/services/SMSClient/EasySmsClient.ts b/server/src/services/SMSClient/EasySmsClient.ts index 85bfea4b6..d7c4f6332 100644 --- a/server/src/services/SMSClient/EasySmsClient.ts +++ b/server/src/services/SMSClient/EasySmsClient.ts @@ -12,7 +12,8 @@ export default class EasySMSClient implements SMSClientInterface { */ send(to: string, message: string) { 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) => { axios.get(`https://easysms.devs.ly/sms/api?${params}`) diff --git a/server/src/services/SMSClient/SMSAPI.ts b/server/src/services/SMSClient/SMSAPI.ts index 0b2e85251..0d2d0b903 100644 --- a/server/src/services/SMSClient/SMSAPI.ts +++ b/server/src/services/SMSClient/SMSAPI.ts @@ -8,7 +8,7 @@ export default class SMSAPI { } /** - * + * Sends the message to the target via the client. * @param {string} to * @param {string} message * @param {array} extraParams diff --git a/server/src/subscribers/authentication.ts b/server/src/subscribers/authentication.ts index 142c017c6..718747dd4 100644 --- a/server/src/subscribers/authentication.ts +++ b/server/src/subscribers/authentication.ts @@ -5,9 +5,11 @@ import events from 'subscribers/events'; @EventSubscriber() export class AuthenticationSubscriber { - + /** + * Resets the login throttle once the login success. + */ @On(events.auth.login) - public async onLogin(payload) { + public async resetLoginThrottleOnceSuccessLogin(payload) { const { emailOrPhone, password, user } = payload; const loginThrottler = Container.get('rateLimiter.login'); @@ -15,26 +17,28 @@ export class AuthenticationSubscriber { // Reset the login throttle by the given email and phone number. await loginThrottler.reset(user.email); await loginThrottler.reset(user.phoneNumber); + await loginThrottler.reset(emailOrPhone); } + /** + * Sends welcome email once the user register. + */ @On(events.auth.register) - public onRegister(payload) { - const { registerDTO, user } = payload; + public async sendWelcomeEmail(payload) { + const { registerDTO, tenant, user } = payload; const agenda = Container.get('agenda'); // Send welcome mail to the user. - agenda.now('welcome-email', { - ...pick(registerDTO, ['organizationName']), + await agenda.now('welcome-email', { + organizationId: tenant.organizationId, user, }); } - @On(events.auth.resetPassword) - public onResetPassword(payload) { - - } - + /** + * Sends reset password mail once the reset password success. + */ @On(events.auth.sendResetPassword) public onSendResetPassword (payload) { const { user, token } = payload; diff --git a/server/src/subscribers/inviteUser.ts b/server/src/subscribers/inviteUser.ts index 1abd7d2f6..ce4df1e2c 100644 --- a/server/src/subscribers/inviteUser.ts +++ b/server/src/subscribers/inviteUser.ts @@ -24,6 +24,5 @@ export class InviteUserSubscriber { const { invite } = payload; const agenda = Container.get('agenda'); - } } \ No newline at end of file diff --git a/server/src/subscribers/organization.ts b/server/src/subscribers/organization.ts index 8e54d2cab..0abe42991 100644 --- a/server/src/subscribers/organization.ts +++ b/server/src/subscribers/organization.ts @@ -4,12 +4,13 @@ import events from 'subscribers/events'; @EventSubscriber() export class OrganizationSubscriber { - + /** + * Sends welcome SMS once the organization build completed. + */ @On(events.organization.build) - public async onBuild(payload) { - const { tenant, user } = payload; + public async onBuild({ tenant, user }) { const agenda = Container.get('agenda'); - - await agenda.now('welcome-sms', { tenant, user }); + + await agenda.now('welcome-sms', { tenant, user }); } } \ No newline at end of file diff --git a/server/src/system/repositories/SubscriptionRepository.ts b/server/src/system/repositories/SubscriptionRepository.ts index 39d3a2d55..408695768 100644 --- a/server/src/system/repositories/SubscriptionRepository.ts +++ b/server/src/system/repositories/SubscriptionRepository.ts @@ -14,6 +14,7 @@ export default class SubscriptionRepository extends SystemRepository{ */ getBySlugInTenant(slug: string, tenantId: number) { const key = `subscription.slug.${slug}.tenant.${tenantId}`; + return this.cache.get(key, () => { return PlanSubscription.query().findOne('slug', slug).where('tenant_id', tenantId); }); diff --git a/server/src/utils/index.js b/server/src/utils/index.js index ab6ddd696..2b4061414 100644 --- a/server/src/utils/index.js +++ b/server/src/utils/index.js @@ -245,6 +245,7 @@ function defaultToTransform( : _transfromedValue; } + export { hashPassword, origin, @@ -265,5 +266,5 @@ export { convertEmptyStringToNull, formatNumber, isBlank, - defaultToTransform + defaultToTransform, }; diff --git a/server/views/mail/LicenseReceive.html b/server/views/mail/LicenseReceive.html index 9ef71c781..6a8c721e3 100644 --- a/server/views/mail/LicenseReceive.html +++ b/server/views/mail/LicenseReceive.html @@ -374,12 +374,12 @@

License Code

-

License {{ licenseCode }},

-

Click On The link blow to reset your password.

- +

+

{{ licenseCode }}

+

+ this email. If you face any issues, please contact us at {{ successEmail }} or call {{ successPhoneNumber }}

diff --git a/server/views/mail/Welcome.html b/server/views/mail/Welcome.html index 2f610316a..31af866ad 100644 --- a/server/views/mail/Welcome.html +++ b/server/views/mail/Welcome.html @@ -371,10 +371,15 @@

-

Hi {{ firstName }}, Welcome to Bigcapital

+

Hi {{ firstName }}, Welcome to Bigcapital,

-

You’ve joined the new Bigcapital workspace {{ organizationName }}.

-

If you need any help to get started please don't hesitate to contact us to help you via phone number or email.

+ +

+ Your organization Id: {{ organizationId }} +

+

We are available to help you get started and answer any questions you may have. You can also email {{ successEmail }} or call {{ successPhoneNumber }} about your set-up questions.

+ +

Thank you for trusting Bigcapital Software for your business needs. We look forward to serving you!