fix: issues in authentication mail messages.

This commit is contained in:
Ahmed Bouhuolia
2020-09-05 13:29:39 +02:00
parent 9ee7ed89ec
commit 481ca8aa8b
13 changed files with 159 additions and 71 deletions

View File

@@ -74,4 +74,6 @@ module.exports = {
api_key: 'b0JDZW56RnV6aEthb0RGPXVEcUI' api_key: 'b0JDZW56RnV6aEthb0RGPXVEcUI'
}, },
jwtSecret: 'b0JDZW56RnV6aEthb0RGPXVEcUI', jwtSecret: 'b0JDZW56RnV6aEthb0RGPXVEcUI',
contactUsMail: 'support@bigcapital.ly',
baseURL: 'https://bigcapital.ly',
}; };

View File

@@ -76,14 +76,15 @@ export default class AuthenticationController extends BaseController{
*/ */
get resetPasswordSchema(): ValidationChain[] { get resetPasswordSchema(): ValidationChain[] {
return [ return [
check('password').exists().isLength({ min: 5 }).custom((value, { req }) => { check('password').exists().isLength({ min: 5 })
if (value !== req.body.confirm_password) { .custom((value, { req }) => {
throw new Error("Passwords don't match"); if (value !== req.body.confirm_password) {
} else { throw new Error("Passwords don't match");
return value; } else {
} return value;
}), }
] }),
];
} }
/** /**
@@ -111,7 +112,7 @@ export default class AuthenticationController extends BaseController{
return res.status(200).send({ token, user }); return res.status(200).send({ token, user });
} catch (error) { } catch (error) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'invalid_details') { if (['invalid_details', 'invalid_password'].indexOf(error.errorType) !== -1) {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'INVALID_DETAILS', code: 100 }], errors: [{ type: 'INVALID_DETAILS', code: 100 }],
}); });
@@ -201,8 +202,6 @@ export default class AuthenticationController extends BaseController{
type: 'RESET_PASSWORD_SUCCESS', type: 'RESET_PASSWORD_SUCCESS',
}) })
} catch(error) { } catch(error) {
console.log(error);
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'token_invalid') { if (error.errorType === 'token_invalid') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {

View File

@@ -2,8 +2,17 @@ import { Container, Inject } from 'typedi';
import AuthenticationService from '@/services/Authentication'; import AuthenticationService from '@/services/Authentication';
export default class WelcomeEmailJob { export default class WelcomeEmailJob {
@Inject() /**
authService: AuthenticationService; * Constructor method.
* @param {Agenda} agenda
*/
constructor(agenda) {
agenda.define(
'reset-password-mail',
{ priority: 'high' },
this.handler.bind(this),
);
}
/** /**
* Handle send welcome mail job. * Handle send welcome mail job.
@@ -11,17 +20,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 { email, organizationName, firstName } = job.attrs.data; const { user, token } = job.attrs.data;
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const authService = Container.get(AuthenticationService);
Logger.info(`Send reset password mail - started: ${job.attrs.data}`); Logger.info(`[send_reset_password] started: ${job.attrs.data}`);
try { try {
await this.authService.mailMessages.sendResetPasswordMessage(); await authService.mailMessages.sendResetPasswordMessage(user, token);
Logger.info(`Send reset password mail - finished: ${job.attrs.data}`); Logger.info(`[send_reset_password] finished: ${job.attrs.data}`);
done() done()
} catch (error) { } catch (error) {
Logger.info(`Send reset password mail - error: ${job.attrs.data}, error: ${error}`); Logger.info(`[send_reset_password] error: ${job.attrs.data}, error: ${error}`);
done(error); done(error);
} }
} }

View File

@@ -2,8 +2,18 @@ import { Container, Inject } from 'typedi';
import AuthenticationService from '@/services/Authentication'; import AuthenticationService from '@/services/Authentication';
export default class WelcomeEmailJob { export default class WelcomeEmailJob {
@Inject() /**
authService: AuthenticationService; * Constructor method.
* @param {Agenda} agenda -
*/
constructor(agenda) {
// Welcome mail and SMS message.
agenda.define(
'welcome-email',
{ priority: 'high' },
this.handler.bind(this),
);
}
/** /**
* Handle send welcome mail job. * Handle send welcome mail job.
@@ -11,17 +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 { email, organizationName, firstName } = job.attrs.data; const { organizationName, user } = job.attrs.data;
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const authService = Container.get(AuthenticationService);
Logger.info(`[welcome_mail] send welcome mail message - started: ${job.attrs.data}`);
Logger.info(`Send welcome mail message - started: ${job.attrs.data}`);
try { try {
await this.authService.mailMessages.sendWelcomeMessage(); await authService.mailMessages.sendWelcomeMessage(user, organizationName);
Logger.info(`Send welcome mail message - finished: ${job.attrs.data}`); Logger.info(`[welcome_mail] send welcome mail message - finished: ${job.attrs.data}`);
done() done();
} catch (error) { } catch (error) {
Logger.info(`Send welcome mail message - error: ${job.attrs.data}, error: ${error}`); Logger.info(`[welcome_mail] send welcome mail message - error: ${job.attrs.data}, error: ${error}`);
done(error); done(error);
} }
} }

View File

@@ -13,23 +13,15 @@ import SendMailNotificationTrialEnd from '@/jobs/MailNotificationTrialEnd';
import UserInviteMailJob from '@/jobs/UserInviteMail'; import UserInviteMailJob from '@/jobs/UserInviteMail';
export default ({ agenda }: { agenda: Agenda }) => { export default ({ agenda }: { agenda: Agenda }) => {
// Welcome mail and SMS message. new WelcomeEmailJob(agenda);
agenda.define( new ResetPasswordMailJob(agenda);
'welcome-email',
{ priority: 'high' },
new WelcomeEmailJob().handler,
);
agenda.define( agenda.define(
'welcome-sms', 'welcome-sms',
{ priority: 'high' }, { priority: 'high' },
new WelcomeSMSJob().handler new WelcomeSMSJob().handler
); );
// Reset password mail.
agenda.define(
'reset-password-mail',
{ priority: 'high' },
new ResetPasswordMailJob().handler,
);
// User invite mail. // User invite mail.
agenda.define( agenda.define(
'user-invite-mail', 'user-invite-mail',

View File

@@ -1,35 +1,78 @@
import { Service } from "typedi"; import fs from 'fs';
import { Service, Container } from "typedi";
import Mustache from 'mustache';
import path from 'path';
import { ISystemUser } from '@/interfaces';
import config from '@/../config/config';
import { ISystemUser } from 'src/interfaces';
@Service() @Service()
export default class AuthenticationMailMesssages { export default class AuthenticationMailMesssages {
/**
sendWelcomeMessage() { * Sends welcome message.
const Logger = Container.get('logger'); * @param {ISystemUser} user - The system user.
* @param {string} organizationName -
* @return {Promise<void>}
*/
sendWelcomeMessage(user: ISystemUser, organizationName: string): Promise<void> {
const Mail = Container.get('mail'); const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html'); const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
const template = fs.readFileSync(filePath, 'utf8'); const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, { const rendered = Mustache.render(template, {
email, organizationName, firstName, email: user.email,
firstName: user.firstName,
organizationName,
}); });
const mailOptions = { const mailOptions = {
to: email, to: user.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`, from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Welcome to Bigcapital', subject: 'Welcome to Bigcapital',
html: rendered, html: rendered,
}; };
Mail.sendMail(mailOptions, (error) => { return new Promise((resolve, reject) => {
if (error) { Mail.sendMail(mailOptions, (error) => {
Logger.error('Failed send welcome mail', { error, form }); if (error) {
done(error); resolve(error);
return; return;
} }
Logger.info('User has been sent welcome email successfuly.', { form }); reject();
done(); });
}); });
} }
sendResetPasswordMessage() { /**
* Sends reset password message.
*
* @param {ISystemUser} user - The system user.
* @param {string} token - Reset password token.
* @return {Promise<void>}
*/
sendResetPasswordMessage(user: ISystemUser, token: string): Promise<void> {
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, '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();
});
});
} }
} }

View File

@@ -65,7 +65,7 @@ export default class AuthenticationService {
this.logger.info('[login] check password validation.'); this.logger.info('[login] check password validation.');
if (!user.verifyPassword(password)) { if (!user.verifyPassword(password)) {
throw new ServiceError('password_invalid'); throw new ServiceError('invalid_password');
} }
if (!user.active) { if (!user.active) {
@@ -133,8 +133,10 @@ export default class AuthenticationService {
tenant_id: tenant.id, tenant_id: tenant.id,
}); });
this.eventDispatcher.dispatch(events.auth.register, { registerDTO }); // Triggers `onRegister` event.
this.eventDispatcher.dispatch(events.auth.register, {
registerDTO, user: registeredUser
});
return registeredUser; return registeredUser;
} }
@@ -194,9 +196,12 @@ export default class AuthenticationService {
await this.validateEmailExistance(email); 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.');
await PasswordReset.query().where('email', email).delete(); await PasswordReset.query().where('email', email).delete();
const token = uniqid(); const token = uniqid();
this.logger.info('[send_reset_password] insert the generated token.');
const passwordReset = await PasswordReset.query().insert({ email, token }); const passwordReset = await PasswordReset.query().insert({ email, token });
const user = await SystemUser.query().findOne('email', email); const user = await SystemUser.query().findOne('email', email);

View File

@@ -2,7 +2,11 @@ import { Service } from "typedi";
@Service() @Service()
export default class SubscriptionMailMessages { export default class SubscriptionMailMessages {
/**
*
* @param phoneNumber
* @param remainingDays
*/
public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) { public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) {
const message: string = ` const message: string = `
Your remaining subscription is ${remainingDays} days, Your remaining subscription is ${remainingDays} days,
@@ -11,6 +15,11 @@ export default class SubscriptionMailMessages {
this.smsClient.sendMessage(phoneNumber, message); this.smsClient.sendMessage(phoneNumber, message);
} }
/**
*
* @param phoneNumber
* @param remainingDays
*/
public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) { public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) {
const message: string = ` const message: string = `
Your remaining free trial is ${remainingDays} days, Your remaining free trial is ${remainingDays} days,

View File

@@ -1,3 +1,4 @@
import { Inject } from 'typedi';
import { Tenant, Plan } from '@/system/models'; import { Tenant, Plan } from '@/system/models';
import { IPaymentContext } from '@/interfaces'; import { IPaymentContext } from '@/interfaces';
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions'; import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
@@ -5,6 +6,9 @@ import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
export default class Subscription<PaymentModel> { export default class Subscription<PaymentModel> {
paymentContext: IPaymentContext|null; paymentContext: IPaymentContext|null;
@Inject('logger')
logger: any;
/** /**
* Constructor method. * Constructor method.
* @param {IPaymentContext} * @param {IPaymentContext}

View File

@@ -3,7 +3,6 @@ import { pick } from 'lodash';
import { EventSubscriber, On } from 'event-dispatch'; import { EventSubscriber, On } from 'event-dispatch';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
@EventSubscriber() @EventSubscriber()
export class AuthenticationSubscriber { export class AuthenticationSubscriber {
@@ -14,13 +13,14 @@ export class AuthenticationSubscriber {
@On(events.auth.register) @On(events.auth.register)
public onRegister(payload) { public onRegister(payload) {
const { registerDTO } = payload; const { registerDTO, 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', { agenda.now('welcome-email', {
...pick(registerDTO, ['email', 'organizationName', 'firstName']), ...pick(registerDTO, ['organizationName']),
user,
}); });
} }
@@ -32,7 +32,6 @@ export class AuthenticationSubscriber {
@On(events.auth.sendResetPassword) @On(events.auth.sendResetPassword)
public onSendResetPassword (payload) { public onSendResetPassword (payload) {
const { user, token } = payload; const { user, token } = payload;
const agenda = Container.get('agenda'); const agenda = Container.get('agenda');
// Send reset password mail. // Send reset password mail.

View File

@@ -0,0 +1,16 @@
import { Container } from 'typedi';
import { On, EventSubscriber } from "event-dispatch";
import events from '@/subscribers/events';
@EventSubscriber()
export class OrganizationSubscriber {
@On(events.organization.build)
public onBuild(payload) {
const agenda = Container.get('agenda');
agenda.now('welcome-sms', {
email, organizationName, firstName,
});
}
}

View File

@@ -184,8 +184,8 @@
} }
.btn-primary a { .btn-primary a {
background-color: #2d95fd; background-color: #1968F0;
border-color: #2d95fd; border-color: #1968F0;
color: #ffffff; color: #ffffff;
} }
@@ -374,8 +374,8 @@
<p class="align-center"> <p class="align-center">
<h3>Reset Your Password</h3> <h3>Reset Your Password</h3>
</p> </p>
<p class="mgb-1x">Hi {{ first_name }} {{ last_name }},</p> <p class="mgb-1x">Hi <strong>{{ first_name }} {{ last_name }}<strong>,</p>
<p class="mgb-2-5x">Click On The link blow to reset your password.</p> <p class="mgb-2-5x">Click on the link blow to reset your password.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody> <tbody>
@@ -384,7 +384,7 @@
<table role="presentation" border="0" cellpadding="0" cellspacing="0"> <table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody> <tbody>
<tr> <tr>
<td> <a href="http://htmlemail.io" target="_blank">Reset Your password</a> </td> <td> <a href="{{ resetPasswordUrl }}" target="_blank">Reset Your password</a> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -335,8 +335,6 @@
border-color: #004dd0 !important; border-color: #004dd0 !important;
} }
} }
[data-icon="bigcapital"] path { [data-icon="bigcapital"] path {
fill: #004dd0; fill: #004dd0;
} }