diff --git a/server/config/config.js b/server/config/config.js index d0fa748a9..23d1c8d44 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -67,5 +67,6 @@ module.exports = { }, easySMSGateway: { api_key: 'b0JDZW56RnV6aEthb0RGPXVEcUI' - } + }, + jwtSecret: 'b0JDZW56RnV6aEthb0RGPXVEcUI', }; diff --git a/server/src/exceptions/ServiceError.ts b/server/src/exceptions/ServiceError.ts new file mode 100644 index 000000000..4a2ce8d12 --- /dev/null +++ b/server/src/exceptions/ServiceError.ts @@ -0,0 +1,11 @@ + + +export default class ServiceError { + errorType: string; + message: string; + + constructor(errorType: string, message?: string) { + this.errorType = errorType; + this.message = message || null; + } +} \ No newline at end of file diff --git a/server/src/exceptions/ServiceErrors.ts b/server/src/exceptions/ServiceErrors.ts new file mode 100644 index 000000000..0cae991fb --- /dev/null +++ b/server/src/exceptions/ServiceErrors.ts @@ -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); + } +} \ No newline at end of file diff --git a/server/src/exceptions/index.ts b/server/src/exceptions/index.ts index 28cbb1a4a..8343e2fc6 100644 --- a/server/src/exceptions/index.ts +++ b/server/src/exceptions/index.ts @@ -1,5 +1,9 @@ import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan'; +import ServiceError from './ServiceError'; +import ServiceErrors from './ServiceErrors'; export { NotAllowedChangeSubscriptionPlan, + ServiceError, + ServiceErrors, }; \ No newline at end of file diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js deleted file mode 100644 index 91f27a711..000000000 --- a/server/src/http/controllers/Authentication.js +++ /dev/null @@ -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({}); - }, - }, -}; diff --git a/server/src/http/controllers/Authentication.ts b/server/src/http/controllers/Authentication.ts new file mode 100644 index 000000000..5fc3d36f3 --- /dev/null +++ b/server/src/http/controllers/Authentication.ts @@ -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 }], + }); + } + } + } + } +}; diff --git a/server/src/http/controllers/InviteUsers.js b/server/src/http/controllers/InviteUsers.js index 325a86dce..9f385fa46 100644 --- a/server/src/http/controllers/InviteUsers.js +++ b/server/src/http/controllers/InviteUsers.js @@ -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'; diff --git a/server/src/http/index.js b/server/src/http/index.js index 9cb582902..d8f6852bc 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -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()); diff --git a/server/src/interfaces/Register.ts b/server/src/interfaces/Register.ts new file mode 100644 index 000000000..cbe5637c0 --- /dev/null +++ b/server/src/interfaces/Register.ts @@ -0,0 +1,10 @@ + + + +export interface IRegisterDTO { + firstName: string, + lastName: string, + email: string, + password: string, + organizationName: string, +}; \ No newline at end of file diff --git a/server/src/interfaces/User.ts b/server/src/interfaces/User.ts new file mode 100644 index 000000000..652305cc7 --- /dev/null +++ b/server/src/interfaces/User.ts @@ -0,0 +1,9 @@ + + +export interface ISystemUser { + +} + +export interface ISystemUserDTO { + +} \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 344e66647..6eef4afee 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -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, }; \ No newline at end of file diff --git a/server/src/jobs/MailNotifcationSubscribeEnd.ts b/server/src/jobs/MailNotifcationSubscribeEnd.ts deleted file mode 100644 index eb24a6382..000000000 --- a/server/src/jobs/MailNotifcationSubscribeEnd.ts +++ /dev/null @@ -1,8 +0,0 @@ - - -export default class MailNotificationSubscribeEnd { - - handler(job) { - - } -} \ No newline at end of file diff --git a/server/src/jobs/MailNotificationSubscribeEnd.ts b/server/src/jobs/MailNotificationSubscribeEnd.ts index 8a5b072c6..1f5d9451d 100644 --- a/server/src/jobs/MailNotificationSubscribeEnd.ts +++ b/server/src/jobs/MailNotificationSubscribeEnd.ts @@ -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'); -} \ No newline at end of file + 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); + } + } +} diff --git a/server/src/jobs/MailNotificationTrialEnd.ts b/server/src/jobs/MailNotificationTrialEnd.ts new file mode 100644 index 000000000..c2b62cffa --- /dev/null +++ b/server/src/jobs/MailNotificationTrialEnd.ts @@ -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); + } + } +} \ No newline at end of file diff --git a/server/src/jobs/ResetPasswordMail.ts b/server/src/jobs/ResetPasswordMail.ts new file mode 100644 index 000000000..49e5176aa --- /dev/null +++ b/server/src/jobs/ResetPasswordMail.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/server/src/jobs/SMSNotificationSubscribeEnd.ts b/server/src/jobs/SMSNotificationSubscribeEnd.ts index fbe8a4f62..d9302a955 100644 --- a/server/src/jobs/SMSNotificationSubscribeEnd.ts +++ b/server/src/jobs/SMSNotificationSubscribeEnd.ts @@ -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); + } } } \ No newline at end of file diff --git a/server/src/jobs/SMSNotificationTrialEnd.ts b/server/src/jobs/SMSNotificationTrialEnd.ts index eb24a6382..69ca54b39 100644 --- a/server/src/jobs/SMSNotificationTrialEnd.ts +++ b/server/src/jobs/SMSNotificationTrialEnd.ts @@ -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); + } } } \ No newline at end of file diff --git a/server/src/jobs/UserInviteMail.ts b/server/src/jobs/UserInviteMail.ts new file mode 100644 index 000000000..e379a8097 --- /dev/null +++ b/server/src/jobs/UserInviteMail.ts @@ -0,0 +1,8 @@ + + +export default class UserInviteMailJob { + + handler(job, done) { + + } +} \ No newline at end of file diff --git a/server/src/jobs/welcomeEmail.ts b/server/src/jobs/welcomeEmail.ts index 089824512..16d160e54 100644 --- a/server/src/jobs/welcomeEmail.ts +++ b/server/src/jobs/welcomeEmail.ts @@ -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 { + 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(); + }); } } diff --git a/server/src/loaders/dependencyInjector.ts b/server/src/loaders/dependencyInjector.ts index 39c5e00f7..56acce66a 100644 --- a/server/src/loaders/dependencyInjector.ts +++ b/server/src/loaders/dependencyInjector.ts @@ -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); diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index e69de29bb..5cbb85728 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -0,0 +1,3 @@ +// Here we import all events. +import '@/subscribers/authentication'; + diff --git a/server/src/loaders/jobs.ts b/server/src/loaders/jobs.ts index 58f014f65..bb72e93ee 100644 --- a/server/src/loaders/jobs.ts +++ b/server/src/loaders/jobs.ts @@ -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(); }; diff --git a/server/src/services/mail.js b/server/src/loaders/mail.ts similarity index 62% rename from server/src/services/mail.js rename to server/src/loaders/mail.ts index 6beded076..3a7988e39 100644 --- a/server/src/services/mail.js +++ b/server/src/loaders/mail.ts @@ -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; \ No newline at end of file diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts new file mode 100644 index 000000000..841a7720a --- /dev/null +++ b/server/src/services/Authentication/index.ts @@ -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 { + 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} + */ + private async newTenantOrganization(): Promise { + 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 { + 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 {} + */ + async sendResetPassword(email: string): Promise { + 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} + */ + async resetPassword(token: string, password: string): Promise { + 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, + ); + } +} \ No newline at end of file diff --git a/server/src/services/Payment/VoucherMailMessages.ts b/server/src/services/Payment/VoucherMailMessages.ts index 14fc2a3bc..cd22d8e6b 100644 --- a/server/src/services/Payment/VoucherMailMessages.ts +++ b/server/src/services/Payment/VoucherMailMessages.ts @@ -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; diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 981c00382..581760148 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -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 { diff --git a/server/src/services/Subscription/MailMessages.ts b/server/src/services/Subscription/MailMessages.ts new file mode 100644 index 000000000..572fc84d2 --- /dev/null +++ b/server/src/services/Subscription/MailMessages.ts @@ -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); + } +} \ No newline at end of file diff --git a/server/src/services/Subscription/SMSMessages.ts b/server/src/services/Subscription/SMSMessages.ts new file mode 100644 index 000000000..5af0a4909 --- /dev/null +++ b/server/src/services/Subscription/SMSMessages.ts @@ -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); + } +} \ No newline at end of file diff --git a/server/src/services/Subscription/SubscriptionService.ts b/server/src/services/Subscription/SubscriptionService.ts index 397582fb0..5a850ec8b 100644 --- a/server/src/services/Subscription/SubscriptionService.ts +++ b/server/src/services/Subscription/SubscriptionService.ts @@ -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. diff --git a/server/src/subscribers/authentication.ts b/server/src/subscribers/authentication.ts new file mode 100644 index 000000000..a2e2da00f --- /dev/null +++ b/server/src/subscribers/authentication.ts @@ -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 }) + } +} \ No newline at end of file diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 83c3035ff..fd3c9611d 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -4,6 +4,7 @@ export default { auth: { login: 'onLogin', register: 'onRegister', + sendResetPassword: 'onSendResetPassword', resetPassword: 'onResetPassword', }, } \ No newline at end of file diff --git a/server/src/subscribers/users.ts b/server/src/subscribers/users.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/system/TenantsManager.js b/server/src/system/TenantsManager.ts similarity index 91% rename from server/src/system/TenantsManager.js rename to server/src/system/TenantsManager.ts index ffde9d466..cb83f90d6 100644 --- a/server/src/system/TenantsManager.js +++ b/server/src/system/TenantsManager.ts @@ -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); diff --git a/server/src/system/models/index.js b/server/src/system/models/index.js index 55cc3e359..0a8b49c9a 100644 --- a/server/src/system/models/index.js +++ b/server/src/system/models/index.js @@ -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, } \ No newline at end of file diff --git a/server/views/mail/Welcome.html b/server/views/mail/Welcome.html index 9e9b926fb..bde1211fe 100644 --- a/server/views/mail/Welcome.html +++ b/server/views/mail/Welcome.html @@ -364,15 +364,18 @@

- bigcapital + + bigcapital + +


-

Hi {{ first_name }}, Welcome to Bigcapital

+

Hi {{ firstName }}, Welcome to Bigcapital

-

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

+

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.