mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 04:10:32 +00:00
refactor: Authentication service.
This commit is contained in:
@@ -67,5 +67,6 @@ module.exports = {
|
||||
},
|
||||
easySMSGateway: {
|
||||
api_key: 'b0JDZW56RnV6aEthb0RGPXVEcUI'
|
||||
}
|
||||
},
|
||||
jwtSecret: 'b0JDZW56RnV6aEthb0RGPXVEcUI',
|
||||
};
|
||||
|
||||
11
server/src/exceptions/ServiceError.ts
Normal file
11
server/src/exceptions/ServiceError.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export default class ServiceError {
|
||||
errorType: string;
|
||||
message: string;
|
||||
|
||||
constructor(errorType: string, message?: string) {
|
||||
this.errorType = errorType;
|
||||
this.message = message || null;
|
||||
}
|
||||
}
|
||||
15
server/src/exceptions/ServiceErrors.ts
Normal file
15
server/src/exceptions/ServiceErrors.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import ServiceError from './ServiceError';
|
||||
|
||||
|
||||
export default class ServiceErrors {
|
||||
errors: ServiceError[];
|
||||
|
||||
constructor(errors: ServiceError[]) {
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
hasType(errorType: string) {
|
||||
return this.errors
|
||||
.filter((error: ServiceError) => error.errorType === errorType);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
|
||||
import ServiceError from './ServiceError';
|
||||
import ServiceErrors from './ServiceErrors';
|
||||
|
||||
export {
|
||||
NotAllowedChangeSubscriptionPlan,
|
||||
ServiceError,
|
||||
ServiceErrors,
|
||||
};
|
||||
@@ -1,327 +0,0 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { pick } from 'lodash';
|
||||
import uniqid from 'uniqid';
|
||||
import moment from 'moment';
|
||||
import Logger from '@/services/Logger';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
import dbManager from '@/database/manager';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import PasswordReset from '@/system/models/PasswordReset';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login',
|
||||
this.login.validation,
|
||||
asyncMiddleware(this.login.handler));
|
||||
|
||||
router.post('/register',
|
||||
this.register.validation,
|
||||
asyncMiddleware(this.register.handler));
|
||||
|
||||
router.post('/send_reset_password',
|
||||
this.sendResetPassword.validation,
|
||||
asyncMiddleware(this.sendResetPassword.handler));
|
||||
|
||||
router.post('/reset/:token',
|
||||
this.resetPassword.validation,
|
||||
asyncMiddleware(this.resetPassword.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* User login authentication request.
|
||||
*/
|
||||
login: {
|
||||
validation: [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const { JWT_SECRET_KEY } = process.env;
|
||||
|
||||
Logger.log('info', 'Someone trying to login.', { form });
|
||||
|
||||
const user = await SystemUser.query()
|
||||
.withGraphFetched('tenant')
|
||||
.where('email', form.crediential)
|
||||
.orWhere('phone_number', form.crediential)
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!user.verifyPassword(form.password)) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!user.active) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 110 }],
|
||||
});
|
||||
}
|
||||
const lastLoginAt = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(user.tenant.organizationId);
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
const updateTenantUser = TenantUser.tenant().query()
|
||||
.where('id', user.id)
|
||||
.update({ last_login_at: lastLoginAt });
|
||||
|
||||
const updateSystemUser = SystemUser.query()
|
||||
.where('id', user.id)
|
||||
.update({ last_login_at: lastLoginAt });
|
||||
|
||||
await Promise.all([updateTenantUser, updateSystemUser]);
|
||||
|
||||
const token = jwt.sign(
|
||||
{ email: user.email, _id: user.id },
|
||||
JWT_SECRET_KEY,
|
||||
{ expiresIn: '1d' },
|
||||
);
|
||||
Logger.log('info', 'Logging success.', { form });
|
||||
|
||||
return res.status(200).send({ token, user });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a new organization.
|
||||
*/
|
||||
register: {
|
||||
validation: [
|
||||
check('organization_name').exists().trim().escape(),
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('email').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
check('country').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
Logger.log('info', 'Someone trying to register.', { form });
|
||||
|
||||
const user = await SystemUser.query()
|
||||
.where('email', form.email)
|
||||
.orWhere('phone_number', form.phone_number)
|
||||
.first();
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
if (user && user.phoneNumber === form.phone_number) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
||||
}
|
||||
if (user && user.email === form.email) {
|
||||
errorReasons.push({ type: 'EMAIL_EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const organizationId = uniqid();
|
||||
const tenantOrganization = await Tenant.query().insert({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
|
||||
const hashedPassword = await hashPassword(form.password);
|
||||
const userInsert = {
|
||||
...pick(form, ['first_name', 'last_name', 'email', 'phone_number']),
|
||||
active: true,
|
||||
};
|
||||
const registeredUser = await SystemUser.query().insert({
|
||||
...userInsert,
|
||||
password: hashedPassword,
|
||||
tenant_id: tenantOrganization.id,
|
||||
});
|
||||
await dbManager.createDb(`bigcapital_tenant_${organizationId}`);
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(organizationId);
|
||||
await tenantDb.migrate.latest();
|
||||
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
await TenantUser.bindKnex(tenantDb).query().insert({
|
||||
...userInsert,
|
||||
invite_accepted_at: moment().format('YYYY/MM/DD HH:mm:ss'),
|
||||
});
|
||||
Logger.log('info', 'New tenant has been created.', { organizationId });
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, { ...form });
|
||||
const mailOptions = {
|
||||
to: userInsert.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: 'Welcome to Bigcapital',
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send welcome mail', { error, form });
|
||||
return;
|
||||
}
|
||||
Logger.log('info', 'User has been sent welcome email successfuly.', { form });
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Send reset password link via email or SMS.
|
||||
*/
|
||||
sendResetPassword: {
|
||||
validation: [
|
||||
check('email').exists().isEmail(),
|
||||
],
|
||||
// eslint-disable-next-line consistent-return
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
Logger.log('info', 'User trying to send reset password.', { form });
|
||||
|
||||
const user = await SystemUser.query().where('email', form.email).first();
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 200 }],
|
||||
});
|
||||
}
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
await PasswordReset.query()
|
||||
.where('email', form.email)
|
||||
.delete();
|
||||
|
||||
const token = uniqid();
|
||||
const passwordReset = await PasswordReset.query()
|
||||
.insert({ email: form.email, token });
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, {
|
||||
url: `${req.protocol}://${req.hostname}/reset/${passwordReset.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,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send reset password mail', { error, form });
|
||||
return;
|
||||
}
|
||||
Logger.log('info', 'User has been sent reset password email successfuly.', { form });
|
||||
});
|
||||
res.status(200).send({ email: passwordReset.email });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset password.
|
||||
*/
|
||||
resetPassword: {
|
||||
validation: [
|
||||
check('password').exists().isLength({ min: 5 }).custom((value, { req }) => {
|
||||
if (value !== req.body.confirm_password) {
|
||||
throw new Error("Passwords don't match");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
Logger.log('info', 'User trying to reset password.');
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
const tokenModel = await PasswordReset.query()
|
||||
.where('token', token)
|
||||
// .where('created_at', '>=', Date.now() - 3600000)
|
||||
.first();
|
||||
|
||||
if (!tokenModel) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||
});
|
||||
}
|
||||
const user = await SystemUser.query()
|
||||
.where('email', tokenModel.email).first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await SystemUser.query()
|
||||
.where('email', tokenModel.email)
|
||||
.update({
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
// Delete the reset password token.
|
||||
await PasswordReset.query().where('token', token).delete();
|
||||
Logger.log('info', 'User password has been reset successfully.');
|
||||
|
||||
return res.status(200).send({});
|
||||
},
|
||||
},
|
||||
};
|
||||
225
server/src/http/controllers/Authentication.ts
Normal file
225
server/src/http/controllers/Authentication.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, validationResult, matchedData, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import prettierMiddleware from '@/http/middleware/prettierMiddleware';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import { IRegisterDTO } from 'src/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationController {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
this.loginSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.login.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/register',
|
||||
this.registerSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.register.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/send_reset_password',
|
||||
this.sendResetPasswordSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.sendResetPassword.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/reset/:token',
|
||||
this.resetPasswordSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.resetPassword.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login schema.
|
||||
*/
|
||||
get loginSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register schema.
|
||||
*/
|
||||
get registerSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('organization_name').exists().trim().escape(),
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('email').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
check('country').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password schema.
|
||||
*/
|
||||
get resetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('password').exists().isLength({ min: 5 }).custom((value, { req }) => {
|
||||
if (value !== req.body.confirm_password) {
|
||||
throw new Error("Passwords don't match");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('email').exists().isEmail().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user login.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async login(req: Request, res: Response, next: Function): Response {
|
||||
const userDTO: IUserOTD = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
|
||||
try {
|
||||
const { token, user } = await this.authService.signIn(
|
||||
userDTO.crediential,
|
||||
userDTO.password
|
||||
);
|
||||
return res.status(200).send({ token, user });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'invalid_details') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'user_inactive') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization register handler.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async register(req: Request, res: Response, next: Function) {
|
||||
const registerDTO: IRegisterDTO = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
|
||||
try {
|
||||
const registeredUser = await this.authService.register(registerDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'REGISTER.SUCCESS',
|
||||
message: 'Register organization has been success.',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceErrors) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (error.hasType('phone_number_exists')) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
||||
}
|
||||
if (error.hasType('email_exists')) {
|
||||
errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(200).send({ errors: errorReasons });
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
||||
});
|
||||
} catch(error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'email_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async resetPassword(req: Request, res: Response) {
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.resetPassword(token, password);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'RESET_PASSWORD_SUCCESS',
|
||||
})
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'token_invalid') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'user_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -10,7 +10,6 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import moment from 'moment';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import Invite from '@/system/models/Invite';
|
||||
|
||||
@@ -31,7 +31,7 @@ import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjecti
|
||||
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
|
||||
|
||||
export default (app) => {
|
||||
app.use('/api/auth', Authentication.router());
|
||||
app.use('/api/auth', Container.get(Authentication).router());
|
||||
app.use('/api/invite', InviteUsers.router());
|
||||
app.use('/api/vouchers', Container.get(VouchersController).router());
|
||||
app.use('/api/subscription', Container.get(Subscription).router());
|
||||
|
||||
10
server/src/interfaces/Register.ts
Normal file
10
server/src/interfaces/Register.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
|
||||
export interface IRegisterDTO {
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
password: string,
|
||||
organizationName: string,
|
||||
};
|
||||
9
server/src/interfaces/User.ts
Normal file
9
server/src/interfaces/User.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
export interface ISystemUser {
|
||||
|
||||
}
|
||||
|
||||
export interface ISystemUserDTO {
|
||||
|
||||
}
|
||||
@@ -28,6 +28,13 @@ import {
|
||||
ISaleEstimate,
|
||||
ISaleEstimateOTD,
|
||||
} from './SaleEstimate';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
} from './Register';
|
||||
import {
|
||||
ISystemUser,
|
||||
ISystemUserDTO,
|
||||
} from './User';
|
||||
|
||||
export {
|
||||
IBillPaymentEntry,
|
||||
@@ -58,4 +65,8 @@ export {
|
||||
|
||||
IPaymentReceive,
|
||||
IPaymentReceiveOTD,
|
||||
|
||||
IRegisterDTO,
|
||||
ISystemUser,
|
||||
ISystemUserDTO,
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
|
||||
export default class MailNotificationSubscribeEnd {
|
||||
|
||||
handler(job) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,27 @@
|
||||
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class MailNotificationSubscribeEnd {
|
||||
/**
|
||||
*
|
||||
* @param {Job} job -
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
}
|
||||
Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
||||
phoneNumber, remainingDays,
|
||||
);
|
||||
Logger.debug(`Send mail notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.error(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
server/src/jobs/MailNotificationTrialEnd.ts
Normal file
27
server/src/jobs/MailNotificationTrialEnd.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class MailNotificationTrialEnd {
|
||||
/**
|
||||
*
|
||||
* @param {Job} job -
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
||||
phoneNumber, remainingDays,
|
||||
);
|
||||
Logger.debug(`Send mail notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.error(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
server/src/jobs/ResetPasswordMail.ts
Normal file
44
server/src/jobs/ResetPasswordMail.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Mustache from 'mustache';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
export default class ResetPasswordMailJob {
|
||||
/**
|
||||
*
|
||||
* @param job
|
||||
* @param done
|
||||
*/
|
||||
handler(job, done) {
|
||||
const { user, token } = job.attrs.data;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
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, {
|
||||
url: `https://google.com/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,
|
||||
};
|
||||
Mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.info('[send_reset_password] failed send reset password mail', { error, user });
|
||||
done(error);
|
||||
return;
|
||||
}
|
||||
Logger.info('[send_reset_password] user has been sent reset password email successfuly.', { user });
|
||||
done();
|
||||
});
|
||||
res.status(200).send({ email: passwordReset.email });
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,28 @@
|
||||
|
||||
|
||||
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class SMSNotificationSubscribeEnd {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Job}job
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, subscriptionSlug } = job.attrs.data;
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send SMS notification subscription end soon - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
subscriptionService.smsMessages.sendRemainingSubscriptionPeriod(
|
||||
phoneNumber, remainingDays,
|
||||
);
|
||||
Logger.debug(`Send SMS notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.error(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,28 @@
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class SMSNotificationTrialEnd {
|
||||
|
||||
export default class MailNotificationSubscribeEnd {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Job}job
|
||||
*/
|
||||
handler(job) {
|
||||
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send notification subscription end soon - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
subscriptionService.smsMessages.sendRemainingTrialPeriod(
|
||||
phoneNumber, remainingDays,
|
||||
);
|
||||
Logger.debug(`Send notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.error(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
server/src/jobs/UserInviteMail.ts
Normal file
8
server/src/jobs/UserInviteMail.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export default class UserInviteMailJob {
|
||||
|
||||
handler(job, done) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,38 @@
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import path from 'path';
|
||||
import { Container } from 'typedi';
|
||||
import MailerService from '../services/mailer';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
/**
|
||||
*
|
||||
* @param {Job} job
|
||||
* @param {Function} done
|
||||
*/
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { email, organizationName, firstName } = job.attrs.data;
|
||||
const Logger = Container.get('logger');
|
||||
const Mail = Container.get('mail');
|
||||
|
||||
console.log('✌Email Sequence Job triggered!');
|
||||
done();
|
||||
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, {
|
||||
email, organizationName, firstName,
|
||||
});
|
||||
const mailOptions = {
|
||||
to: email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: 'Welcome to Bigcapital',
|
||||
html: rendered,
|
||||
};
|
||||
Mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.error('Failed send welcome mail', { error, form });
|
||||
done(error);
|
||||
return;
|
||||
}
|
||||
Logger.info('User has been sent welcome email successfuly.', { form });
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Container } from 'typedi';
|
||||
import LoggerInstance from '@/services/Logger';
|
||||
import agendaFactory from '@/loaders/agenda';
|
||||
import SmsClientLoader from '@/loaders/smsClient';
|
||||
import mailInstance from '@/loaders/mail';
|
||||
|
||||
export default ({ mongoConnection, knex }) => {
|
||||
try {
|
||||
@@ -20,6 +21,9 @@ export default ({ mongoConnection, knex }) => {
|
||||
Container.set('SMSClient', smsClientInstance);
|
||||
LoggerInstance.info('SMS client has been injected into container');
|
||||
|
||||
Container.set('mail', mailInstance);
|
||||
LoggerInstance.info('Mail instance has been injected into container');
|
||||
|
||||
return { agenda: agendaInstance };
|
||||
} catch (e) {
|
||||
LoggerInstance.error('Error on dependency injector loader: %o', e);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Here we import all events.
|
||||
import '@/subscribers/authentication';
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import Agenda from 'agenda';
|
||||
import WelcomeEmailJob from '@/Jobs/welcomeEmail';
|
||||
import ResetPasswordMailJob from '@/Jobs/ResetPasswordMail';
|
||||
import ComputeItemCost from '@/Jobs/ComputeItemCost';
|
||||
import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries';
|
||||
import SendVoucherViaPhoneJob from '@/jobs/SendVoucherPhone';
|
||||
import SendVoucherViaEmailJob from '@/jobs/SendVoucherEmail';
|
||||
import SendSMSNotificationSubscribeEnd from '@/jobs/SMSNotificationSubscribeEnd';
|
||||
import SendSMSNotificationTrialEnd from '@/jobs/SMSNotificationTrialEnd';
|
||||
import SendMailNotificationSubscribeEnd from '@/jobs/MailNotificationSubscribeEnd';
|
||||
import SendMailNotificationTrialEnd from '@/jobs/MailNotificationTrialEnd';
|
||||
import UserInviteMailJob from '@/jobs/UserInviteMail';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
agenda.define(
|
||||
@@ -11,6 +17,16 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
{ priority: 'high' },
|
||||
new WelcomeEmailJob().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'reset-password-mail',
|
||||
{ priority: 'high' },
|
||||
new ResetPasswordMailJob().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'user-invite-mail',
|
||||
{ priority: 'high' },
|
||||
new UserInviteMailJob().handler,
|
||||
)
|
||||
agenda.define(
|
||||
'compute-item-cost',
|
||||
{ priority: 'high', concurrency: 20 },
|
||||
@@ -31,21 +47,25 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
new SendVoucherViaEmailJob().handler,
|
||||
);
|
||||
// agenda.define(
|
||||
// 'send-sms-notification-subscribe-end',
|
||||
// { priority: 'high', concurrency: 1, },
|
||||
// );
|
||||
// agenda.define(
|
||||
// 'send-mail-notification-subscribe-end',
|
||||
// { priority: 'high', concurrency: 1, },
|
||||
// );
|
||||
// agenda.define(
|
||||
// 'send-sms-notification-trial-end',
|
||||
// { priority: 'high', concurrency: 1, },
|
||||
// );
|
||||
// agenda.define(
|
||||
// 'send-mail-notification-trial-end',
|
||||
// { priority: 'high', concurrency: 1, },
|
||||
// );
|
||||
agenda.define(
|
||||
'send-sms-notification-subscribe-end',
|
||||
{ priority: 'nromal', concurrency: 1, },
|
||||
new SendSMSNotificationSubscribeEnd().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'send-sms-notification-trial-end',
|
||||
{ priority: 'normal', concurrency: 1, },
|
||||
new SendSMSNotificationTrialEnd().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'send-mail-notification-subscribe-end',
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
new SendMailNotificationSubscribeEnd().handler
|
||||
);
|
||||
agenda.define(
|
||||
'send-mail-notification-trial-end',
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
new SendMailNotificationTrialEnd().handler
|
||||
);
|
||||
agenda.start();
|
||||
};
|
||||
|
||||
@@ -12,14 +12,4 @@ const transporter = nodemailer.createTransport({
|
||||
},
|
||||
});
|
||||
|
||||
console.log({
|
||||
host: config.mail.host,
|
||||
port: config.mail.port,
|
||||
secure: config.mail.secure, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: config.mail.username,
|
||||
pass: config.mail.password,
|
||||
},
|
||||
});
|
||||
|
||||
export default transporter;
|
||||
export default transporter;
|
||||
252
server/src/services/Authentication/index.ts
Normal file
252
server/src/services/Authentication/index.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Service, Inject, Container } from "typedi";
|
||||
import JWT from 'jsonwebtoken';
|
||||
import uniqid from 'uniqid';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
EventDispatcher
|
||||
EventDispatcherInterface
|
||||
} from '@/decorators/eventDispatcher';
|
||||
import {
|
||||
SystemUser,
|
||||
PasswordReset,
|
||||
Tenant,
|
||||
} from '@/system/models';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
ITenant,
|
||||
ISystemUser,
|
||||
IPasswordReset,
|
||||
} from '@/interfaces';
|
||||
import TenantsManager from "@/system/TenantsManager";
|
||||
import { hashPassword } from '@/utils';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import config from '@/../config/config';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} emailOrPhone - Email or phone number.
|
||||
* @param {string} password - Password.
|
||||
* @return {Promise<{user: IUser, token: string}>}
|
||||
*/
|
||||
async signIn(emailOrPhone: string, password: string): Promise<{user: IUser, token: string }> {
|
||||
this.logger.info('[login] Someone trying to login.', { emailOrPhone, password });
|
||||
|
||||
const user = await SystemUser.query()
|
||||
.where('email', emailOrPhone)
|
||||
.orWhere('phone_number', emailOrPhone)
|
||||
.withGraphFetched('tenant')
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
this.logger.info('[login] invalid data');
|
||||
throw new ServiceError('invalid_details');
|
||||
}
|
||||
|
||||
this.logger.info('[login] check password validation.');
|
||||
if (!user.verifyPassword(password)) {
|
||||
throw new ServiceError('password_invalid');
|
||||
}
|
||||
|
||||
if (!user.active) {
|
||||
this.logger.info('[login] user inactive.');
|
||||
throw new ServiceError('user_inactive');
|
||||
}
|
||||
|
||||
this.logger.info('[login] generating JWT token.');
|
||||
const token = this.generateToken(user);
|
||||
|
||||
this.logger.info('[login] Logging success.', { user, token });
|
||||
|
||||
this.eventDispatcher.dispatch(events.auth.login, {
|
||||
emailOrPhone, password,
|
||||
});
|
||||
return { user, token };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates email and phone number uniqiness on the storage.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IRegisterDTO} registerDTO - Register data object.
|
||||
*/
|
||||
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
|
||||
const user: ISystemUser = await SystemUser.query()
|
||||
.where('email', registerDTO.email)
|
||||
.orWhere('phone_number', registerDTO.phoneNumber)
|
||||
.first();
|
||||
|
||||
const errorReasons: ServiceErrors[] = [];
|
||||
|
||||
if (user && user.phoneNumber === registerDTO.phoneNumber) {
|
||||
this.logger.info('[register] phone number exists on the storage.');
|
||||
errorReasons.push(new ServiceError('phone_number_exists'));
|
||||
}
|
||||
if (user && user.email === registerDTO.email) {
|
||||
this.logger.info('[register] email exists on the storage.');
|
||||
errorReasons.push(new ServiceError('email_exists'));
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
throw new ServiceErrors(errorReasons);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new tenant with user from user input.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IUserDTO} user
|
||||
*/
|
||||
async register(registerDTO: IRegisterDTO): Promise<ISystemUser> {
|
||||
this.logger.info('[register] Someone trying to register.');
|
||||
await this.validateEmailAndPhoneUniqiness(registerDTO);
|
||||
|
||||
this.logger.info('[register] Creating a new tenant org.')
|
||||
const tenant = await this.newTenantOrganization();
|
||||
|
||||
this.logger.info('[register] Trying hashing the password.')
|
||||
const hashedPassword = await hashPassword(registerDTO.password);
|
||||
|
||||
const registeredUser = await SystemUser.query().insert({
|
||||
...omit(registerDTO, 'country', 'organizationName'),
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenant_id: tenant.id,
|
||||
});
|
||||
|
||||
this.eventDispatcher.dispatch(events.auth.register, { registerDTO });
|
||||
|
||||
return registeredUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and insert new tenant organization id.
|
||||
* @async
|
||||
* @return {Promise<ITenant>}
|
||||
*/
|
||||
private async newTenantOrganization(): Promise<ITenant> {
|
||||
const organizationId = uniqid();
|
||||
const tenantOrganization = await Tenant.query().insert({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
return tenantOrganization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tenant database.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @return {void}
|
||||
*/
|
||||
async initializeTenant(tenantId: number): Promise<void> {
|
||||
const dbManager = Container.get('dbManager');
|
||||
|
||||
const tenant = await Tenant.query().findById(tenantId);
|
||||
|
||||
this.logger.info('[tenant_init] Tenant DB creating.', { tenant });
|
||||
await dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`);
|
||||
|
||||
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
|
||||
|
||||
this.logger.info('[tenant_init] Tenant DB migrating to latest version.', { tenant });
|
||||
await tenantDb.migrate.latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given email existance on the storage.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} email - email address.
|
||||
*/
|
||||
private async validateEmailExistance(email: string) {
|
||||
const foundEmail = await SystemUser.query().findOne('email', email);
|
||||
|
||||
if (!foundEmail) {
|
||||
this.logger.info('[send_reset_password] The given email not found.');
|
||||
throw new ServiceError('email_not_found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and retrieve password reset token for the given user email.
|
||||
* @param {string} email
|
||||
* @return {<Promise<IPasswordReset>}
|
||||
*/
|
||||
async sendResetPassword(email: string): Promise<IPasswordReset> {
|
||||
this.logger.info('[send_reset_password] Trying to send reset password.');
|
||||
await this.validateEmailExistance(email);
|
||||
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
await PasswordReset.query().where('email', email).delete();
|
||||
|
||||
const token = uniqid();
|
||||
const passwordReset = await PasswordReset.query().insert({ email, token });
|
||||
const user = await SystemUser.query().findOne('email', email);
|
||||
|
||||
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token });
|
||||
|
||||
return passwordReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a user password from given token.
|
||||
* @param {string} token - Password reset token.
|
||||
* @param {string} password - New Password.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async resetPassword(token: string, password: string): Promise<void> {
|
||||
const tokenModel = await PasswordReset.query().findOne('token', token)
|
||||
|
||||
if (!tokenModel) {
|
||||
this.logger.info('[reset_password] token invalid.');
|
||||
throw new ServiceError('token_invalid');
|
||||
}
|
||||
const user = await SystemUser.query().findOne('email', tokenModel.email)
|
||||
|
||||
if (!user) {
|
||||
throw new ServiceError('user_not_found');
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
this.logger.info('[reset_password] saving a new hashed password.');
|
||||
await SystemUser.query()
|
||||
.where('email', tokenModel.email)
|
||||
.update({
|
||||
password: hashedPassword,
|
||||
});
|
||||
// Delete the reset password token.
|
||||
await PasswordReset.query().where('email', user.email).delete();
|
||||
|
||||
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token, password });
|
||||
|
||||
this.logger.info('[reset_password] reset password success.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JWT token for the given user.
|
||||
* @param {IUser} user
|
||||
* @return {string} token
|
||||
*/
|
||||
generateToken(user: IUser): string {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
this.logger.silly(`Sign JWT for userId: ${user._id}`);
|
||||
return JWT.sign(
|
||||
{
|
||||
_id: user._id, // We are gonna use this in the middleware 'isAuth'
|
||||
exp: exp.getTime() / 1000,
|
||||
},
|
||||
config.jwtSecret,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Mustache from 'mustache';
|
||||
import { Container } from 'typedi';
|
||||
import mail from '@/services/mail';
|
||||
|
||||
export default class SubscriptionMailMessages {
|
||||
/**
|
||||
@@ -11,7 +10,8 @@ export default class SubscriptionMailMessages {
|
||||
* @param {email} email
|
||||
*/
|
||||
public async sendMailVoucher(voucherCode: string, email: string) {
|
||||
const logger = Container.get('logger');
|
||||
const Logger = Container.get('logger');
|
||||
const Mail = Container.get('mail');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/VoucherReceive.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
@@ -24,7 +24,7 @@ export default class SubscriptionMailMessages {
|
||||
html: rendered,
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
Mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
||||
@@ -326,7 +326,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
/**
|
||||
* Schedules compute sale invoice items cost based on each item
|
||||
* cost method.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @return {Promise}
|
||||
*/
|
||||
async scheduleComputeInvoiceItemsCost(
|
||||
@@ -343,7 +343,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
.filter((entry: IItemEntry) => entry.item.type === 'inventory')
|
||||
.map((entry: IItemEntry) => entry.itemId)
|
||||
.uniq().value();
|
||||
|
||||
|
||||
if (inventoryItemsIds.length === 0) {
|
||||
await this.writeNonInventoryInvoiceJournals(tenantId, saleInvoice, override);
|
||||
} else {
|
||||
|
||||
21
server/src/services/Subscription/MailMessages.ts
Normal file
21
server/src/services/Subscription/MailMessages.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Service } from "typedi";
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionMailMessages {
|
||||
|
||||
public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) {
|
||||
const message: string = `
|
||||
Your remaining subscription is ${remainingDays} days,
|
||||
please renew your subscription before expire.
|
||||
`;
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
|
||||
public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) {
|
||||
const message: string = `
|
||||
Your remaining free trial is ${remainingDays} days,
|
||||
please subscription before ends, if you have any quation to contact us.`;
|
||||
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
}
|
||||
24
server/src/services/Subscription/SMSMessages.ts
Normal file
24
server/src/services/Subscription/SMSMessages.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import SMSClient from '@/services/SMSClient';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionSMSMessages {
|
||||
@Inject('SMSClient')
|
||||
smsClient: SMSClient;
|
||||
|
||||
public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) {
|
||||
const message: string = `
|
||||
Your remaining subscription is ${remainingDays} days,
|
||||
please renew your subscription before expire.
|
||||
`;
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
|
||||
public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) {
|
||||
const message: string = `
|
||||
Your remaining free trial is ${remainingDays} days,
|
||||
please subscription before ends, if you have any quation to contact us.`;
|
||||
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import { Service } from 'typedi';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Plan, Tenant, Voucher } from '@/system/models';
|
||||
import Subscription from '@/services/Subscription/Subscription';
|
||||
import VocuherPaymentMethod from '@/services/Payment/VoucherPaymentMethod';
|
||||
import PaymentContext from '@/services/Payment';
|
||||
import SubscriptionSMSMessages from '@/services/Subscription/SMSMessages';
|
||||
import SubscriptionMailMessages from '@/services/Subscription/MailMessages';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionService {
|
||||
@Inject()
|
||||
smsMessages: SubscriptionSMSMessages;
|
||||
|
||||
@Inject()
|
||||
mailMessages: SubscriptionMailMessages;
|
||||
|
||||
/**
|
||||
* Handles the payment process via voucher code and than subscribe to
|
||||
* the given tenant.
|
||||
|
||||
41
server/src/subscribers/authentication.ts
Normal file
41
server/src/subscribers/authentication.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Container } from 'typedi';
|
||||
import { pick } from 'lodash';
|
||||
import { EventSubscriber, On } from 'event-dispatch';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
|
||||
@EventSubscriber()
|
||||
export class AuthenticationSubscriber {
|
||||
|
||||
@On(events.auth.login)
|
||||
public onLogin(payload) {
|
||||
const { emailOrPhone, password } = payload;
|
||||
}
|
||||
|
||||
@On(events.auth.register)
|
||||
public onRegister(payload) {
|
||||
const { registerDTO } = payload;
|
||||
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
// Send welcome mail to the user.
|
||||
agenda.now('welcome-email', {
|
||||
...pick(registerDTO, ['email', 'organizationName', 'firstName']),
|
||||
});
|
||||
}
|
||||
|
||||
@On(events.auth.resetPassword)
|
||||
public onResetPassword(payload) {
|
||||
|
||||
}
|
||||
|
||||
@On(events.auth.sendResetPassword)
|
||||
public onSendResetPassword (payload) {
|
||||
const { user, token } = payload;
|
||||
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
// Send reset password mail.
|
||||
agenda.now('reset-password-mail', { user, token })
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
auth: {
|
||||
login: 'onLogin',
|
||||
register: 'onRegister',
|
||||
sendResetPassword: 'onSendResetPassword',
|
||||
resetPassword: 'onResetPassword',
|
||||
},
|
||||
}
|
||||
0
server/src/subscribers/users.ts
Normal file
0
server/src/subscribers/users.ts
Normal file
@@ -1,5 +1,6 @@
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import { Service } from 'typedi';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import config from '@/../config/config';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
@@ -17,13 +18,14 @@ import TenantUser from '@/models/TenantUser';
|
||||
// tenantOrganizationId: String,
|
||||
// }
|
||||
|
||||
@Service()
|
||||
export default class TenantsManager {
|
||||
|
||||
constructor() {
|
||||
this.knexCache = new Map();
|
||||
}
|
||||
|
||||
static async getTenant(organizationId) {
|
||||
async getTenant(organizationId) {
|
||||
const tenant = await Tenant.query()
|
||||
.where('organization_id', organizationId).first();
|
||||
|
||||
@@ -35,7 +37,7 @@ export default class TenantsManager {
|
||||
* @param {Integer} uniqId
|
||||
* @return {TenantWebsite}
|
||||
*/
|
||||
static async createTenant(uniqId) {
|
||||
async createTenant(uniqId) {
|
||||
const organizationId = uniqId || uniqid();
|
||||
const tenantOrganization = await Tenant.query().insert({
|
||||
organization_id: organizationId,
|
||||
@@ -58,7 +60,7 @@ export default class TenantsManager {
|
||||
* Drop tenant database of the given tenant website.
|
||||
* @param {TenantWebsite} tenantWebsite
|
||||
*/
|
||||
static async dropTenant(tenantWebsite) {
|
||||
async dropTenant(tenantWebsite) {
|
||||
const tenantDbName = `bigcapital_tenant_${tenantWebsite.organizationId}`;
|
||||
await dbManager.dropDb(tenantDbName);
|
||||
|
||||
@@ -69,7 +71,7 @@ export default class TenantsManager {
|
||||
/**
|
||||
* Creates a user that associate to the given tenant.
|
||||
*/
|
||||
static async createTenantUser(tenantWebsite, user) {
|
||||
async createTenantUser(tenantWebsite, user) {
|
||||
const userInsert = { ...user };
|
||||
|
||||
const systemUser = await SystemUser.query().insert({
|
||||
@@ -92,7 +94,7 @@ export default class TenantsManager {
|
||||
/**
|
||||
* Retrieve all tenants metadata from system storage.
|
||||
*/
|
||||
static getAllTenants() {
|
||||
getAllTenants() {
|
||||
return Tenant.query();
|
||||
}
|
||||
|
||||
@@ -100,7 +102,7 @@ export default class TenantsManager {
|
||||
* Retrieve the given organization id knex configuration.
|
||||
* @param {String} organizationId -
|
||||
*/
|
||||
static getTenantKnexConfig(organizationId) {
|
||||
getTenantKnexConfig(organizationId) {
|
||||
return {
|
||||
client: config.tenant.db_client,
|
||||
connection: {
|
||||
@@ -120,7 +122,7 @@ export default class TenantsManager {
|
||||
};
|
||||
}
|
||||
|
||||
static knexInstance(organizationId) {
|
||||
knexInstance(organizationId) {
|
||||
const knexCache = new Map();
|
||||
let knex = knexCache.get(organizationId);
|
||||
|
||||
@@ -4,6 +4,8 @@ import PlanFeature from './Subscriptions/PlanFeature';
|
||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||
import Voucher from './Subscriptions/Voucher';
|
||||
import Tenant from './Tenant';
|
||||
import SystemUser from './SystemUser';
|
||||
import PasswordReset from './PasswordReset';
|
||||
|
||||
export {
|
||||
Plan,
|
||||
@@ -11,4 +13,6 @@ export {
|
||||
PlanSubscription,
|
||||
Voucher,
|
||||
Tenant,
|
||||
SystemUser,
|
||||
PasswordReset,
|
||||
}
|
||||
@@ -364,15 +364,18 @@
|
||||
<tr>
|
||||
<td>
|
||||
<p class="align-center">
|
||||
<svg data-icon="bigcapital" class="bigcapital" width="190" height="37" viewBox="0 0 309.09 42.89"><desc>bigcapital</desc><path d="M56,3.16,61.33,8.5,31.94,37.9l-5.35-5.35Z" class="path-1" fill-rule="evenodd"></path><path d="M29.53,6.94l5.35,5.34L5.49,41.67.14,36.33l15.8-15.8Z" class="path-2" fill-rule="evenodd"></path><path d="M94.36,38.87H79.62v-31H94c6.33,0,10.22,3.15,10.22,8V16a7.22,7.22,0,0,1-4.07,6.69c3.58,1.37,5.8,3.45,5.8,7.61v.09C106,36,101.35,38.87,94.36,38.87Zm3.1-21.81c0-2-1.59-3.19-4.47-3.19H86.26v6.55h6.29c3,0,4.91-1,4.91-3.28Zm1.72,12.39c0-2.08-1.54-3.37-5-3.37H86.26V32.9h8.1c3,0,4.82-1.06,4.82-3.36Z" class="path-3" fill-rule="evenodd"></path><path d="M110.56,12.54v-6h7.08v6Zm.18,26.33V15.15h6.72V38.87Z" class="path-4" fill-rule="evenodd"></path><path d="M134,46a22.55,22.55,0,0,1-10.49-2.47l2.3-5a15.52,15.52,0,0,0,8,2.17c4.61,0,6.78-2.21,6.78-6.46V33.08c-2,2.39-4.16,3.85-7.75,3.85-5.53,0-10.53-4-10.53-11.07v-.09c0-7.08,5.09-11.06,10.53-11.06a9.63,9.63,0,0,1,7.66,3.54v-3.1h6.72V33.52C147.2,42.46,142.78,46,134,46Zm6.6-20.27a5.79,5.79,0,0,0-11.56,0v.09a5.42,5.42,0,0,0,5.76,5.49,5.49,5.49,0,0,0,5.8-5.49Z" class="path-5" fill-rule="evenodd"></path><path d="M164,39.41a12.11,12.11,0,0,1-12.35-12.26v-.09a12.18,12.18,0,0,1,12.44-12.35c4.47,0,7.25,1.5,9.47,4l-4.12,4.43a6.93,6.93,0,0,0-5.4-2.61c-3.36,0-5.75,3-5.75,6.46v.09c0,3.63,2.34,6.55,6,6.55,2.26,0,3.8-1,5.44-2.53l3.94,4A12,12,0,0,1,164,39.41Z" class="path-6" fill-rule="evenodd"></path><path d="M191.51,38.87V36.31a9.15,9.15,0,0,1-7.17,3c-4.47,0-8.15-2.57-8.15-7.26V32c0-5.18,3.94-7.57,9.56-7.57a16.74,16.74,0,0,1,5.8,1V25c0-2.79-1.72-4.34-5.09-4.34a17.57,17.57,0,0,0-6.55,1.28l-1.68-5.13a21,21,0,0,1,9.21-1.9c7.34,0,10.57,3.8,10.57,10.22V38.87Zm.13-9.55a10.3,10.3,0,0,0-4.29-.89c-2.88,0-4.65,1.15-4.65,3.27v.09c0,1.82,1.5,2.88,3.67,2.88,3.15,0,5.27-1.73,5.27-4.16Z" class="path-7" fill-rule="evenodd"></path><path d="M217.49,39.32a9.1,9.1,0,0,1-7.39-3.54V46h-6.73V15.15h6.73v3.41a8.7,8.7,0,0,1,7.39-3.85c5.53,0,10.8,4.34,10.8,12.26v.09C228.29,35,223.11,39.32,217.49,39.32ZM221.56,27c0-3.94-2.66-6.55-5.8-6.55S210,23,210,27v.09c0,3.94,2.61,6.55,5.75,6.55s5.8-2.57,5.8-6.55Z" class="path-8" fill-rule="evenodd"></path><path d="M232.93,12.54v-6H240v6Zm.18,26.33V15.15h6.73V38.87Z" class="path-9" fill-rule="evenodd"></path><path d="M253.73,39.27c-4.11,0-6.9-1.63-6.9-7.12V20.91H244V15.15h2.83V9.09h6.73v6.06h5.57v5.76h-5.57V31c0,1.55.66,2.3,2.16,2.3A6.84,6.84,0,0,0,259,32.5v5.4A9.9,9.9,0,0,1,253.73,39.27Z" class="path-10" fill-rule="evenodd"></path><path d="M277.55,38.87V36.31a9.15,9.15,0,0,1-7.18,3c-4.46,0-8.14-2.57-8.14-7.26V32c0-5.18,3.94-7.57,9.56-7.57a16.74,16.74,0,0,1,5.8,1V25c0-2.79-1.73-4.34-5.09-4.34A17.57,17.57,0,0,0,266,21.92l-1.68-5.13a20.94,20.94,0,0,1,9.2-1.9c7.35,0,10.58,3.8,10.58,10.22V38.87Zm.13-9.55a10.31,10.31,0,0,0-4.3-.89c-2.87,0-4.64,1.15-4.64,3.27v.09c0,1.82,1.5,2.88,3.67,2.88,3.14,0,5.27-1.73,5.27-4.16Z" class="path-11" fill-rule="evenodd"></path><path d="M289.72,38.87V6.57h6.72v32.3Z" class="path-12" fill-rule="evenodd"></path><path d="M302.06,38.87V31.79h7.17v7.08Z" class="path-13" fill-rule="evenodd"></path></svg>
|
||||
<svg data-icon="bigcapital" class="bigcapital" width="190" height="37" viewBox="0 0 309.09 42.89">
|
||||
<desc>bigcapital</desc>
|
||||
<path d="M56,3.16,61.33,8.5,31.94,37.9l-5.35-5.35Z" class="path-1" fill-rule="evenodd"></path><path d="M29.53,6.94l5.35,5.34L5.49,41.67.14,36.33l15.8-15.8Z" class="path-2" fill-rule="evenodd"></path><path d="M94.36,38.87H79.62v-31H94c6.33,0,10.22,3.15,10.22,8V16a7.22,7.22,0,0,1-4.07,6.69c3.58,1.37,5.8,3.45,5.8,7.61v.09C106,36,101.35,38.87,94.36,38.87Zm3.1-21.81c0-2-1.59-3.19-4.47-3.19H86.26v6.55h6.29c3,0,4.91-1,4.91-3.28Zm1.72,12.39c0-2.08-1.54-3.37-5-3.37H86.26V32.9h8.1c3,0,4.82-1.06,4.82-3.36Z" class="path-3" fill-rule="evenodd"></path><path d="M110.56,12.54v-6h7.08v6Zm.18,26.33V15.15h6.72V38.87Z" class="path-4" fill-rule="evenodd"></path><path d="M134,46a22.55,22.55,0,0,1-10.49-2.47l2.3-5a15.52,15.52,0,0,0,8,2.17c4.61,0,6.78-2.21,6.78-6.46V33.08c-2,2.39-4.16,3.85-7.75,3.85-5.53,0-10.53-4-10.53-11.07v-.09c0-7.08,5.09-11.06,10.53-11.06a9.63,9.63,0,0,1,7.66,3.54v-3.1h6.72V33.52C147.2,42.46,142.78,46,134,46Zm6.6-20.27a5.79,5.79,0,0,0-11.56,0v.09a5.42,5.42,0,0,0,5.76,5.49,5.49,5.49,0,0,0,5.8-5.49Z" class="path-5" fill-rule="evenodd"></path><path d="M164,39.41a12.11,12.11,0,0,1-12.35-12.26v-.09a12.18,12.18,0,0,1,12.44-12.35c4.47,0,7.25,1.5,9.47,4l-4.12,4.43a6.93,6.93,0,0,0-5.4-2.61c-3.36,0-5.75,3-5.75,6.46v.09c0,3.63,2.34,6.55,6,6.55,2.26,0,3.8-1,5.44-2.53l3.94,4A12,12,0,0,1,164,39.41Z" class="path-6" fill-rule="evenodd"></path><path d="M191.51,38.87V36.31a9.15,9.15,0,0,1-7.17,3c-4.47,0-8.15-2.57-8.15-7.26V32c0-5.18,3.94-7.57,9.56-7.57a16.74,16.74,0,0,1,5.8,1V25c0-2.79-1.72-4.34-5.09-4.34a17.57,17.57,0,0,0-6.55,1.28l-1.68-5.13a21,21,0,0,1,9.21-1.9c7.34,0,10.57,3.8,10.57,10.22V38.87Zm.13-9.55a10.3,10.3,0,0,0-4.29-.89c-2.88,0-4.65,1.15-4.65,3.27v.09c0,1.82,1.5,2.88,3.67,2.88,3.15,0,5.27-1.73,5.27-4.16Z" class="path-7" fill-rule="evenodd"></path><path d="M217.49,39.32a9.1,9.1,0,0,1-7.39-3.54V46h-6.73V15.15h6.73v3.41a8.7,8.7,0,0,1,7.39-3.85c5.53,0,10.8,4.34,10.8,12.26v.09C228.29,35,223.11,39.32,217.49,39.32ZM221.56,27c0-3.94-2.66-6.55-5.8-6.55S210,23,210,27v.09c0,3.94,2.61,6.55,5.75,6.55s5.8-2.57,5.8-6.55Z" class="path-8" fill-rule="evenodd"></path><path d="M232.93,12.54v-6H240v6Zm.18,26.33V15.15h6.73V38.87Z" class="path-9" fill-rule="evenodd"></path><path d="M253.73,39.27c-4.11,0-6.9-1.63-6.9-7.12V20.91H244V15.15h2.83V9.09h6.73v6.06h5.57v5.76h-5.57V31c0,1.55.66,2.3,2.16,2.3A6.84,6.84,0,0,0,259,32.5v5.4A9.9,9.9,0,0,1,253.73,39.27Z" class="path-10" fill-rule="evenodd"></path><path d="M277.55,38.87V36.31a9.15,9.15,0,0,1-7.18,3c-4.46,0-8.14-2.57-8.14-7.26V32c0-5.18,3.94-7.57,9.56-7.57a16.74,16.74,0,0,1,5.8,1V25c0-2.79-1.73-4.34-5.09-4.34A17.57,17.57,0,0,0,266,21.92l-1.68-5.13a20.94,20.94,0,0,1,9.2-1.9c7.35,0,10.58,3.8,10.58,10.22V38.87Zm.13-9.55a10.31,10.31,0,0,0-4.3-.89c-2.87,0-4.64,1.15-4.64,3.27v.09c0,1.82,1.5,2.88,3.67,2.88,3.14,0,5.27-1.73,5.27-4.16Z" class="path-11" fill-rule="evenodd"></path><path d="M289.72,38.87V6.57h6.72v32.3Z" class="path-12" fill-rule="evenodd"></path><path d="M302.06,38.87V31.79h7.17v7.08Z" class="path-13" fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="align-center">
|
||||
<h3>Hi {{ first_name }}, Welcome to Bigcapital</h3>
|
||||
<h3>Hi {{ firstName }}, Welcome to Bigcapital</h3>
|
||||
</p>
|
||||
<p class="mgb-1x">You’ve joined the new Bigcapital workspace {{ organization_name }}.</p>
|
||||
<p class="mgb-1x">You’ve 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>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user