mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
- feat: remove unnecessary migrations, controllers and models files.
- feat: metable store - feat: metable store with settings store. - feat: settings middleware to auto-save and load. - feat: DI db manager to master container. - feat: write some logs to sale invoices.
This commit is contained in:
@@ -4,13 +4,11 @@ import config from '@/../config/config';
|
||||
|
||||
const knexConfig = knexfile[process.env.NODE_ENV];
|
||||
|
||||
const dbManager = knexManager.databaseManagerFactory({
|
||||
export default () => knexManager.databaseManagerFactory({
|
||||
knex: knexConfig,
|
||||
dbManager: {
|
||||
collate: [],
|
||||
superUser: config.manager.superUser,
|
||||
superPassword: config.manager.superPassword,
|
||||
},
|
||||
});
|
||||
|
||||
export default dbManager;
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('permissions', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTable('permissions');
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
exports.up = (knex) => knex.schema.createTable('roles', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('description');
|
||||
table.boolean('predefined').default(false);
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `ROLES` AUTO_INCREMENT = 1000');
|
||||
|
||||
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTable('roles');
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('user_has_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('user_id').unsigned();
|
||||
table.integer('role_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('user_has_roles');
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('role_has_accounts', (table) => {
|
||||
table.increments();
|
||||
table.integer('role_id').unsigned();
|
||||
table.integer('account_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('role_has_accounts');
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('role_has_permissions', (table) => {
|
||||
table.increments();
|
||||
table.integer('role_id').unsigned();
|
||||
table.integer('permission_id').unsigned();
|
||||
table.integer('resource_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('role_has_permissions');
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('view_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('index');
|
||||
table.integer('field_id').unsigned();
|
||||
table.string('comparator');
|
||||
table.string('value');
|
||||
table.integer('view_id').unsigned();
|
||||
}).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000').then(() => {
|
||||
return knex.seed.run({
|
||||
specific: 'seed_views_roles.js',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('view_roles');
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('currency_adjustments', (table) => {
|
||||
table.increments();
|
||||
table.date('date');
|
||||
table.string('currency_code');
|
||||
table.decimal('exchange_rate', 8, 5);
|
||||
table.string('note');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('currency_adjustments');
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('recurring_journals', (table) => {
|
||||
table.increments();
|
||||
table.string('template_name');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('recurring_journals');
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('budgets', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('fiscal_year');
|
||||
table.string('period');
|
||||
table.string('account_types');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('budgets');
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('budget_entries', (table) => {
|
||||
table.increments();
|
||||
table.integer('budget_id').unsigned();
|
||||
table.integer('account_id').unsigned();
|
||||
table.decimal('amount', 15, 5);
|
||||
table.integer('order');
|
||||
})
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('budget_entries');
|
||||
};
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Originally taken from 'w3tecch/express-typescript-boilerplate'
|
||||
* Credits to the author
|
||||
*/
|
||||
|
||||
import { EventDispatcher as EventDispatcherClass } from 'event-dispatch';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
|
||||
9
server/src/exceptions/HttpException.ts
Normal file
9
server/src/exceptions/HttpException.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
class HttpException extends Error {
|
||||
public status: number;
|
||||
public message: string;
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult, oneOf } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.post('/',
|
||||
this.openingBalnace.validation,
|
||||
asyncMiddleware(this.openingBalnace.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opening balance to the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
openingBalnace: {
|
||||
validation: [
|
||||
check('date').optional(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('balance_adjustment_account').exists().isNumeric().toInt(),
|
||||
check('accounts').isArray({ min: 1 }),
|
||||
check('accounts.*.id').exists().isInt(),
|
||||
oneOf([
|
||||
check('accounts.*.debit').exists().isNumeric().toFloat(),
|
||||
check('accounts.*.credit').exists().isNumeric().toFloat(),
|
||||
]),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { accounts } = req.body;
|
||||
const { user } = req;
|
||||
const form = { ...req.body };
|
||||
const date = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
const accountsIds = accounts.map((account) => account.id);
|
||||
|
||||
const { Account, ManualJournal } = req.models;
|
||||
const storedAccounts = await Account.query()
|
||||
.select(['id']).whereIn('id', accountsIds)
|
||||
.withGraphFetched('type');
|
||||
|
||||
const accountsCollection = new Map(storedAccounts.map((i) => [i.id, i]));
|
||||
|
||||
// Get the stored accounts Ids and difference with submit accounts.
|
||||
const accountsStoredIds = storedAccounts.map((account) => account.id);
|
||||
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
|
||||
const errorReasons = [];
|
||||
|
||||
if (notFoundAccountsIds.length > 0) {
|
||||
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
|
||||
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
|
||||
}
|
||||
if (form.balance_adjustment_account) {
|
||||
const account = await Account.query().findById(form.balance_adjustment_account);
|
||||
|
||||
if (!account) {
|
||||
errorReasons.push({ type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300 });
|
||||
}
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badData(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
|
||||
accounts.forEach((account) => {
|
||||
const storedAccount = accountsCollection.get(account.id);
|
||||
|
||||
// Can't continue in case the stored account was not found.
|
||||
if (!storedAccount) { return; }
|
||||
|
||||
const entryModel = new JournalEntry({
|
||||
referenceType: 'OpeningBalance',
|
||||
account: account.id,
|
||||
accountNormal: storedAccount.type.normal,
|
||||
userId: user.id,
|
||||
});
|
||||
if (account.credit) {
|
||||
entryModel.entry.credit = account.credit;
|
||||
journalEntries.credit(entryModel);
|
||||
} else if (account.debit) {
|
||||
entryModel.entry.debit = account.debit;
|
||||
journalEntries.debit(entryModel);
|
||||
}
|
||||
});
|
||||
// Calculates the credit and debit balance of stacked entries.
|
||||
const trial = journalEntries.getTrialBalance();
|
||||
|
||||
if (trial.credit !== trial.debit) {
|
||||
const entryModel = new JournalEntry({
|
||||
referenceType: 'OpeningBalance',
|
||||
account: form.balance_adjustment_account,
|
||||
accountNormal: 'credit',
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (trial.credit > trial.debit) {
|
||||
entryModel.entry.credit = Math.abs(trial.credit);
|
||||
journalEntries.credit(entryModel);
|
||||
|
||||
} else if (trial.credit < trial.debit) {
|
||||
entryModel.entry.debit = Math.abs(trial.debit);
|
||||
journalEntries.debit(entryModel);
|
||||
}
|
||||
}
|
||||
const manualJournal = await ManualJournal.query().insert({
|
||||
amount: Math.max(trial.credit, trial.debit),
|
||||
transaction_type: 'OpeningBalance',
|
||||
date,
|
||||
note: form.note,
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
journalEntries.entries = journalEntries.entries.map((entry) => ({
|
||||
...entry,
|
||||
referenceId: manualJournal.id,
|
||||
}));
|
||||
await Promise.all([
|
||||
journalEntries.saveEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id: manualJournal.id });
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, validationResult, matchedData, ValidationChain } from 'express-validator';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
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 {
|
||||
export default class AuthenticationController extends BaseController{
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
@@ -88,6 +86,9 @@ export default class AuthenticationController {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password validation schema.
|
||||
*/
|
||||
get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('email').exists().isEmail().trim().escape(),
|
||||
@@ -100,10 +101,7 @@ export default class AuthenticationController {
|
||||
* @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));
|
||||
const userDTO: IUserOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { token, user } = await this.authService.signIn(
|
||||
@@ -134,13 +132,10 @@ export default class AuthenticationController {
|
||||
* @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));
|
||||
const registerDTO: IRegisterOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const registeredUser = await this.authService.register(registerDTO);
|
||||
const registeredUser: ISystemUser = await this.authService.register(registerDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'REGISTER.SUCCESS',
|
||||
@@ -170,7 +165,7 @@ export default class AuthenticationController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body;
|
||||
const { email } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
reconciliations: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
reconciliation: {
|
||||
validation: [
|
||||
body('from_date'),
|
||||
body('to_date'),
|
||||
body('closing_balance'),
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
import { matchedData } from "express-validator";
|
||||
import { mapKeys, camelCase, omit } from "lodash";
|
||||
|
||||
export default class BaseController {
|
||||
|
||||
matchedBodyData(req: Request, options: any) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
...omit(options, ['locations']), // override any propery except locations.
|
||||
});
|
||||
return mapKeys(data, (v, k) => camelCase(k));
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export default {
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import express from 'express';
|
||||
import uniqid from 'uniqid';
|
||||
import {
|
||||
check,
|
||||
body,
|
||||
param,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import moment from 'moment';
|
||||
import { hashPassword } from '@/utils';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import Invite from '@/system/models/Invite';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import Logger from '@/services/Logger';
|
||||
import Option from '@/models/Option';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/send', jwtAuth);
|
||||
router.use('/send', TenancyMiddleware);
|
||||
|
||||
router.post('/send',
|
||||
this.invite.validation,
|
||||
asyncMiddleware(this.invite.handler));
|
||||
|
||||
router.post('/accept/:token',
|
||||
this.accept.validation,
|
||||
asyncMiddleware(this.accept.handler));
|
||||
|
||||
router.get('/invited/:token',
|
||||
this.invited.validation,
|
||||
asyncMiddleware(this.invited.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
*/
|
||||
invite: {
|
||||
validation: [
|
||||
body('email').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 };
|
||||
const { user } = req;
|
||||
const { TenantUser } = req.models;
|
||||
const foundUser = await SystemUser.query()
|
||||
.where('email', form.email).first();
|
||||
|
||||
if (foundUser) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'USER.EMAIL.ALREADY.REGISTERED', code: 100 }],
|
||||
});
|
||||
}
|
||||
const token = uniqid();
|
||||
const invite = await Invite.query().insert({
|
||||
email: form.email,
|
||||
tenant_id: user.tenantId,
|
||||
token,
|
||||
});
|
||||
const tenantUser = await TenantUser.query().insert({
|
||||
first_name: form.email,
|
||||
email: form.email,
|
||||
});
|
||||
const { Option } = req.models;
|
||||
const organizationOptions = await Option.query()
|
||||
.where('key', 'organization_name');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const rendered = Mustache.render(template, {
|
||||
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
organizationName: organizationOptions.getMeta('organization_name'),
|
||||
});
|
||||
const mailOptions = {
|
||||
to: user.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: `${user.fullName} has invited you to join a Bigcapital`,
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send user invite mail', { error, form });
|
||||
}
|
||||
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
|
||||
});
|
||||
return res.status(200).send();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Acceprt the inviation.
|
||||
*/
|
||||
accept: {
|
||||
validation: [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { token } = req.params;
|
||||
const inviteToken = await Invite.query()
|
||||
.where('token', token).first();
|
||||
|
||||
if (!inviteToken) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
language: 'en',
|
||||
...req.body,
|
||||
};
|
||||
const systemUser = await SystemUser.query()
|
||||
.where('phone_number', form.phone_number)
|
||||
.first();
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate there is already registered phone number.
|
||||
if (systemUser && systemUser.phoneNumber === form.phone_number) {
|
||||
errorReasons.push({
|
||||
type: 'PHONE_MUMNER.ALREADY.EXISTS', code: 400,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query()
|
||||
.where('id', inviteToken.tenantId).first();
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
|
||||
const hashedPassword = await hashPassword(form.password);
|
||||
|
||||
const userForm = {
|
||||
first_name: form.first_name,
|
||||
last_name: form.last_name,
|
||||
email: inviteToken.email,
|
||||
phone_number: form.phone_number,
|
||||
language: form.language,
|
||||
active: 1,
|
||||
};
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
const foundTenantUser = await TenantUser.query()
|
||||
.where('phone_number', form.phone_number).first();
|
||||
|
||||
if (foundTenantUser) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PHONE_NUMBER.ALREADY.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
|
||||
const insertUserOper = TenantUser.bindKnex(tenantDb)
|
||||
.query()
|
||||
.where('email', userForm.email)
|
||||
.patch({
|
||||
...userForm,
|
||||
invite_accepted_at: moment().format('YYYY/MM/DD'),
|
||||
});
|
||||
|
||||
const insertSysUserOper = SystemUser.query().insert({
|
||||
...userForm,
|
||||
password: hashedPassword,
|
||||
tenant_id: inviteToken.tenantId,
|
||||
});
|
||||
|
||||
const deleteInviteTokenOper = Invite.query()
|
||||
.where('token', inviteToken.token).delete();
|
||||
|
||||
await Promise.all([
|
||||
insertUserOper,
|
||||
insertSysUserOper,
|
||||
deleteInviteTokenOper,
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Get
|
||||
*/
|
||||
invited: {
|
||||
validation: [
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { token } = req.params;
|
||||
const inviteToken = await Invite.query()
|
||||
.where('token', token).first();
|
||||
|
||||
if (!inviteToken) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query()
|
||||
.where('id', inviteToken.tenantId).first();
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
|
||||
const organizationOptions = await Option.bindKnex(tenantDb).query()
|
||||
.where('key', 'organization_name');
|
||||
|
||||
return res.status(200).send({
|
||||
data: {
|
||||
organization_name: organizationOptions.getMeta('organization_name', '') ,
|
||||
invited_email: inviteToken.email,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
140
server/src/http/controllers/InviteUsers.ts
Normal file
140
server/src/http/controllers/InviteUsers.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import {
|
||||
check,
|
||||
body,
|
||||
param,
|
||||
matchedData,
|
||||
} from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import InviteUserService from '@/services/InviteUsers';
|
||||
import { ServiceErrors, ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersController {
|
||||
@Inject()
|
||||
inviteUsersService: InviteUserService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/send', jwtAuth);
|
||||
router.use('/send', TenancyMiddleware);
|
||||
|
||||
router.post('/send', [
|
||||
body('email').exists().trim().escape(),
|
||||
],
|
||||
asyncMiddleware(this.sendInvite),
|
||||
);
|
||||
|
||||
router.post('/accept/:token', [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
asyncMiddleware(this.accept)
|
||||
);
|
||||
|
||||
router.get('/invited/:token', [
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
asyncMiddleware(this.invited)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async sendInvite(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body;
|
||||
const { tenantId } = req;
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.sendInvite(tenantId, email, user);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'email_already_invited') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.ALREADY.INVITED' }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the inviation.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async accept(req: Request, res: Response, next: Function) {
|
||||
const inviteUserInput: IInviteUserInput = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.acceptInvite(token, inviteUserInput);
|
||||
return res.status(200).send();
|
||||
} catch (error) {
|
||||
|
||||
if (error instanceof ServiceErrors) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (error.hasType('email_exists')) {
|
||||
errorReasons.push({ type: 'EMAIL.EXISTS' });
|
||||
}
|
||||
if (error.hasType('phone_number_exists')) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER.EXISTS' });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the invite token is valid.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async invited(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.checkInvite(token);
|
||||
|
||||
return res.status(200).send();
|
||||
} catch (error) {
|
||||
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'invite_token_invalid') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.INVALID' }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import express from 'express';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
@@ -17,22 +17,21 @@ import {
|
||||
mapFilterRolesToDynamicFilter,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
|
||||
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
|
||||
@Service()
|
||||
export default class ItemsCategoriesController {
|
||||
export default class ItemsCategoriesController extends BaseController {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
const router = express.Router();
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/:id', [
|
||||
...this.categoryValidationSchema,
|
||||
...this.specificCategoryValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateParentCategoryExistance),
|
||||
asyncMiddleware(this.validateSellAccountExistance),
|
||||
asyncMiddleware(this.validateCostAccountExistance),
|
||||
@@ -42,7 +41,6 @@ export default class ItemsCategoriesController {
|
||||
router.post('/',
|
||||
this.categoryValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateParentCategoryExistance),
|
||||
asyncMiddleware(this.validateSellAccountExistance),
|
||||
asyncMiddleware(this.validateCostAccountExistance),
|
||||
@@ -52,28 +50,24 @@ export default class ItemsCategoriesController {
|
||||
router.delete('/bulk',
|
||||
this.categoriesBulkValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateCategoriesIdsExistance),
|
||||
asyncMiddleware(this.bulkDeleteCategories),
|
||||
);
|
||||
router.delete('/:id',
|
||||
this.specificCategoryValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateItemCategoryExistance),
|
||||
asyncMiddleware(this.deleteItem),
|
||||
);
|
||||
router.get('/:id',
|
||||
this.specificCategoryValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateItemCategoryExistance),
|
||||
asyncMiddleware(this.getCategory)
|
||||
);
|
||||
router.get('/',
|
||||
this.categoriesListValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.getList)
|
||||
);
|
||||
return router;
|
||||
@@ -164,7 +158,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { Account, AccountType } = req.models;
|
||||
const category: IItemCategoryOTD = { ...req.body };
|
||||
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.costAccountId) {
|
||||
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
|
||||
@@ -191,7 +185,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async validateSellAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { Account, AccountType } = req.models;
|
||||
const category: IItemCategoryOTD = { ...req.body };
|
||||
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.sellAccountId) {
|
||||
const incomeType = await AccountType.query().findOne('key', 'income');
|
||||
@@ -218,7 +212,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { Account, AccountType } = req.models;
|
||||
const category: IItemCategoryOTD = { ...req.body };
|
||||
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.inventoryAccountId) {
|
||||
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
|
||||
@@ -244,7 +238,7 @@ export default class ItemsCategoriesController {
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
|
||||
const category: IItemCategory = { ...req.body };
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
if (category.parentCategoryId) {
|
||||
@@ -290,7 +284,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async newCategory(req: Request, res: Response) {
|
||||
const { user } = req;
|
||||
const category: IItemCategory = { ...req.body };
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
const storedCategory = await ItemCategory.query().insert({
|
||||
@@ -308,7 +302,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async editCategory(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
const category: IItemCategory = { ...req.body };
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
const updateItemCategory = await ItemCategory.query()
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import Container from 'typedi';
|
||||
import fs from 'fs';
|
||||
import { difference } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Logger from '@/services/Logger';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
@@ -70,6 +70,8 @@ export default {
|
||||
// check('attachment').exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
if (!req.files.attachment) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
|
||||
@@ -93,9 +95,9 @@ export default {
|
||||
|
||||
try {
|
||||
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
|
||||
Logger.log('info', 'Attachment uploaded successfully');
|
||||
Logger.info('[attachment] uploaded successfully');
|
||||
} catch (error) {
|
||||
Logger.log('info', 'Attachment uploading failed.', { error });
|
||||
Logger.info('[attachment] uploading failed.', { error });
|
||||
}
|
||||
|
||||
const media = await Media.query().insert({
|
||||
@@ -114,6 +116,7 @@ export default {
|
||||
query('ids.*').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const Logger = Container.get('logger');
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
@@ -142,12 +145,12 @@ export default {
|
||||
});
|
||||
await Promise.all(unlinkOpers).then((resolved) => {
|
||||
resolved.forEach(() => {
|
||||
Logger.log('error', 'Attachment file has been deleted.');
|
||||
Logger.info('[attachment] file has been deleted.');
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
errors.forEach((error) => {
|
||||
Logger.log('error', 'Delete item attachment file delete failed.', { error });
|
||||
Logger.info('[attachment] Delete item attachment file delete failed.', { error });
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import express from 'express';
|
||||
import { body, query, validationResult } from 'express-validator';
|
||||
import { pick } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.saveOptions.validation,
|
||||
asyncMiddleware(this.saveOptions.handler));
|
||||
|
||||
router.get('/',
|
||||
this.getOptions.validation,
|
||||
asyncMiddleware(this.getOptions.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the given options to the storage.
|
||||
*/
|
||||
saveOptions: {
|
||||
validation: [
|
||||
body('options').isArray({ min: 1 }),
|
||||
body('options.*.key').exists(),
|
||||
body('options.*.value').exists(),
|
||||
body('options.*.group').exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Option } = req.models;
|
||||
const form = { ...req.body };
|
||||
const optionsCollections = await Option.query();
|
||||
|
||||
const errorReasons = [];
|
||||
const notDefinedOptions = Option.validateDefined(form.options);
|
||||
|
||||
if (notDefinedOptions.length) {
|
||||
errorReasons.push({
|
||||
type: 'OPTIONS.KEY.NOT.DEFINED',
|
||||
code: 200,
|
||||
keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })),
|
||||
});
|
||||
}
|
||||
if (errorReasons.length) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
form.options.forEach((option) => {
|
||||
optionsCollections.setMeta({ ...option });
|
||||
});
|
||||
await optionsCollections.saveMeta();
|
||||
|
||||
return res.status(200).send({ options: form });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
getOptions: {
|
||||
validation: [
|
||||
query('key').optional(),
|
||||
query('group').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Option } = req.models;
|
||||
const filter = { ...req.query };
|
||||
const options = await Option.query().onBuild((builder) => {
|
||||
if (filter.key) {
|
||||
builder.where('key', filter.key);
|
||||
}
|
||||
if (filter.group) {
|
||||
builder.where('group', filter.group);
|
||||
}
|
||||
});
|
||||
return res.status(200).send({ options: options.metadata });
|
||||
},
|
||||
},
|
||||
};
|
||||
65
server/src/http/controllers/Organization.ts
Normal file
65
server/src/http/controllers/Organization.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { check, matchedData } from 'express-validator';
|
||||
import { mapKeys, camelCase } from 'lodash';
|
||||
import asyncMiddleware from "@/http/middleware/asyncMiddleware";
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import OrganizationService from '@/services/Organization';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/build', [
|
||||
check('organization_id').exists().trim().escape(),
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.build.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds tenant database and seed initial data.
|
||||
* @param {Request} req - Express request.
|
||||
* @param {Response} res - Express response.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async build(req: Request, res: Response, next: Function) {
|
||||
const buildOTD = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
|
||||
try {
|
||||
await this.organizationService.build(buildOTD.organizationId);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'ORGANIZATION.DATABASE.INITIALIZED',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'tenant_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'tenant_initialized') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.DATABASE.ALREADY.BUILT', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Role from '@/models/Role';
|
||||
import Permission from '@/models/Permission';
|
||||
import Resource from '@/models/Resource';
|
||||
import knex from '@/database/knex';
|
||||
|
||||
const AccessControllSchema = [
|
||||
{
|
||||
resource: 'items',
|
||||
label: 'products_services',
|
||||
permissions: ['create', 'edit', 'delete', 'view'],
|
||||
fullAccess: true,
|
||||
ownControl: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getResourceSchema = (resource) => AccessControllSchema
|
||||
.find((schema) => schema.resource === resource);
|
||||
|
||||
const getResourcePermissions = (resource) => {
|
||||
const foundResource = getResourceSchema(resource);
|
||||
return foundResource ? foundResource.permissions : [];
|
||||
};
|
||||
|
||||
const findNotFoundResources = (resourcesSlugs) => {
|
||||
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
|
||||
return difference(resourcesSlugs, schemaResourcesSlugs);
|
||||
};
|
||||
|
||||
const findNotFoundPermissions = (permissions, resourceSlug) => {
|
||||
const schemaPermissions = getResourcePermissions(resourceSlug);
|
||||
return difference(permissions, schemaPermissions);
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.newRole.validation,
|
||||
asyncMiddleware(this.newRole.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.editRole.validation,
|
||||
asyncMiddleware(this.editRole.handler.bind(this)));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteRole.validation,
|
||||
asyncMiddleware(this.deleteRole.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new role.
|
||||
*/
|
||||
newRole: {
|
||||
validation: [
|
||||
check('name').exists().trim().escape(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('permissions').isArray({ min: 0 }),
|
||||
check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'),
|
||||
check('permissions.*.permissions').isArray({ min: 1 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { name, description, permissions } = req.body;
|
||||
|
||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||
const permissionsSlugs = [];
|
||||
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||
|
||||
const errorReasons = [];
|
||||
const notFoundPermissions = [];
|
||||
|
||||
if (resourcesNotFound.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound,
|
||||
});
|
||||
}
|
||||
permissions.forEach((perm) => {
|
||||
const abilities = perm.permissions.map((ability) => ability);
|
||||
|
||||
// Gets the not found permissions in the schema.
|
||||
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
if (notFoundAbilities.length > 0) {
|
||||
notFoundPermissions.push({
|
||||
resource_slug: perm.resource_slug,
|
||||
permissions: notFoundAbilities,
|
||||
});
|
||||
} else {
|
||||
const perms = perm.permissions || [];
|
||||
perms.forEach((permission) => {
|
||||
if (perms.indexOf(permission) !== -1) {
|
||||
permissionsSlugs.push(permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (notFoundPermissions.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PERMISSIONS_SLUG_NOT_FOUND',
|
||||
code: 200,
|
||||
permissions: notFoundPermissions,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
// Permissions.
|
||||
const [resourcesCollection, permsCollection] = await Promise.all([
|
||||
Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const notStoredResources = difference(
|
||||
resourcesSlugs, resourcesCollection.map((s) => s.name),
|
||||
);
|
||||
const notStoredPermissions = difference(
|
||||
permissionsSlugs, permsCollection.map((perm) => perm.slug),
|
||||
);
|
||||
|
||||
const insertThread = [];
|
||||
|
||||
if (notStoredResources.length > 0) {
|
||||
insertThread.push(knex('resources').insert([
|
||||
...notStoredResources.map((resource) => ({ name: resource })),
|
||||
]));
|
||||
}
|
||||
if (notStoredPermissions.length > 0) {
|
||||
insertThread.push(knex('permissions').insert([
|
||||
...notStoredPermissions.map((permission) => ({ name: permission })),
|
||||
]));
|
||||
}
|
||||
|
||||
await Promise.all(insertThread);
|
||||
|
||||
const [storedPermissions, storedResources] = await Promise.all([
|
||||
Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const storedResourcesSet = new Map(storedResources.map((resource) => [
|
||||
resource.attributes.name, resource.attributes.id,
|
||||
]));
|
||||
const storedPermissionsSet = new Map(storedPermissions.map((perm) => [
|
||||
perm.attributes.name, perm.attributes.id,
|
||||
]));
|
||||
const role = Role.forge({ name, description });
|
||||
|
||||
await role.save();
|
||||
|
||||
const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({
|
||||
role_id: role.id,
|
||||
resource_id: storedResourcesSet.get(resource.resource_slug),
|
||||
permission_id: storedPermissionsSet.get(perm),
|
||||
})));
|
||||
|
||||
if (roleHasPerms.length > 0) {
|
||||
await knex('role_has_permissions').insert(roleHasPerms[0]);
|
||||
}
|
||||
return res.status(200).send({ id: role.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the give role.
|
||||
*/
|
||||
editRole: {
|
||||
validation: [
|
||||
check('name').exists().trim().escape(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('permissions').isArray({ min: 0 }),
|
||||
check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'),
|
||||
check('permissions.*.permissions').isArray({ min: 1 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const role = await Role.where('id', id).fetch();
|
||||
|
||||
if (!role) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
const { permissions } = req.body;
|
||||
const errorReasons = [];
|
||||
const permissionsSlugs = [];
|
||||
const notFoundPermissions = [];
|
||||
|
||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||
|
||||
if (resourcesNotFound.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RESOURCE_SLUG_NOT_FOUND',
|
||||
code: 100,
|
||||
resources: resourcesNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
permissions.forEach((perm) => {
|
||||
const abilities = perm.permissions.map((ability) => ability);
|
||||
// Gets the not found permissions in the schema.
|
||||
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
if (notFoundAbilities.length > 0) {
|
||||
notFoundPermissions.push({
|
||||
resource_slug: perm.resource_slug, permissions: notFoundAbilities,
|
||||
});
|
||||
} else {
|
||||
const perms = perm.permissions || [];
|
||||
perms.forEach((permission) => {
|
||||
if (perms.indexOf(permission) !== -1) {
|
||||
permissionsSlugs.push(permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (notFoundPermissions.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PERMISSIONS_SLUG_NOT_FOUND',
|
||||
code: 200,
|
||||
permissions: notFoundPermissions,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
// Permissions.
|
||||
const [resourcesCollection, permsCollection] = await Promise.all([
|
||||
Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const notStoredResources = difference(
|
||||
resourcesSlugs, resourcesCollection.map((s) => s.name),
|
||||
);
|
||||
const notStoredPermissions = difference(
|
||||
permissionsSlugs, permsCollection.map((perm) => perm.slug),
|
||||
);
|
||||
const insertThread = [];
|
||||
|
||||
if (notStoredResources.length > 0) {
|
||||
insertThread.push(knex('resources').insert([
|
||||
...notStoredResources.map((resource) => ({ name: resource })),
|
||||
]));
|
||||
}
|
||||
if (notStoredPermissions.length > 0) {
|
||||
insertThread.push(knex('permissions').insert([
|
||||
...notStoredPermissions.map((permission) => ({ name: permission })),
|
||||
]));
|
||||
}
|
||||
|
||||
await Promise.all(insertThread);
|
||||
|
||||
const [storedPermissions, storedResources] = await Promise.all([
|
||||
Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const storedResourcesSet = new Map(storedResources.map((resource) => [
|
||||
resource.attributes.name, resource.attributes.id,
|
||||
]));
|
||||
const storedPermissionsSet = new Map(storedPermissions.map((perm) => [
|
||||
perm.attributes.name, perm.attributes.id,
|
||||
]));
|
||||
|
||||
await role.save();
|
||||
|
||||
|
||||
const savedRoleHasPerms = await knex('role_has_permissions').where({
|
||||
role_id: role.id,
|
||||
});
|
||||
|
||||
console.log(savedRoleHasPerms);
|
||||
|
||||
// const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({
|
||||
// role_id: role.id,
|
||||
// resource_id: storedResourcesSet.get(resource.resource_slug),
|
||||
// permission_id: storedPermissionsSet.get(perm),
|
||||
// })));
|
||||
|
||||
// if (roleHasPerms.length > 0) {
|
||||
// await knex('role_has_permissions').insert(roleHasPerms[0]);
|
||||
// }
|
||||
return res.status(200).send({ id: role.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
deleteRole: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const role = await Role.where('id', id).fetch();
|
||||
|
||||
if (!role) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
if (role.attributes.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ROLE_PREDEFINED', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
await knex('role_has_permissions')
|
||||
.where('role_id', role.id).delete({ require: false });
|
||||
|
||||
await role.destroy();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
getRole: {
|
||||
validation: [],
|
||||
handler(req, res) {
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
};
|
||||
89
server/src/http/controllers/Settings.ts
Normal file
89
server/src/http/controllers/Settings.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { body, query, validationResult } from 'express-validator';
|
||||
import { pick } from 'lodash';
|
||||
import { IOptionDTO, IOptionsDTO } from '@/interfaces';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default class SettingsController extends BaseController{
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/',
|
||||
this.saveSettingsValidationSchema,
|
||||
asyncMiddleware(this.saveSettings.bind(this)));
|
||||
|
||||
router.get('/',
|
||||
this.getSettingsSchema,
|
||||
asyncMiddleware(this.getSettings.bind(this)));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings validation schema.
|
||||
*/
|
||||
get saveSettingsValidationSchema() {
|
||||
return [
|
||||
body('options').isArray({ min: 1 }),
|
||||
body('options.*.key').exists(),
|
||||
body('options.*.value').exists(),
|
||||
body('options.*.group').exists(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
get getSettingsSchema() {
|
||||
return [
|
||||
query('key').optional(),
|
||||
query('group').optional(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given options to the storage.
|
||||
*/
|
||||
saveSettings(req: Request, res: Response) {
|
||||
const { Option } = req.models;
|
||||
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
|
||||
const { settings } = req;
|
||||
|
||||
const errorReasons: { type: string, code: number, keys: [] }[] = [];
|
||||
const notDefinedOptions = Option.validateDefined(optionsDTO.options);
|
||||
|
||||
if (notDefinedOptions.length) {
|
||||
errorReasons.push({
|
||||
type: 'OPTIONS.KEY.NOT.DEFINED',
|
||||
code: 200,
|
||||
keys: notDefinedOptions.map((o) => ({
|
||||
...pick(o, ['key', 'group'])
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (errorReasons.length) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
optionsDTO.options.forEach((option: IOptionDTO) => {
|
||||
settings.set({ ...option });
|
||||
});
|
||||
|
||||
return res.status(200).send({ });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve settings.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
getSettings(req: Request, res: Response) {
|
||||
const { settings } = req;
|
||||
const allSettings = settings.all();
|
||||
|
||||
return res.status(200).send({ settings: allSettings });
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { Plan } from '@/system/models';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||
|
||||
export default class PaymentMethodController {
|
||||
export default class PaymentMethodController extends BaseController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
@@ -16,7 +17,7 @@ export default class PaymentMethodController {
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
|
||||
const { planSlug } = req.body;
|
||||
const { planSlug } = this.matchedBodyData(req);
|
||||
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
||||
|
||||
if (!foundPlan) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { check, param, query, ValidationSchema } from 'express-validator';
|
||||
import { Voucher, Plan } from '@/system/models';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
|
||||
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
|
||||
import {
|
||||
NotAllowedChangeSubscriptionPlan
|
||||
} from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class PaymentViaVoucherController extends PaymentMethodController {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -22,7 +24,6 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
'/payment',
|
||||
this.paymentViaVoucherSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
|
||||
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
||||
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
|
||||
@@ -48,7 +49,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
* @param {Response} res
|
||||
*/
|
||||
async validateVoucherCodeExistance(req: Request, res: Response, next: Function) {
|
||||
const { voucherCode } = req.body;
|
||||
const { voucherCode } = this.matchedBodyData(req);
|
||||
this.logger.info('[voucher_payment] trying to validate voucher code.', { voucherCode });
|
||||
|
||||
const foundVoucher = await Voucher.query()
|
||||
.modify('filterActiveVoucher')
|
||||
@@ -70,7 +72,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateVoucherAndPlan(req: Request, res: Response, next: Function) {
|
||||
const { planSlug, voucherCode } = req.body;
|
||||
const { planSlug, voucherCode } = this.matchedBodyData(req);
|
||||
this.logger.info('[voucher_payment] trying to validate voucher with the plan.', { voucherCode });
|
||||
|
||||
const voucher = await Voucher.query().findOne('voucher_code', voucherCode);
|
||||
const plan = await Plan.query().findOne('slug', planSlug);
|
||||
@@ -90,11 +93,12 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
* @return {Response}
|
||||
*/
|
||||
async paymentViaVoucher(req: Request, res: Response, next: Function) {
|
||||
const { planSlug, voucherCode } = req.body;
|
||||
const { planSlug, voucherCode } = this.matchedBodyData(req);
|
||||
const { tenant } = req;
|
||||
|
||||
try {
|
||||
await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
|
||||
await this.subscriptionService
|
||||
.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'PAYMENT.SUCCESSFULLY.MADE',
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import { repeat, times, orderBy } from 'lodash';
|
||||
import { check, oneOf, param, query, ValidationChain } from 'express-validator';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import { check, oneOf, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Voucher, Plan } from '@/system/models';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import VoucherService from '@/services/Payment/Voucher';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import PrettierMiddleware from '@/http/middleware/prettierMiddleware';
|
||||
import { IVouchersFilter } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class VouchersController {
|
||||
export default class VouchersController extends BaseController {
|
||||
@Inject()
|
||||
voucherService: VoucherService;
|
||||
|
||||
@@ -24,14 +23,12 @@ export default class VouchersController {
|
||||
'/generate',
|
||||
this.generateVoucherSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validatePlanExistance.bind(this)),
|
||||
asyncMiddleware(this.generateVoucher.bind(this)),
|
||||
);
|
||||
router.post(
|
||||
'/disable/:voucherId',
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
||||
asyncMiddleware(this.validateNotDisabledVoucher.bind(this)),
|
||||
asyncMiddleware(this.disableVoucher.bind(this)),
|
||||
@@ -40,18 +37,15 @@ export default class VouchersController {
|
||||
'/send',
|
||||
this.sendVoucherSchemaValidation,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.sendVoucher.bind(this)),
|
||||
);
|
||||
router.delete(
|
||||
'/:voucherId',
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
||||
asyncMiddleware(this.deleteVoucher.bind(this)),
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.listVouchers.bind(this)),
|
||||
);
|
||||
return router;
|
||||
@@ -106,7 +100,8 @@ export default class VouchersController {
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePlanExistance(req: Request, res: Response, next: Function) {
|
||||
const planId: number = req.body.planId || req.params.planId;
|
||||
const body = this.matchedBodyData(req);
|
||||
const planId: number = body.planId || req.params.planId;
|
||||
const foundPlan = await Plan.query().findById(planId);
|
||||
|
||||
if (!foundPlan) {
|
||||
@@ -124,7 +119,9 @@ export default class VouchersController {
|
||||
* @param {Function}
|
||||
*/
|
||||
async validateVoucherExistance(req: Request, res: Response, next: Function) {
|
||||
const voucherId = req.body.voucherId || req.params.voucherId;
|
||||
const body = this.matchedBodyData(req);
|
||||
|
||||
const voucherId = body.voucherId || req.params.voucherId;
|
||||
const foundVoucher = await Voucher.query().findById(voucherId);
|
||||
|
||||
if (!foundVoucher) {
|
||||
@@ -160,7 +157,7 @@ export default class VouchersController {
|
||||
* @return {Response}
|
||||
*/
|
||||
async generateVoucher(req: Request, res: Response, next: Function) {
|
||||
const { loop = 10, period, periodInterval, planId } = req.body;
|
||||
const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.voucherService.generateVouchers(
|
||||
@@ -211,7 +208,7 @@ export default class VouchersController {
|
||||
* @return {Response}
|
||||
*/
|
||||
async sendVoucher(req: Request, res: Response) {
|
||||
const { phoneNumber, email, period, periodInterval, planId } = req.body;
|
||||
const { phoneNumber, email, period, periodInterval, planId } = this.matchedBodyData(req);
|
||||
|
||||
const voucher = await Voucher.query()
|
||||
.modify('filterActiveVoucher')
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
||||
import { Container, Service } from 'typedi';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
|
||||
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
|
||||
|
||||
@Service()
|
||||
@@ -13,6 +14,7 @@ export default class SubscriptionController {
|
||||
const router = Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/voucher', Container.get(PaymentViaVoucherController).router());
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import express from 'express';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
// Middlewares
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
|
||||
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import EnsureTenantIsInitialized from '@/http/middleware/EnsureTenantIsInitialized';
|
||||
import SettingsMiddleware from '@/http/middleware/SettingsMiddleware';
|
||||
|
||||
// Routes
|
||||
import Authentication from '@/http/controllers/Authentication';
|
||||
import InviteUsers from '@/http/controllers/InviteUsers';
|
||||
import Organization from '@/http/controllers/Organization';
|
||||
import Users from '@/http/controllers/Users';
|
||||
import Items from '@/http/controllers/Items';
|
||||
import ItemCategories from '@/http/controllers/ItemCategories';
|
||||
@@ -11,7 +22,7 @@ import Views from '@/http/controllers/Views';
|
||||
import Accounting from '@/http/controllers/Accounting';
|
||||
import FinancialStatements from '@/http/controllers/FinancialStatements';
|
||||
import Expenses from '@/http/controllers/Expenses';
|
||||
import Options from '@/http/controllers/Options';
|
||||
import Settings from '@/http/controllers/Settings';
|
||||
import Currencies from '@/http/controllers/Currencies';
|
||||
import Customers from '@/http/controllers/Customers';
|
||||
import Vendors from '@/http/controllers/Vendors';
|
||||
@@ -20,19 +31,16 @@ import Purchases from '@/http/controllers/Purchases';
|
||||
import Resources from './controllers/Resources';
|
||||
import ExchangeRates from '@/http/controllers/ExchangeRates';
|
||||
import Media from '@/http/controllers/Media';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import Ping from '@/http/controllers/Ping';
|
||||
import Agendash from '@/http/controllers/Agendash';
|
||||
import Subscription from '@/http/controllers/Subscription';
|
||||
import VouchersController from '@/http/controllers/Subscription/Vouchers';
|
||||
|
||||
import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjection';
|
||||
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
|
||||
|
||||
export default (app) => {
|
||||
app.use('/api/auth', Container.get(Authentication).router());
|
||||
app.use('/api/invite', InviteUsers.router());
|
||||
app.use('/api/invite', Container.get(InviteUsers).router());
|
||||
app.use('/api/organization', Container.get(Organization).router());
|
||||
app.use('/api/vouchers', Container.get(VouchersController).router());
|
||||
app.use('/api/subscription', Container.get(Subscription).router());
|
||||
app.use('/api/ping', Container.get(Ping).router());
|
||||
@@ -40,20 +48,23 @@ export default (app) => {
|
||||
const dashboard = express.Router();
|
||||
|
||||
dashboard.use(JWTAuth);
|
||||
dashboard.use(AttachCurrentTenantUser)
|
||||
dashboard.use(TenancyMiddleware);
|
||||
dashboard.use(SubscriptionMiddleware('main'));
|
||||
|
||||
dashboard.use('/api/currencies', Currencies.router());
|
||||
dashboard.use(EnsureTenantIsInitialized);
|
||||
dashboard.use(SettingsMiddleware);
|
||||
|
||||
dashboard.use('/api/users', Users.router());
|
||||
dashboard.use('/api/currencies', Currencies.router());
|
||||
dashboard.use('/api/accounts', Accounts.router());
|
||||
dashboard.use('/api/account_types', AccountTypes.router());
|
||||
dashboard.use('/api/accounting', Accounting.router());
|
||||
dashboard.use('/api/views', Views.router());
|
||||
dashboard.use('/api/items', Container.get(Items).router());
|
||||
dashboard.use('/api/item_categories', Container.get(ItemCategories));
|
||||
dashboard.use('/api/item_categories', Container.get(ItemCategories).router());
|
||||
dashboard.use('/api/expenses', Expenses.router());
|
||||
dashboard.use('/api/financial_statements', FinancialStatements.router());
|
||||
dashboard.use('/api/options', Options.router());
|
||||
dashboard.use('/api/settings', Container.get(Settings).router());
|
||||
dashboard.use('/api/sales', Sales.router());
|
||||
dashboard.use('/api/customers', Customers.router());
|
||||
dashboard.use('/api/vendors', Vendors.router());
|
||||
|
||||
29
server/src/http/middleware/AttachCurrentTenantUser.ts
Normal file
29
server/src/http/middleware/AttachCurrentTenantUser.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Container } from 'typedi';
|
||||
import { SystemUser } from '@/system/models';
|
||||
|
||||
/**
|
||||
* Attach user to req.currentUser
|
||||
* @param {Request} req Express req Object
|
||||
* @param {Response} res Express res Object
|
||||
* @param {NextFunction} next Express next Function
|
||||
*/
|
||||
const attachCurrentUser = async (req: Request, res: Response, next: Function) => {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
try {
|
||||
Logger.info('[attach_user_middleware] finding system user by id.');
|
||||
const user = await SystemUser.query().findById(req.token.id);
|
||||
|
||||
if (!user) {
|
||||
Logger.info('[attach_user_middleware] the system user not found.');
|
||||
return res.boom.unauthorized();
|
||||
}
|
||||
req.user = user;
|
||||
return next();
|
||||
} catch (e) {
|
||||
Logger.error('[attach_user_middleware] error attaching user to req: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
};
|
||||
|
||||
export default attachCurrentUser;
|
||||
17
server/src/http/middleware/EnsureTenantIsInitialized.ts
Normal file
17
server/src/http/middleware/EnsureTenantIsInitialized.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Container } from 'typedi';
|
||||
|
||||
export default (req: Request, res: Response, next: Function) => {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
if (!req.tenant) {
|
||||
Logger.info('[ensure_tenant_intialized_middleware] no tenant model.');
|
||||
throw new Error('Should load this middleware after `TenancyMiddleware`.');
|
||||
}
|
||||
if (!req.tenant.initialized) {
|
||||
Logger.info('[ensure_tenant_intialized_middleware] tenant database not initalized.');
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
8
server/src/http/middleware/LoggerMiddleware.ts
Normal file
8
server/src/http/middleware/LoggerMiddleware.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NextFunction, Request } from 'express';
|
||||
|
||||
function loggerMiddleware(request: Request, response: Response, next: NextFunction) {
|
||||
console.log(`${request.method} ${request.path}`);
|
||||
next();
|
||||
}
|
||||
|
||||
export default loggerMiddleware;
|
||||
29
server/src/http/middleware/SettingsMiddleware.ts
Normal file
29
server/src/http/middleware/SettingsMiddleware.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import SettingsStore from '@/services/Settings/SettingsStore';
|
||||
|
||||
export default async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { tenantId } = req.user;
|
||||
const { knex } = req;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
const tenantContainer = Container.of(`tenant-${tenantId}`);
|
||||
|
||||
if (tenantContainer && !tenantContainer.has('settings')) {
|
||||
Logger.info('[settings_middleware] initialize settings store.');
|
||||
const settings = new SettingsStore(knex);
|
||||
|
||||
Logger.info('[settings_middleware] load settings from storage or cache.');
|
||||
await settings.load();
|
||||
|
||||
tenantContainer.set('settings', settings);
|
||||
}
|
||||
Logger.info('[settings_middleware] get settings instance from container.');
|
||||
const settings = tenantContainer.get('settings');
|
||||
req.settings = settings;
|
||||
|
||||
res.on('finish', async () => {
|
||||
await settings.save();
|
||||
});
|
||||
next();
|
||||
}
|
||||
@@ -1,34 +1,29 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
function loadModelsFromDirectory() {
|
||||
const models = {};
|
||||
fs.readdirSync('src/models/').forEach((filename) => {
|
||||
const model = {
|
||||
path: path.join(__dirname, 'src/models/', filename),
|
||||
name: filename.replace(/\.[^/.]+$/, ''),
|
||||
};
|
||||
// eslint-disable-next-line global-require
|
||||
model.resource = require(`@/models/${model.name}`);
|
||||
models[model.name] = model;
|
||||
});
|
||||
return models;
|
||||
}
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import tenantModelsLoader from '@/loaders/tenantModels';
|
||||
|
||||
export default async (req, res, next) => {
|
||||
const Logger = Container.get('logger');
|
||||
const organizationId = req.headers['organization-id'] || req.query.organization;
|
||||
const notFoundOrganization = () => res.boom.unauthorized(
|
||||
'Organization identication not found.',
|
||||
{ errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }] },
|
||||
);
|
||||
|
||||
const notFoundOrganization = () => {
|
||||
Logger.info('[tenancy_middleware] organization id not found.');
|
||||
return res.boom.unauthorized(
|
||||
'Organization identication not found.',
|
||||
{ errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }] },
|
||||
);
|
||||
}
|
||||
// In case the given organization not found.
|
||||
if (!organizationId) {
|
||||
return notFoundOrganization();
|
||||
}
|
||||
const tenant = await TenantsManager.getTenant(organizationId);
|
||||
const tenantsManager = Container.get(TenantsManager);
|
||||
|
||||
Logger.info('[tenancy_middleware] trying get tenant by org. id from storage.');
|
||||
const tenant = await tenantsManager.getTenant(organizationId);
|
||||
|
||||
Logger.info('[tenancy_middleware] initializing tenant knex instance.');
|
||||
const tenantKnex = tenantsManager.knexInstance(organizationId);
|
||||
|
||||
// When the given organization id not found on the system storage.
|
||||
if (!tenant) {
|
||||
@@ -36,30 +31,22 @@ export default async (req, res, next) => {
|
||||
}
|
||||
// When user tenant not match the given organization id.
|
||||
if (tenant.id !== req.user.tenantId) {
|
||||
Logger.info('[tenancy_middleware] authorized user not match org. tenant.');
|
||||
return res.boom.unauthorized();
|
||||
}
|
||||
const knex = TenantsManager.knexInstance(organizationId);
|
||||
const models = loadModelsFromDirectory();
|
||||
|
||||
TenantModel.knexBinded = knex;
|
||||
|
||||
req.knex = knex;
|
||||
const models = tenantModelsLoader(tenantKnex);
|
||||
|
||||
req.knex = tenantKnex;
|
||||
req.organizationId = organizationId;
|
||||
req.tenant = tenant;
|
||||
req.tenantId = tenant.id;
|
||||
req.models = {
|
||||
...Object.values(models).reduce((acc, model) => {
|
||||
if (typeof model.resource.default !== 'undefined' &&
|
||||
typeof model.resource.default.requestModel === 'function' &&
|
||||
model.resource.default.requestModel() &&
|
||||
model.name !== 'TenantModel') {
|
||||
acc[model.name] = model.resource.default.bindKnex(knex);
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
Container.of(`tenant-${tenant.id}`).set('models', {
|
||||
...req.models,
|
||||
});
|
||||
req.models = models;
|
||||
|
||||
const tenantContainer = Container.of(`tenant-${tenant.id}`);
|
||||
|
||||
tenantContainer.set('models', models);
|
||||
tenantContainer.set('knex', tenantKnex);
|
||||
tenantContainer.set('tenant', tenant);
|
||||
Logger.info('[tenancy_middleware] tenant dependencies injected to container.');
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
export default async (req: Request, res: Response, next: Function) => {
|
||||
const { organizationId, knex } = req;
|
||||
|
||||
if (!organizationId || !knex) {
|
||||
throw new Error('Should load `TenancyMiddleware` before this middleware.');
|
||||
}
|
||||
Container.of(`tenant-${organizationId}`).set('knex', knex);
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/* eslint-disable consistent-return */
|
||||
const authorization = (resourceName) => (...permissions) => (req, res, next) => {
|
||||
const { user } = req;
|
||||
const onError = () => {
|
||||
res.boom.unauthorized();
|
||||
};
|
||||
user.hasPermissions(resourceName, permissions)
|
||||
.then((authorized) => {
|
||||
if (!authorized) {
|
||||
return onError();
|
||||
}
|
||||
next();
|
||||
}).catch(onError);
|
||||
};
|
||||
|
||||
export default authorization;
|
||||
@@ -1,31 +1,31 @@
|
||||
/* eslint-disable consistent-return */
|
||||
import { Container } from 'typedi';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import config from '@/../config/config';
|
||||
|
||||
const authMiddleware = (req, res, next) => {
|
||||
const { JWT_SECRET_KEY } = process.env;
|
||||
const Logger = Container.get('logger');
|
||||
const token = req.headers['x-access-token'] || req.query.token;
|
||||
|
||||
const onError = () => { res.boom.unauthorized(); };
|
||||
|
||||
const onError = () => {
|
||||
Logger.info('[auth_middleware] jwt verify error.');
|
||||
res.boom.unauthorized();
|
||||
};
|
||||
const onSuccess = (decoded) => {
|
||||
req.token = decoded;
|
||||
Logger.info('[auth_middleware] jwt verify success.');
|
||||
next();
|
||||
};
|
||||
if (!token) { return onError(); }
|
||||
|
||||
const verify = new Promise((resolve, reject) => {
|
||||
jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => {
|
||||
jwt.verify(token, config.jwtSecret, async (error, decoded) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
req.user = await SystemUser.query().findById(decoded._id);
|
||||
|
||||
if (!req.user) {
|
||||
return onError();
|
||||
}
|
||||
resolve(decoded);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
verify.then(() => { next(); }).catch(onError);
|
||||
verify.then(onSuccess).catch(onError);
|
||||
};
|
||||
export default authMiddleware;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { camelCase, snakeCase, mapKeys } from 'lodash';
|
||||
|
||||
/**
|
||||
* create a middleware to change json format from snake case to camelcase in request
|
||||
* then change back to snake case in response
|
||||
*
|
||||
*/
|
||||
export default (req: Request, res: Response, next: Function) => {
|
||||
/**
|
||||
* camelize `req.body`
|
||||
*/
|
||||
if (req.body && typeof req.body === 'object') {
|
||||
req.body = mapKeys(req.body, (value: any, key: string) => camelCase(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* camelize `req.query`
|
||||
*/
|
||||
if (req.query && typeof req.query === 'object') {
|
||||
req.query = mapKeys(req.query, (value: any, key: string) => camelCase(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* wrap `res.json()`
|
||||
*/
|
||||
const sendJson = res.json;
|
||||
|
||||
res.json = (data: any) => {
|
||||
const mapped = mapKeys(data, (value: any, key: string) => snakeCase(key));
|
||||
return sendJson.call(res, mapped);
|
||||
};
|
||||
return next();
|
||||
};
|
||||
28
server/src/interfaces/Metable.ts
Normal file
28
server/src/interfaces/Metable.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
export interface IMetadata {
|
||||
key: string,
|
||||
value: string|boolean|number,
|
||||
group: string,
|
||||
_markAsDeleted?: boolean,
|
||||
_markAsInserted?: boolean,
|
||||
_markAsUpdated?: boolean,
|
||||
};
|
||||
|
||||
export interface IMetaQuery {
|
||||
key: string,
|
||||
group: string,
|
||||
};
|
||||
|
||||
export interface IMetableStore {
|
||||
find(query: string|IMetaQuery): IMetadata;
|
||||
all(): IMetadata[];
|
||||
get(query: string|IMetaQuery, defaultValue: any): string|number|boolean;
|
||||
remove(query: string|IMetaQuery): void;
|
||||
removeAll(): void;
|
||||
toArray(): IMetadata[];
|
||||
};
|
||||
|
||||
export interface IMetableStoreStorage {
|
||||
save(): Promise<void>;
|
||||
}
|
||||
11
server/src/interfaces/Options.ts
Normal file
11
server/src/interfaces/Options.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export interface IOptionDTO {
|
||||
key: string,
|
||||
value: string|number,
|
||||
group: string,
|
||||
};
|
||||
|
||||
export interface IOptionsDTO {
|
||||
options: IOptionDTO[],
|
||||
};
|
||||
@@ -6,4 +6,8 @@ export interface ISystemUser {
|
||||
|
||||
export interface ISystemUserDTO {
|
||||
|
||||
}
|
||||
|
||||
export interface IInviteUserInput {
|
||||
|
||||
}
|
||||
@@ -34,7 +34,18 @@ import {
|
||||
import {
|
||||
ISystemUser,
|
||||
ISystemUserDTO,
|
||||
IInviteUserInput,
|
||||
} from './User';
|
||||
import {
|
||||
IMetadata,
|
||||
IMetaQuery,
|
||||
IMetableStore,
|
||||
IMetableStoreStorage,
|
||||
} from './Metable';
|
||||
import {
|
||||
IOptionDTO,
|
||||
IOptionsDTO,
|
||||
} from './Options';
|
||||
|
||||
export {
|
||||
IBillPaymentEntry,
|
||||
@@ -69,4 +80,13 @@ export {
|
||||
IRegisterDTO,
|
||||
ISystemUser,
|
||||
ISystemUserDTO,
|
||||
IInviteUserInput,
|
||||
|
||||
IMetadata,
|
||||
IMetaQuery,
|
||||
IMetableStore,
|
||||
IMetableStoreStorage,
|
||||
|
||||
IOptionDTO,
|
||||
IOptionsDTO,
|
||||
};
|
||||
@@ -25,15 +25,15 @@ export default class ComputeItemCostJob {
|
||||
const Logger = Container.get('logger');
|
||||
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
|
||||
|
||||
Logger.debug(`Compute item cost - started: ${job.attrs.data}`);
|
||||
Logger.info(`Compute item cost - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
||||
Logger.debug(`Compute item cost - completed: ${job.attrs.data}`);
|
||||
Logger.info(`Compute item cost - completed: ${job.attrs.data}`);
|
||||
done();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`);
|
||||
Logger.info(`Compute item cost: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,8 @@ export default class ComputeItemCostJob {
|
||||
async onJobFinished() {
|
||||
const agenda = Container.get('agenda');
|
||||
const startingDate = this.startingDate;
|
||||
this.depends = Math.max(this.depends - 1, 0);
|
||||
|
||||
console.log(startingDate);
|
||||
this.depends = Math.max(this.depends - 1, 0);
|
||||
|
||||
if (this.depends === 0) {
|
||||
this.startingDate = null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class MailNotificationSubscribeEnd {
|
||||
/**
|
||||
*
|
||||
* Job handler.
|
||||
* @param {Job} job -
|
||||
*/
|
||||
handler(job) {
|
||||
@@ -12,15 +12,15 @@ export default class MailNotificationSubscribeEnd {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,15 @@ export default class MailNotificationTrialEnd {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,28 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Mustache from 'mustache';
|
||||
import { Container } from 'typedi';
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
export default class ResetPasswordMailJob {
|
||||
/**
|
||||
*
|
||||
* @param job
|
||||
* @param done
|
||||
* Handle send welcome mail job.
|
||||
* @param {Job} job
|
||||
* @param {Function} done
|
||||
*/
|
||||
handler(job, done) {
|
||||
const { user, token } = job.attrs.data;
|
||||
|
||||
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');
|
||||
|
||||
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 });
|
||||
Logger.info(`Send reset password mail - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await this.authService.mailMessages.sendResetPasswordMessage();
|
||||
Logger.info(`Send reset password mail - finished: ${job.attrs.data}`);
|
||||
done()
|
||||
} catch (error) {
|
||||
Logger.info(`Send reset password mail - error: ${job.attrs.data}, error: ${error}`);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ export default class SMSNotificationSubscribeEnd {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send SMS notification subscription end soon - started: ${job.attrs.data}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ export default class SMSNotificationTrialEnd {
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.debug(`Send notification subscription end soon - started: ${job.attrs.data}`);
|
||||
Logger.info(`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}`);
|
||||
Logger.info(`Send notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.error(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
Logger.info(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import VoucherService from '@/services/Payment/Voucher';
|
||||
|
||||
export default class SendVoucherViaPhoneJob {
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { phoneNumber, voucherCode } = job.attrs.data;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
const voucherService = Container.get(VoucherService);
|
||||
const { phoneNumber, voucherCode } = job.attrs.data;
|
||||
|
||||
Logger.debug(`Send voucher via phone number - started: ${job.attrs.data}`);
|
||||
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
|
||||
import { Container, Inject } from 'typedi';
|
||||
import InviteUserService from '@/services/InviteUsers';
|
||||
|
||||
export default class UserInviteMailJob {
|
||||
@Inject()
|
||||
inviteUsersService: InviteUserService;
|
||||
|
||||
handler(job, done) {
|
||||
|
||||
/**
|
||||
* Handle invite user job.
|
||||
* @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');
|
||||
|
||||
Logger.info(`Send invite user mail - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.mailMessages.sendInviteMail();
|
||||
Logger.info(`Send invite user mail - finished: ${job.attrs.data}`);
|
||||
done()
|
||||
} catch (error) {
|
||||
Logger.info(`Send invite user mail - error: ${job.attrs.data}, error: ${error}`);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
server/src/jobs/WelcomeSMS.ts
Normal file
28
server/src/jobs/WelcomeSMS.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
|
||||
export default class WelcomeSMSJob {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
/**
|
||||
* Handle send welcome mail job.
|
||||
* @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');
|
||||
|
||||
Logger.info(`Send welcome SMS message - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await this.authService.smsMessages.sendWelcomeMessage();
|
||||
Logger.info(`Send welcome SMS message - finished: ${job.attrs.data}`);
|
||||
done()
|
||||
} catch (error) {
|
||||
Logger.info(`Send welcome SMS message - error: ${job.attrs.data}, error: ${error}`);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,28 @@
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import path from 'path';
|
||||
import { Container } from 'typedi';
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
/**
|
||||
*
|
||||
* Handle send welcome mail job.
|
||||
* @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');
|
||||
|
||||
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();
|
||||
});
|
||||
Logger.info(`Send welcome mail message - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await this.authService.mailMessages.sendWelcomeMessage();
|
||||
Logger.info(`Send welcome mail message - finished: ${job.attrs.data}`);
|
||||
done()
|
||||
} catch (error) {
|
||||
Logger.info(`Send welcome mail message - error: ${job.attrs.data}, error: ${error}`);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ export default class WriteInvoicesJournalEntries {
|
||||
const Logger = Container.get('logger');
|
||||
const { startingDate } = job.attrs.data;
|
||||
|
||||
Logger.debug(`Write sales invoices journal entries - started: ${job.attrs.data}`);
|
||||
Logger.info(`Write sales invoices journal entries - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await SalesInvoicesCost.writeJournalEntries(startingDate, true);
|
||||
Logger.debug(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
|
||||
Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
|
||||
done();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
Logger.error(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
|
||||
Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
|
||||
export default class MetableCollection {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
this.metadata = [];
|
||||
this.KEY_COLUMN = 'key';
|
||||
this.VALUE_COLUMN = 'value';
|
||||
this.TYPE_COLUMN = 'type';
|
||||
this.model = null;
|
||||
this.extraColumns = [];
|
||||
|
||||
this.extraQuery = (query, meta) => {
|
||||
query.where('key', meta[this.KEY_COLUMN]);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set model of this metadata collection.
|
||||
* @param {Object} model -
|
||||
*/
|
||||
setModel(model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a extra columns.
|
||||
* @param {Array} columns -
|
||||
*/
|
||||
setExtraColumns(columns) {
|
||||
this.extraColumns = columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the given metadata key.
|
||||
* @param {String} key -
|
||||
* @return {object} - Metadata object.
|
||||
*/
|
||||
findMeta(payload) {
|
||||
const { key, extraColumns } = this.parsePayload(payload);
|
||||
|
||||
return this.allMetadata().find((meta) => {
|
||||
const isSameKey = meta.key === key;
|
||||
const sameExtraColumns = this.extraColumns.some((extraColumn) => {
|
||||
return !extraColumns || (extraColumns[extraColumn] === meta[extraColumn]);
|
||||
});
|
||||
return isSameKey && sameExtraColumns;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all metadata.
|
||||
*/
|
||||
allMetadata() {
|
||||
return this.metadata.filter((meta) => !meta.markAsDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve metadata of the given key.
|
||||
* @param {String} key -
|
||||
* @param {Mixied} defaultValue -
|
||||
*/
|
||||
getMeta(payload, defaultValue) {
|
||||
const metadata = this.findMeta(payload);
|
||||
return metadata ? metadata.value : defaultValue || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markes the metadata to should be deleted.
|
||||
* @param {String} key -
|
||||
*/
|
||||
removeMeta(key) {
|
||||
const metadata = this.findMeta(key);
|
||||
|
||||
if (metadata) {
|
||||
metadata.markAsDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all meta data of the given group.
|
||||
* @param {*} group
|
||||
*/
|
||||
removeAllMeta(group = 'default') {
|
||||
this.metadata = this.metadata.map((meta) => ({
|
||||
...meta,
|
||||
markAsDeleted: true,
|
||||
}));
|
||||
}
|
||||
|
||||
setExtraQuery(callback) {
|
||||
this.extraQuery = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the meta data to the stack.
|
||||
* @param {String} key -
|
||||
* @param {String} value -
|
||||
*/
|
||||
setMeta(payload, ...args) {
|
||||
if (Array.isArray(key)) {
|
||||
const metadata = key;
|
||||
|
||||
metadata.forEach((meta) => {
|
||||
this.setMeta(meta.key, meta.value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { key, value, ...extraColumns } = this.parsePayload(payload, args[0]);
|
||||
const metadata = this.findMeta(payload);
|
||||
|
||||
if (metadata) {
|
||||
metadata.value = value;
|
||||
metadata.markAsUpdated = true;
|
||||
} else {
|
||||
this.metadata.push({
|
||||
value, key, ...extraColumns, markAsInserted: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parsePayload(payload, value) {
|
||||
return typeof payload !== 'object' ? { key: payload, value } : { ...payload };
|
||||
}
|
||||
|
||||
/**
|
||||
* Saved the modified/deleted and inserted metadata.
|
||||
*/
|
||||
async saveMeta() {
|
||||
const inserted = this.metadata.filter((m) => (m.markAsInserted === true));
|
||||
const updated = this.metadata.filter((m) => (m.markAsUpdated === true));
|
||||
const deleted = this.metadata.filter((m) => (m.markAsDeleted === true));
|
||||
const opers = [];
|
||||
|
||||
if (deleted.length > 0) {
|
||||
deleted.forEach((meta) => {
|
||||
const deleteOper = this.model.query().onBuild((query, result) => {
|
||||
this.extraQuery(query, meta);
|
||||
return result;
|
||||
}).delete();
|
||||
opers.push(deleteOper);
|
||||
});
|
||||
}
|
||||
inserted.forEach((meta) => {
|
||||
const insertOper = this.model.query().insert({
|
||||
[this.KEY_COLUMN]: meta.key,
|
||||
[this.VALUE_COLUMN]: meta.value,
|
||||
...this.extraColumns.reduce((obj, column) => {
|
||||
if (typeof meta[column] !== 'undefined') {
|
||||
obj[column] = meta[column];
|
||||
}
|
||||
return obj;
|
||||
}, {}),
|
||||
});
|
||||
opers.push(insertOper);
|
||||
});
|
||||
updated.forEach((meta) => {
|
||||
const updateOper = this.model.query().onBuild((query) => {
|
||||
this.extraQuery(query, meta);
|
||||
}).patch({
|
||||
[this.VALUE_COLUMN]: meta.value,
|
||||
});
|
||||
opers.push(updateOper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the metadata from the storage.
|
||||
* @param {String|Array} key -
|
||||
* @param {Boolean} force -
|
||||
*/
|
||||
async load() {
|
||||
const metadata = await this.query();
|
||||
|
||||
const metadataArray = this.mapMetadataCollection(metadata);
|
||||
metadataArray.forEach((meta) => {
|
||||
this.metadata.push(meta);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the metadata before saving to the database.
|
||||
* @param {String|Number|Boolean} value -
|
||||
* @param {String} valueType -
|
||||
* @return {String|Number|Boolean} -
|
||||
*/
|
||||
static formatMetaValue(value, valueType) {
|
||||
let parsedValue;
|
||||
|
||||
switch (valueType) {
|
||||
case 'number':
|
||||
parsedValue = `${value}`;
|
||||
break;
|
||||
case 'boolean':
|
||||
parsedValue = value ? '1' : '0';
|
||||
break;
|
||||
case 'json':
|
||||
parsedValue = JSON.stringify(parsedValue);
|
||||
break;
|
||||
default:
|
||||
parsedValue = value;
|
||||
break;
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping and parse metadata to collection entries.
|
||||
* @param {Meta} attr -
|
||||
* @param {String} parseType -
|
||||
*/
|
||||
mapMetadata(attr, parseType = 'parse') {
|
||||
return {
|
||||
key: attr[this.KEY_COLUMN],
|
||||
value: (parseType === 'parse')
|
||||
? MetableCollection.parseMetaValue(
|
||||
attr[this.VALUE_COLUMN],
|
||||
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
|
||||
)
|
||||
: MetableCollection.formatMetaValue(
|
||||
attr[this.VALUE_COLUMN],
|
||||
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
|
||||
),
|
||||
...this.extraColumns.map((extraCol) => ({
|
||||
[extraCol]: attr[extraCol] || null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the metadata to the collection.
|
||||
* @param {Array} collection -
|
||||
*/
|
||||
mapMetadataToCollection(metadata, parseType = 'parse') {
|
||||
return metadata.map((model) => this.mapMetadataToCollection(model, parseType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata to the metable collection.
|
||||
* @param {Array} meta -
|
||||
*/
|
||||
from(meta) {
|
||||
if (Array.isArray(meta)) {
|
||||
meta.forEach((m) => { this.from(m); });
|
||||
return;
|
||||
}
|
||||
this.metadata.push(meta);
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to load metadata to the collection.
|
||||
* @param {Array} meta
|
||||
*/
|
||||
static from(meta) {
|
||||
const collection = new MetableCollection();
|
||||
collection.from(meta);
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
204
server/src/lib/Metable/MetableStore.ts
Normal file
204
server/src/lib/Metable/MetableStore.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { Model } from 'objection';
|
||||
import { omit, isEmpty } from 'lodash';
|
||||
import {
|
||||
IMetadata,
|
||||
IMetaQuery,
|
||||
IMetableStore,
|
||||
} from '@/interfaces';
|
||||
import { itemsStartWith } from '@/utils';
|
||||
|
||||
export default class MetableStore implements IMetableStore{
|
||||
metadata: IMetadata[];
|
||||
model: Model;
|
||||
extraColumns: string[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
this.metadata = [];
|
||||
this.model = null;
|
||||
this.extraColumns = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a extra columns.
|
||||
* @param {Array} columns -
|
||||
*/
|
||||
setExtraColumns(columns: string[]): void {
|
||||
this.extraColumns = columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the given metadata key.
|
||||
* @param {string|IMetaQuery} query -
|
||||
* @returns {IMetadata} - Metadata object.
|
||||
*/
|
||||
find(query: string|IMetaQuery): IMetadata {
|
||||
const { key, value, ...extraColumns } = this.parseQuery(query);
|
||||
|
||||
return this.metadata.find((meta: IMetadata) => {
|
||||
const isSameKey = meta.key === key;
|
||||
const sameExtraColumns = this.extraColumns
|
||||
.some((extraColumn: string) => extraColumns[extraColumn] === meta[extraColumn]);
|
||||
|
||||
const isSameExtraColumns = (sameExtraColumns || isEmpty(extraColumns));
|
||||
|
||||
return isSameKey && isSameExtraColumns;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all metadata.
|
||||
* @returns {IMetadata[]}
|
||||
*/
|
||||
all(): IMetadata[] {
|
||||
return this.metadata
|
||||
.filter((meta: IMetadata) => !meta._markAsDeleted)
|
||||
.map((meta: IMetadata) => omit(
|
||||
meta,
|
||||
itemsStartWith(Object.keys(meta), '_')
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve metadata of the given key.
|
||||
* @param {String} key -
|
||||
* @param {Mixied} defaultValue -
|
||||
*/
|
||||
get(query: string|IMetaQuery, defaultValue: any): any|false {
|
||||
const metadata = this.find(query);
|
||||
return metadata ? metadata.value : defaultValue || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markes the metadata to should be deleted.
|
||||
* @param {String} key -
|
||||
*/
|
||||
remove(query: string|IMetaQuery): void {
|
||||
const metadata: IMetadata = this.find(query);
|
||||
|
||||
if (metadata) {
|
||||
metadata._markAsDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all meta data of the given group.
|
||||
* @param {string} group
|
||||
*/
|
||||
removeAll(group: string = 'default'): void {
|
||||
this.metadata = this.metadata.map((meta) => ({
|
||||
...meta,
|
||||
_markAsDeleted: true,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the meta data to the stack.
|
||||
* @param {String} key -
|
||||
* @param {String} value -
|
||||
*/
|
||||
set(query: IMetaQuery|IMetadata[]|string, metaValue?: any): void {
|
||||
if (Array.isArray(query)) {
|
||||
const metadata = query;
|
||||
|
||||
metadata.forEach((meta: IMetadata) => {
|
||||
this.set(meta.key, meta.value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { key, value, ...extraColumns } = this.parseQuery(query);
|
||||
const metadata = this.find(query);
|
||||
const newValue = metaValue || value;
|
||||
|
||||
if (metadata) {
|
||||
metadata.value = newValue;
|
||||
metadata._markAsUpdated = true;
|
||||
} else {
|
||||
this.metadata.push({
|
||||
value: newValue,
|
||||
key,
|
||||
...extraColumns,
|
||||
_markAsInserted: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses query query.
|
||||
* @param query
|
||||
* @param value
|
||||
*/
|
||||
parseQuery(query: string|IMetaQuery): IMetaQuery {
|
||||
return typeof query !== 'object' ? { key: query } : { ...query };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the metadata before saving to the database.
|
||||
* @param {string|number|boolean} value -
|
||||
* @param {string} valueType -
|
||||
* @return {string|number|boolean} -
|
||||
*/
|
||||
static formatMetaValue(
|
||||
value: string|boolean|number,
|
||||
valueType: string
|
||||
) : string|number|boolean {
|
||||
let parsedValue;
|
||||
|
||||
switch (valueType) {
|
||||
case 'number':
|
||||
parsedValue = `${value}`;
|
||||
break;
|
||||
case 'boolean':
|
||||
parsedValue = value ? '1' : '0';
|
||||
break;
|
||||
case 'json':
|
||||
parsedValue = JSON.stringify(parsedValue);
|
||||
break;
|
||||
default:
|
||||
parsedValue = value;
|
||||
break;
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the metadata to the collection.
|
||||
* @param {Array} collection -
|
||||
*/
|
||||
mapMetadataToCollection(metadata: IMetadata[], parseType: string = 'parse') {
|
||||
return metadata.map((model) => this.mapMetadataToCollection(model, parseType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata to the metable collection.
|
||||
* @param {Array} meta -
|
||||
*/
|
||||
from(meta: []) {
|
||||
if (Array.isArray(meta)) {
|
||||
meta.forEach((m) => { this.from(m); });
|
||||
return;
|
||||
}
|
||||
this.metadata.push(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {array}
|
||||
*/
|
||||
toArray(): IMetadata[] {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to load metadata to the collection.
|
||||
* @param {Array} meta
|
||||
*/
|
||||
static from(meta) {
|
||||
const collection = new MetableCollection();
|
||||
collection.from(meta);
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
213
server/src/lib/Metable/MetableStoreDB.ts
Normal file
213
server/src/lib/Metable/MetableStoreDB.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Model } from 'objection';
|
||||
import {
|
||||
IMetadata,
|
||||
IMetableStoreStorage,
|
||||
} from '@/interfaces';
|
||||
import MetableStore from './MetableStore';
|
||||
|
||||
export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{
|
||||
model: Model;
|
||||
KEY_COLUMN: string;
|
||||
VALUE_COLUMN: string;
|
||||
TYPE_COLUMN: string;
|
||||
extraQuery: Function;
|
||||
loaded: Boolean;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.loaded = false;
|
||||
this.KEY_COLUMN = 'key';
|
||||
this.VALUE_COLUMN = 'value';
|
||||
this.TYPE_COLUMN = 'type';
|
||||
this.model = null;
|
||||
|
||||
this.extraQuery = (query, meta) => {
|
||||
query.where('key', meta[this.KEY_COLUMN]);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set model of this metadata collection.
|
||||
* @param {Object} model -
|
||||
*/
|
||||
setModel(model: Model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a extra query callback.
|
||||
* @param callback
|
||||
*/
|
||||
setExtraQuery(callback) {
|
||||
this.extraQuery = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the modified, deleted and insert metadata.
|
||||
*/
|
||||
save() {
|
||||
this.validateStoreIsLoaded();
|
||||
|
||||
return Promise.all([
|
||||
this.saveUpdated(this.metadata),
|
||||
this.saveDeleted(this.metadata),
|
||||
this.saveInserted(this.metadata),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the updated metadata.
|
||||
* @param {IMetadata[]} metadata -
|
||||
* @returns {Promise}
|
||||
*/
|
||||
saveUpdated(metadata: IMetadata[]) {
|
||||
const updated = metadata.filter((m) => (m._markAsUpdated === true));
|
||||
const opers = [];
|
||||
|
||||
updated.forEach((meta) => {
|
||||
const updateOper = this.model.query().onBuild((query) => {
|
||||
this.extraQuery(query, meta);
|
||||
}).patch({
|
||||
[this.VALUE_COLUMN]: meta.value,
|
||||
}).then(() => {
|
||||
meta._markAsUpdated = false;
|
||||
});
|
||||
opers.push(updateOper);
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the deleted metadata.
|
||||
* @param {IMetadata[]} metadata -
|
||||
* @returns {Promise}
|
||||
*/
|
||||
saveDeleted(metadata: IMetadata[]) {
|
||||
const deleted = metadata.filter((m: IMetadata) => (m._markAsDeleted === true));
|
||||
const opers: Promise<void> = [];
|
||||
|
||||
if (deleted.length > 0) {
|
||||
deleted.forEach((meta) => {
|
||||
const deleteOper = this.model.query().onBuild((query) => {
|
||||
this.extraQuery(query, meta);
|
||||
}).delete().then(() => {
|
||||
meta._markAsDeleted = false;
|
||||
});
|
||||
opers.push(deleteOper);
|
||||
});
|
||||
}
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the inserted metadata.
|
||||
* @param {IMetadata[]} metadata -
|
||||
* @returns {Promise}
|
||||
*/
|
||||
saveInserted(metadata: IMetadata[]) {
|
||||
const inserted = metadata.filter((m: IMetadata) => (m._markAsInserted === true));
|
||||
const opers: Promise<void> = [];
|
||||
|
||||
inserted.forEach((meta) => {
|
||||
const insertData = {
|
||||
[this.KEY_COLUMN]: meta.key,
|
||||
[this.VALUE_COLUMN]: meta.value,
|
||||
|
||||
...this.extraColumns.reduce((obj, column) => {
|
||||
if (typeof meta[column] !== 'undefined') {
|
||||
obj[column] = meta[column];
|
||||
}
|
||||
return obj;
|
||||
}, {}),
|
||||
};
|
||||
const insertOper = this.model.query()
|
||||
.insert(insertData)
|
||||
.then(() => {
|
||||
meta._markAsInserted = false;
|
||||
});
|
||||
opers.push(insertOper);
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the metadata from the storage.
|
||||
* @param {String|Array} key -
|
||||
* @param {Boolean} force -
|
||||
*/
|
||||
async load() {
|
||||
const metadata = await this.model.query();
|
||||
const mappedMetadata = this.mapMetadataCollection(metadata);
|
||||
|
||||
mappedMetadata.forEach((meta: IMetadata) => {
|
||||
this.metadata.push(meta);
|
||||
});
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the metadata before saving to the database.
|
||||
* @param {String|Number|Boolean} value -
|
||||
* @param {String} valueType -
|
||||
* @return {String|Number|Boolean} -
|
||||
*/
|
||||
static formatMetaValue(value: string|number|boolean, valueType: striung|false) {
|
||||
let parsedValue: string|number|boolean;
|
||||
|
||||
switch (valueType) {
|
||||
case 'number':
|
||||
parsedValue = `${value}`;
|
||||
break;
|
||||
case 'boolean':
|
||||
parsedValue = value ? '1' : '0';
|
||||
break;
|
||||
case 'json':
|
||||
parsedValue = JSON.stringify(parsedValue);
|
||||
break;
|
||||
default:
|
||||
parsedValue = value;
|
||||
break;
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping and parse metadata to collection entries.
|
||||
* @param {Meta} attr -
|
||||
* @param {String} parseType -
|
||||
*/
|
||||
mapMetadata(metadata: IMetadata) {
|
||||
return {
|
||||
key: metadata[this.KEY_COLUMN],
|
||||
value: MetableDBStore.formatMetaValue(
|
||||
metadata[this.VALUE_COLUMN],
|
||||
this.TYPE_COLUMN ? metadata[this.TYPE_COLUMN] : false,
|
||||
),
|
||||
...this.extraColumns.reduce((obj, extraCol: string) => {
|
||||
obj[extraCol] = metadata[extraCol] || null;
|
||||
return obj;
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the metadata to the collection.
|
||||
* @param {Array} collection -
|
||||
*/
|
||||
mapMetadataCollection(metadata: IMetadata[]) {
|
||||
return metadata.map((model) => this.mapMetadata(model));
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw error in case the store is not loaded yet.
|
||||
*/
|
||||
private validateStoreIsLoaded() {
|
||||
if (!this.loaded) {
|
||||
throw new Error('You could not save the store before loaded from the storage.');
|
||||
}
|
||||
}
|
||||
}
|
||||
14
server/src/loaders/dbManager.ts
Normal file
14
server/src/loaders/dbManager.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import knexManager from 'knex-db-manager';
|
||||
import knexfile from '@/../config/systemKnexfile';
|
||||
import config from '@/../config/config';
|
||||
|
||||
const knexConfig = knexfile[process.env.NODE_ENV];
|
||||
|
||||
export default () => knexManager.databaseManagerFactory({
|
||||
knex: knexConfig,
|
||||
dbManager: {
|
||||
collate: [],
|
||||
superUser: config.manager.superUser,
|
||||
superPassword: config.manager.superPassword,
|
||||
},
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Container } from 'typedi';
|
||||
import LoggerInstance from '@/services/Logger';
|
||||
import LoggerInstance from '@/loaders/Logger';
|
||||
import agendaFactory from '@/loaders/agenda';
|
||||
import SmsClientLoader from '@/loaders/smsClient';
|
||||
import mailInstance from '@/loaders/mail';
|
||||
import dbManagerFactory from '@/loaders/dbManager';
|
||||
|
||||
export default ({ mongoConnection, knex }) => {
|
||||
try {
|
||||
const agendaInstance = agendaFactory({ mongoConnection });
|
||||
const smsClientInstance = SmsClientLoader();
|
||||
const dbManager = dbManagerFactory();
|
||||
|
||||
Container.set('agenda', agendaInstance);
|
||||
LoggerInstance.info('Agenda has been injected into container');
|
||||
@@ -24,6 +26,9 @@ export default ({ mongoConnection, knex }) => {
|
||||
Container.set('mail', mailInstance);
|
||||
LoggerInstance.info('Mail instance has been injected into container');
|
||||
|
||||
Container.set('dbManager', dbManager);
|
||||
LoggerInstance.info('Database manager has been injected into container.');
|
||||
|
||||
return { agenda: agendaInstance };
|
||||
} catch (e) {
|
||||
LoggerInstance.error('Error on dependency injector loader: %o', e);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Logger from '@/services/Logger';
|
||||
import Logger from '@/loaders/Logger';
|
||||
import mongooseLoader from '@/loaders/mongoose';
|
||||
import jobsLoader from '@/loaders/jobs';
|
||||
import expressLoader from '@/loaders/express';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Agenda from 'agenda';
|
||||
import WelcomeEmailJob from '@/Jobs/welcomeEmail';
|
||||
import ResetPasswordMailJob from '@/Jobs/ResetPasswordMail';
|
||||
import ComputeItemCost from '@/Jobs/ComputeItemCost';
|
||||
import WelcomeEmailJob from '@/jobs/WelcomeEmail';
|
||||
import WelcomeSMSJob from '@/jobs/WelcomeSMS';
|
||||
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';
|
||||
@@ -12,16 +13,24 @@ import SendMailNotificationTrialEnd from '@/jobs/MailNotificationTrialEnd';
|
||||
import UserInviteMailJob from '@/jobs/UserInviteMail';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
// Welcome mail and SMS message.
|
||||
agenda.define(
|
||||
'welcome-email',
|
||||
{ priority: 'high' },
|
||||
new WelcomeEmailJob().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'welcome-sms',
|
||||
{ priority: 'high' },
|
||||
new WelcomeSMSJob().handler
|
||||
);
|
||||
// Reset password mail.
|
||||
agenda.define(
|
||||
'reset-password-mail',
|
||||
{ priority: 'high' },
|
||||
new ResetPasswordMailJob().handler,
|
||||
);
|
||||
// User invite mail.
|
||||
agenda.define(
|
||||
'user-invite-mail',
|
||||
{ priority: 'high' },
|
||||
|
||||
70
server/src/loaders/tenantModels.ts
Normal file
70
server/src/loaders/tenantModels.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { mapValues } from 'lodash';
|
||||
|
||||
import Account from '@/models/Account';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import AccountType from '@/models/AccountType';
|
||||
import Bill from '@/models/Bill';
|
||||
import BillPayment from '@/models/BillPayment';
|
||||
import BillPaymentEntry from '@/models/BillPaymentEntry';
|
||||
import Currency from '@/models/Currency';
|
||||
import Customer from '@/models/Customer';
|
||||
import Vendor from '@/models/Vendor';
|
||||
import ExchangeRate from '@/models/ExchangeRate';
|
||||
import Expense from '@/models/Expense';
|
||||
import ExpenseCategory from '@/models/ExpenseCategory';
|
||||
import View from '@/models/View';
|
||||
import ViewRole from '@/models/ViewRole';
|
||||
import ViewColumn from '@/models/ViewColumn';
|
||||
import Setting from '@/models/Setting';
|
||||
import SaleInvoice from '@/models/SaleInvoice';
|
||||
import SaleInvoiceEntry from '@/models/SaleInvoiceEntry';
|
||||
import SaleReceipt from '@/models/SaleReceipt';
|
||||
import SaleReceiptEntry from '@/models/SaleReceiptEntry';
|
||||
import SaleEstimate from '@/models/SaleEstimate';
|
||||
import SaleEstimateEntry from '@/models/SaleEstimateEntry';
|
||||
import PaymentReceive from '@/models/PaymentReceive';
|
||||
import PaymentReceiveEntry from '@/models/PaymentReceiveEntry';
|
||||
import Option from '@/models/Option';
|
||||
import Resource from '@/models/Resource';
|
||||
import InventoryCostLotTracker from '@/models/InventoryCostLotTracker';
|
||||
import InventoryTransaction from '@/models/InventoryTransaction';
|
||||
import ResourceField from '@/models/ResourceField';
|
||||
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
|
||||
|
||||
export default (knex) => {
|
||||
const models = {
|
||||
Option,
|
||||
Account,
|
||||
AccountBalance,
|
||||
AccountTransaction,
|
||||
AccountType,
|
||||
Bill,
|
||||
BillPayment,
|
||||
BillPaymentEntry,
|
||||
Currency,
|
||||
Customer,
|
||||
Vendor,
|
||||
ExchangeRate,
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
View,
|
||||
ViewRole,
|
||||
ViewColumn,
|
||||
Setting,
|
||||
SaleInvoice,
|
||||
SaleInvoiceEntry,
|
||||
SaleReceipt,
|
||||
SaleReceiptEntry,
|
||||
SaleEstimate,
|
||||
SaleEstimateEntry,
|
||||
PaymentReceive,
|
||||
PaymentReceiveEntry,
|
||||
Resource,
|
||||
InventoryTransaction,
|
||||
InventoryCostLotTracker,
|
||||
ResourceField,
|
||||
ResourceFieldMetadata,
|
||||
};
|
||||
return mapValues(models, (model) => model.bindKnex(knex));
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import TenantModel from '@/models/Model';
|
||||
|
||||
export default class Budget extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'budgets';
|
||||
}
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['rangeBy', 'rangeIncrement'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterByYear(query, year) {
|
||||
query.where('year', year);
|
||||
},
|
||||
filterByIncomeStatement(query) {
|
||||
query.where('account_types', 'income_statement');
|
||||
},
|
||||
filterByProfitLoss(query) {
|
||||
query.where('accounts_types', 'profit_loss');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get rangeBy() {
|
||||
switch (this.period) {
|
||||
case 'half-year':
|
||||
case 'quarter':
|
||||
return 'month';
|
||||
default:
|
||||
return this.period;
|
||||
}
|
||||
}
|
||||
|
||||
get rangeIncrement() {
|
||||
switch (this.period) {
|
||||
case 'half-year':
|
||||
return 6;
|
||||
case 'quarter':
|
||||
return 3;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
get rangeOffset() {
|
||||
switch (this.period) {
|
||||
case 'half-year': return 5;
|
||||
case 'quarter': return 2;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
|
||||
export default class Budget extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'budget_entries';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
||||
import definedOptions from '@/data/options';
|
||||
|
||||
|
||||
@@ -11,27 +10,6 @@ export default class Option extends TenantModel {
|
||||
return 'options';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the model query.
|
||||
* @param {...any} args -
|
||||
*/
|
||||
static query(...args) {
|
||||
return super.query(...args).runAfter((result) => {
|
||||
if (result instanceof MetableCollection) {
|
||||
result.setModel(this.tenant());
|
||||
result.setExtraColumns(['group']);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Model collection.
|
||||
*/
|
||||
static get collection() {
|
||||
return MetableCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given options is defined or either not.
|
||||
* @param {Array} options
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Model } from 'objection';
|
||||
import path from 'path';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
|
||||
export default class Permission extends TenantModel {
|
||||
/**
|
||||
* Table name of Role model.
|
||||
* @type {String}
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'permissions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Role = require('@/models/Role');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Permission model may belongs to role model.
|
||||
*/
|
||||
// role: {
|
||||
// relation: Model.BelongsToOneRelation,
|
||||
// modelBase: path.join(__dirname, 'Role').bindKnex(this.knexBinded),
|
||||
// join: {
|
||||
// from: 'permissions.role_id',
|
||||
// to: 'roles.id',
|
||||
// },
|
||||
// },
|
||||
|
||||
// resource: {
|
||||
// relation: Model.BelongsToOneRelation,
|
||||
// modelBase: path.join(__dirname, 'Resource'),
|
||||
// join: {
|
||||
// from: 'permissions.',
|
||||
// to: '',
|
||||
// }
|
||||
// }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) {
|
||||
static get relationMappings() {
|
||||
const View = require('@/models/View');
|
||||
const ResourceField = require('@/models/ResourceField');
|
||||
const Permission = require('@/models/Permission');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -57,22 +56,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) {
|
||||
to: 'resource_fields.resourceId',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Resource model may has many associated permissions.
|
||||
*/
|
||||
permissions: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: this.relationBindKnex(Permission.default),
|
||||
join: {
|
||||
from: 'resources.id',
|
||||
through: {
|
||||
from: 'role_has_permissions.resourceId',
|
||||
to: 'role_has_permissions.permissionId',
|
||||
},
|
||||
to: 'permissions.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
|
||||
|
||||
export default class ResourceFieldMetadata extends TenantModel {
|
||||
/**
|
||||
@@ -8,11 +7,4 @@ export default class ResourceFieldMetadata extends TenantModel {
|
||||
static get tableName() {
|
||||
return 'resource_custom_fields_metadata';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the resource field metadata collection.
|
||||
*/
|
||||
static get collection() {
|
||||
return ResourceFieldMetadataCollection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
|
||||
export default class Role extends TenantModel {
|
||||
/**
|
||||
* Table name of Role model.
|
||||
* @type {String}
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'roles';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
static get hasTimestamps() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Permission = require('@/models/Permission');
|
||||
const Resource = require('@/models/Resource');
|
||||
const User = require('@/models/TenantUser');
|
||||
const ResourceField = require('@/models/ResourceField');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Role may has many permissions.
|
||||
*/
|
||||
permissions: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Permission.default.bindKnex(this.knexBinded),
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
through: {
|
||||
from: 'role_has_permissions.roleId',
|
||||
to: 'role_has_permissions.permissionId',
|
||||
},
|
||||
to: 'permissions.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Role may has many resources.
|
||||
*/
|
||||
resources: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Resource.default.bindKnex(this.knexBinded),
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
through: {
|
||||
from: 'role_has_permissions.roleId',
|
||||
to: 'role_has_permissions.resourceId',
|
||||
},
|
||||
to: 'resources.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Role may has resource field.
|
||||
*/
|
||||
field: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: ResourceField.default.bindKnex(this.knexBinded),
|
||||
join: {
|
||||
from: 'roles.fieldId',
|
||||
to: 'resource_fields.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Role may has many associated users.
|
||||
*/
|
||||
users: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: User.default.bindKnex(this.knexBinded),
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
through: {
|
||||
from: 'user_has_roles.roleId',
|
||||
to: 'user_has_roles.userId',
|
||||
},
|
||||
to: 'users.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
// import PermissionsService from '@/services/PermissionsService';
|
||||
|
||||
export default class TenantUser extends TenantModel {
|
||||
/**
|
||||
@@ -24,29 +22,7 @@ export default class TenantUser extends TenantModel {
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Role = require('@/models/Role');
|
||||
|
||||
return {
|
||||
roles: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: this.relationBindKnex(Role.default),
|
||||
join: {
|
||||
from: 'users.id',
|
||||
through: {
|
||||
from: 'user_has_roles.userId',
|
||||
to: 'user_has_roles.roleId',
|
||||
},
|
||||
to: 'roles.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify the password of the user.
|
||||
* @param {String} password - The given password.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Service } from "typedi";
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationMailMesssages {
|
||||
|
||||
sendWelcomeMessage() {
|
||||
const Logger = Container.get('logger');
|
||||
const Mail = Container.get('mail');
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
sendResetPasswordMessage() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Service } from "typedi";
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationSMSMessages {
|
||||
smsClient: any;
|
||||
|
||||
sendWelcomeMessage() {
|
||||
const message: string = `Hi ${firstName}, Welcome to Bigcapital, You've joined the new workspace,
|
||||
if you need any help please don't hesitate to contact us.`
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import JWT from 'jsonwebtoken';
|
||||
import uniqid from 'uniqid';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
EventDispatcher
|
||||
EventDispatcherInterface
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from '@/decorators/eventDispatcher';
|
||||
import {
|
||||
SystemUser,
|
||||
@@ -22,6 +22,8 @@ import { hashPassword } from '@/utils';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import config from '@/../config/config';
|
||||
import events from '@/subscribers/events';
|
||||
import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages';
|
||||
import AuthenticationSMSMessages from '@/services/Authentication/AuthenticationSMSMessages';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationService {
|
||||
@@ -34,6 +36,12 @@ export default class AuthenticationService {
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject()
|
||||
smsMessages: AuthenticationSMSMessages;
|
||||
|
||||
@Inject()
|
||||
mailMessages: AuthenticationMailMessages;
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
@@ -70,6 +78,7 @@ export default class AuthenticationService {
|
||||
|
||||
this.logger.info('[login] Logging success.', { user, token });
|
||||
|
||||
// Triggers `onLogin` event.
|
||||
this.eventDispatcher.dispatch(events.auth.login, {
|
||||
emailOrPhone, password,
|
||||
});
|
||||
@@ -191,6 +200,7 @@ export default class AuthenticationService {
|
||||
const passwordReset = await PasswordReset.query().insert({ email, token });
|
||||
const user = await SystemUser.query().findOne('email', email);
|
||||
|
||||
// Triggers `onSendResetPassword` event.
|
||||
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token });
|
||||
|
||||
return passwordReset;
|
||||
@@ -225,25 +235,26 @@ export default class AuthenticationService {
|
||||
// Delete the reset password token.
|
||||
await PasswordReset.query().where('email', user.email).delete();
|
||||
|
||||
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token, password });
|
||||
// Triggers `onResetPassword` event.
|
||||
this.eventDispatcher.dispatch(events.auth.resetPassword, { user, token, password });
|
||||
|
||||
this.logger.info('[reset_password] reset password success.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JWT token for the given user.
|
||||
* @param {IUser} user
|
||||
* @param {ISystemUser} user
|
||||
* @return {string} token
|
||||
*/
|
||||
generateToken(user: IUser): string {
|
||||
generateToken(user: ISystemUser): string {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
this.logger.silly(`Sign JWT for userId: ${user._id}`);
|
||||
this.logger.silly(`Sign JWT for userId: ${user.id}`);
|
||||
return JWT.sign(
|
||||
{
|
||||
_id: user._id, // We are gonna use this in the middleware 'isAuth'
|
||||
id: user.id, // We are gonna use this in the middleware 'isAuth'
|
||||
exp: exp.getTime() / 1000,
|
||||
},
|
||||
config.jwtSecret,
|
||||
|
||||
31
server/src/services/InviteUsers/InviteUsersMailMessages.ts
Normal file
31
server/src/services/InviteUsers/InviteUsersMailMessages.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Service } from "typedi";
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersMailMessages {
|
||||
|
||||
sendInviteMail() {
|
||||
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const rendered = Mustache.render(template, {
|
||||
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
organizationName: organizationOptions.getMeta('organization_name'),
|
||||
});
|
||||
const mailOptions = {
|
||||
to: user.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: `${user.fullName} has invited you to join a Bigcapital`,
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send user invite mail', { error, form });
|
||||
}
|
||||
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
|
||||
});
|
||||
}
|
||||
}
|
||||
172
server/src/services/InviteUsers/index.ts
Normal file
172
server/src/services/InviteUsers/index.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import uniqid from 'uniqid';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from '@/decorators/eventDispatcher';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import { SystemUser, Invite } from "@/system/models";
|
||||
import { hashPassword } from '@/utils';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import TenantsManager from "@/system/TenantsManager";
|
||||
import InviteUsersMailMessages from "@/services/InviteUsers/InviteUsersMailMessages";
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ISystemUser,
|
||||
IInviteUserInput,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class InviteUserService {
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
mailMessages: InviteUsersMailMessages;
|
||||
|
||||
/**
|
||||
* Accept the received invite.
|
||||
* @param {string} token
|
||||
* @param {IInviteUserInput} inviteUserInput
|
||||
* @throws {ServiceErrors}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void> {
|
||||
const inviteToken = await this.getInviteOrThrowError(token);
|
||||
await this.validateUserEmailAndPhone(inviteUserInput);
|
||||
|
||||
this.logger.info('[aceept_invite] trying to hash the user password.');
|
||||
const hashedPassword = await hashPassword(inviteUserInput.password);
|
||||
|
||||
const user = SystemUser.query()
|
||||
.where('email', inviteUserInput.email)
|
||||
.patch({
|
||||
...inviteUserInput,
|
||||
active: 1,
|
||||
email: inviteToken.email,
|
||||
invite_accepted_at: moment().format('YYYY/MM/DD'),
|
||||
password: hashedPassword,
|
||||
tenant_id: inviteToken.tenantId,
|
||||
});
|
||||
|
||||
const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete();
|
||||
|
||||
await Promise.all([
|
||||
insertUserOper,
|
||||
deleteInviteTokenOper,
|
||||
]);
|
||||
|
||||
// Triggers `onUserAcceptInvite` event.
|
||||
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
|
||||
inviteToken, user,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends invite mail to the given email from the given tenant and user.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} email -
|
||||
* @param {IUser} authorizedUser -
|
||||
*
|
||||
* @return {Promise<IInvite>}
|
||||
*/
|
||||
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<IInvite> {
|
||||
const { Option } = this.tenancy.models(tenantId);
|
||||
await this.throwErrorIfUserEmailExists(email);
|
||||
|
||||
const invite = await Invite.query().insert({
|
||||
email,
|
||||
tenant_id: authorizedUser.tenantId,
|
||||
token: uniqid(),
|
||||
});
|
||||
|
||||
// Triggers `onUserSendInvite` event.
|
||||
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
|
||||
invite,
|
||||
});
|
||||
return { invite };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given invite token.
|
||||
* @param {string} token - the given token string.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async checkInvite(token: string) {
|
||||
const inviteToken = await this.getInviteOrThrowError(token)
|
||||
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query().findOne('id', inviteToken.tenantId);
|
||||
|
||||
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
|
||||
|
||||
const organizationOptions = await Option.bindKnex(tenantDb).query()
|
||||
.where('key', 'organization_name');
|
||||
|
||||
// Triggers `onUserCheckInvite` event.
|
||||
this.eventDispatcher.dispatch(events.inviteUser.checkInvite, {
|
||||
inviteToken, organizationOptions,
|
||||
});
|
||||
return { inviteToken, organizationOptions };
|
||||
}
|
||||
|
||||
private async throwErrorIfUserEmailExists(email: string) {
|
||||
const foundUser = await SystemUser.query().findOne('email', email);
|
||||
|
||||
if (foundUser) {
|
||||
throw new ServiceError('email_already_invited');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve invite model from the given token or throw error.
|
||||
* @param {string} token - Then given token string.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
private async getInviteOrThrowError(token: string) {
|
||||
const inviteToken = await Invite.query().findOne('token', token);
|
||||
|
||||
if (!inviteToken) {
|
||||
this.logger.info('[aceept_invite] the invite token is invalid.');
|
||||
throw new ServiceError('invite_token_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given user email and phone number uniquine.
|
||||
* @param {IInviteUserInput} inviteUserInput
|
||||
*/
|
||||
private async validateUserEmailAndPhone(inviteUserInput: IInviteUserInput) {
|
||||
const foundUser = await SystemUser.query()
|
||||
.onBuild(query => {
|
||||
query.where('email', inviteUserInput.email);
|
||||
|
||||
if (inviteUserInput.phoneNumber) {
|
||||
query.where('phone_number', inviteUserInput.phoneNumber);
|
||||
}
|
||||
});
|
||||
const serviceErrors: ServiceError[] = [];
|
||||
|
||||
if (foundUser && foundUser.email === inviteUserInput.email) {
|
||||
this.logger.info('[send_user_invite] the given email exists.');
|
||||
serviceErrors.push(new ServiceError('email_exists'));
|
||||
}
|
||||
if (foundUser && foundUser.phoneNumber === inviteUserInput.phoneNumber) {
|
||||
this.logger.info('[send_user_invite] the given phone number exists.');
|
||||
serviceErrors.push(new ServiceError('phone_number_exists'));
|
||||
}
|
||||
if (serviceErrors.length > 0) {
|
||||
throw new ServiceErrors(serviceErrors);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import Moment from 'moment';
|
||||
import { extendMoment } from 'moment-range';
|
||||
|
||||
const moment = extendMoment(Moment);
|
||||
|
||||
export default moment;
|
||||
56
server/src/services/Organization/index.ts
Normal file
56
server/src/services/Organization/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { Tenant } from '@/system/models';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ITenant } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationService {
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
@Inject('dbManager')
|
||||
dbManager: any;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Builds the database schema and seed data of the given organization id.
|
||||
* @param {srting} organizationId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async build(organizationId: string): Promise<void> {
|
||||
const tenant = await Tenant.query().findOne('organization_id', organizationId);
|
||||
this.throwIfTenantNotExists(tenant);
|
||||
this.throwIfTenantInitizalized(tenant);
|
||||
|
||||
this.logger.info('[tenant_db_build] tenant DB creating.', { tenant });
|
||||
await this.dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`);
|
||||
|
||||
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
|
||||
|
||||
this.logger.info('[tenant_db_build] tenant DB migrating to latest version.', { tenant });
|
||||
await tenantDb.migrate.latest();
|
||||
|
||||
this.logger.info('[tenant_db_build] mark tenant as initialized.', { tenant });
|
||||
await tenant.$query().update({ initialized: true });
|
||||
}
|
||||
|
||||
private throwIfTenantNotExists(tenant: ITenant) {
|
||||
if (!tenant) {
|
||||
this.logger.info('[tenant_db_build] organization id not found.');
|
||||
throw new ServiceError('tenant_not_found');
|
||||
}
|
||||
}
|
||||
|
||||
private throwIfTenantInitizalized(tenant: ITenant) {
|
||||
if (tenant.initialized) {
|
||||
throw new ServiceError('tenant_initialized');
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import cache from 'memory-cache';
|
||||
import { difference } from 'lodash';
|
||||
import Role from '@/models/Role';
|
||||
|
||||
export default {
|
||||
cacheKey: 'ratteb.cache,',
|
||||
cacheExpirationTime: null,
|
||||
permissions: [],
|
||||
cache: null,
|
||||
|
||||
/**
|
||||
* Initialize the cache.
|
||||
*/
|
||||
initializeCache() {
|
||||
if (!this.cache) {
|
||||
this.cache = new cache.Cache();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Purge all cached permissions.
|
||||
*/
|
||||
forgetCachePermissions() {
|
||||
this.cache.del(this.cacheKey);
|
||||
this.permissions = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all stored permissions.
|
||||
*/
|
||||
async getPermissions() {
|
||||
if (this.permissions.length <= 0) {
|
||||
const cachedPerms = this.cache.get(this.cacheKey);
|
||||
|
||||
if (!cachedPerms) {
|
||||
this.permissions = await this.getPermissionsFromStorage();
|
||||
this.cache.put(this.cacheKey, this.permissions);
|
||||
} else {
|
||||
this.permissions = cachedPerms;
|
||||
}
|
||||
}
|
||||
return this.permissions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all roles and permissions from the storage.
|
||||
*/
|
||||
async getPermissionsFromStorage() {
|
||||
const roles = await Role.fetchAll({
|
||||
withRelated: ['resources.permissions'],
|
||||
});
|
||||
return roles.toJSON();
|
||||
},
|
||||
|
||||
/**
|
||||
* Detarmine the given resource has the permissions.
|
||||
* @param {String} resource -
|
||||
* @param {Array} permissions -
|
||||
*/
|
||||
async hasPermissions(resource, permissions) {
|
||||
await this.getPermissions();
|
||||
|
||||
const userRoles = this.permissions.filter((role) => role.id === this.id);
|
||||
const perms = [];
|
||||
|
||||
userRoles.forEach((role) => {
|
||||
const roleResources = role.resources || [];
|
||||
const foundResource = roleResources.find((r) => r.name === resource);
|
||||
|
||||
if (foundResource && foundResource.permissions) {
|
||||
foundResource.permissions.forEach((p) => perms.push(p.name));
|
||||
}
|
||||
});
|
||||
const notAllowedPerms = difference(permissions, perms);
|
||||
return (notAllowedPerms.length <= 0);
|
||||
},
|
||||
};
|
||||
@@ -27,6 +27,9 @@ export default class PaymentReceiveService {
|
||||
@Inject()
|
||||
journalService: JournalPosterService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Creates a new payment receive and store it to the storage
|
||||
* with associated invoices payment and journal transactions.
|
||||
@@ -43,6 +46,8 @@ export default class PaymentReceiveService {
|
||||
} = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
|
||||
|
||||
this.logger.info('[payment_receive] inserting to the storage.');
|
||||
const storedPaymentReceive = await PaymentReceive.query()
|
||||
.insert({
|
||||
amount: paymentAmount,
|
||||
@@ -50,12 +55,15 @@ export default class PaymentReceiveService {
|
||||
});
|
||||
const storeOpers: Array<any> = [];
|
||||
|
||||
this.logger.info('[payment_receive] inserting associated entries to the storage.');
|
||||
paymentReceive.entries.forEach((entry: any) => {
|
||||
const oper = PaymentReceiveEntry.query()
|
||||
.insert({
|
||||
payment_receive_id: storedPaymentReceive.id,
|
||||
...entry,
|
||||
});
|
||||
|
||||
this.logger.info('[payment_receive] increment the sale invoice payment amount.');
|
||||
// Increment the invoice payment amount.
|
||||
const invoice = SaleInvoice.query()
|
||||
.where('id', entry.invoice_id)
|
||||
@@ -64,6 +72,8 @@ export default class PaymentReceiveService {
|
||||
storeOpers.push(oper);
|
||||
storeOpers.push(invoice);
|
||||
});
|
||||
|
||||
this.logger.info('[payment_receive] decrementing customer balance.');
|
||||
const customerIncrementOper = Customer.decrementBalance(
|
||||
paymentReceive.customer_id,
|
||||
paymentAmount,
|
||||
|
||||
@@ -16,6 +16,9 @@ export default class SaleEstimateService {
|
||||
@Inject()
|
||||
itemsEntriesService: HasItemsEntries;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
* @async
|
||||
@@ -31,12 +34,15 @@ export default class SaleEstimateService {
|
||||
amount,
|
||||
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
|
||||
};
|
||||
|
||||
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
|
||||
const storedEstimate = await SaleEstimate.query()
|
||||
.insert({
|
||||
...omit(estimate, ['entries']),
|
||||
});
|
||||
const storeEstimateEntriesOpers: any[] = [];
|
||||
|
||||
this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.');
|
||||
estimate.entries.forEach((entry: any) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insert({
|
||||
@@ -48,6 +54,8 @@ export default class SaleEstimateService {
|
||||
});
|
||||
await Promise.all([...storeEstimateEntriesOpers]);
|
||||
|
||||
this.logger.info('[sale_estimate] insert sale estimated success.');
|
||||
|
||||
return storedEstimate;
|
||||
}
|
||||
|
||||
@@ -67,6 +75,7 @@ export default class SaleEstimateService {
|
||||
amount,
|
||||
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
|
||||
};
|
||||
this.logger.info('[sale_estimate] editing sale estimate on the storage.');
|
||||
const updatedEstimate = await SaleEstimate.query()
|
||||
.update({
|
||||
...omit(estimate, ['entries']),
|
||||
@@ -96,14 +105,14 @@ export default class SaleEstimateService {
|
||||
*/
|
||||
async deleteEstimate(tenantId: number, estimateId: number) {
|
||||
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.');
|
||||
await ItemEntry.query()
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate')
|
||||
.delete();
|
||||
|
||||
await SaleEstimate.query()
|
||||
.where('id', estimateId)
|
||||
.delete();
|
||||
await SaleEstimate.query().where('id', estimateId).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,10 +122,10 @@ export default class SaleEstimateService {
|
||||
* @param {Numeric} estimateId
|
||||
* @return {Boolean}
|
||||
*/
|
||||
async isEstimateExists(estimateId: number) {
|
||||
async isEstimateExists(tenantId: number, estimateId: number) {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
const foundEstimate = await SaleEstimate.query()
|
||||
.where('id', estimateId);
|
||||
const foundEstimate = await SaleEstimate.query().where('id', estimateId);
|
||||
|
||||
return foundEstimate.length !== 0;
|
||||
}
|
||||
|
||||
@@ -192,7 +201,6 @@ export default class SaleEstimateService {
|
||||
const foundEstimates = await SaleEstimate.query()
|
||||
.onBuild((query: any) => {
|
||||
query.where('estimate_number', estimateNumber);
|
||||
|
||||
if (excludeEstimateId) {
|
||||
query.whereNot('id', excludeEstimateId);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
@Inject()
|
||||
itemsEntriesService: HasItemsEntries;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Creates a new sale invoices and store it to the storage
|
||||
* with associated to entries and journal transactions.
|
||||
@@ -43,12 +46,15 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
paymentAmount: 0,
|
||||
invLotNumber,
|
||||
};
|
||||
|
||||
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
|
||||
const storedInvoice = await SaleInvoice.query()
|
||||
.insert({
|
||||
...omit(saleInvoice, ['entries']),
|
||||
});
|
||||
const opers: Array<any> = [];
|
||||
|
||||
this.logger.info('[sale_invoice] inserting sale invoice entries to the storage.');
|
||||
saleInvoice.entries.forEach((entry: any) => {
|
||||
const oper = ItemEntry.query()
|
||||
.insertAndFetch({
|
||||
@@ -61,15 +67,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
opers.push(oper);
|
||||
});
|
||||
|
||||
this.logger.info('[sale_invoice] trying to increment the customer balance.');
|
||||
// Increment the customer balance after deliver the sale invoice.
|
||||
const incrementOper = Customer.incrementBalance(
|
||||
saleInvoice.customer_id,
|
||||
balance,
|
||||
);
|
||||
|
||||
// Await all async operations.
|
||||
await Promise.all([
|
||||
...opers, incrementOper,
|
||||
]);
|
||||
await Promise.all([ ...opers, incrementOper ]);
|
||||
|
||||
// Records the inventory transactions for inventory items.
|
||||
await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id);
|
||||
|
||||
@@ -100,6 +107,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
balance,
|
||||
invLotNumber: oldSaleInvoice.invLotNumber,
|
||||
};
|
||||
|
||||
this.logger.info('[sale_invoice] trying to update sale invoice.');
|
||||
const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query()
|
||||
.where('id', saleInvoiceId)
|
||||
.update({
|
||||
@@ -114,6 +123,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
|
||||
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
|
||||
);
|
||||
|
||||
this.logger.info('[sale_invoice] change customer different balance.');
|
||||
// Changes the diff customer balance between old and new amount.
|
||||
const changeCustomerBalanceOper = Customer.changeDiffBalance(
|
||||
saleInvoice.customer_id,
|
||||
@@ -155,12 +166,14 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries');
|
||||
|
||||
this.logger.info('[sale_invoice] delete sale invoice with entries.');
|
||||
await SaleInvoice.query().where('id', saleInvoiceId).delete();
|
||||
await ItemEntry.query()
|
||||
.where('reference_id', saleInvoiceId)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.delete();
|
||||
|
||||
this.logger.info('[sale_invoice] revert the customer balance.');
|
||||
const revertCustomerBalanceOper = Customer.changeBalance(
|
||||
oldSaleInvoice.customerId,
|
||||
oldSaleInvoice.balance * -1,
|
||||
@@ -203,7 +216,13 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {number} saleInvoiceId -
|
||||
* @param {boolean} override -
|
||||
*/
|
||||
recordInventoryTranscactions(tenantId: number, saleInvoice, saleInvoiceId: number, override?: boolean){
|
||||
recordInventoryTranscactions(
|
||||
tenantId: number,
|
||||
saleInvoice,
|
||||
saleInvoiceId: number,
|
||||
override?: boolean
|
||||
){
|
||||
this.logger.info('[sale_invoice] saving inventory transactions');
|
||||
const inventortyTransactions = saleInvoice.entries
|
||||
.map((entry) => ({
|
||||
...pick(entry, ['item_id', 'quantity', 'rate',]),
|
||||
@@ -228,6 +247,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
|
||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
const opers: Promise<[]>[] = [];
|
||||
|
||||
this.logger.info('[sale_invoice] reverting inventory transactions');
|
||||
|
||||
inventoryTransactions.forEach((trans: any) => {
|
||||
switch(trans.direction) {
|
||||
@@ -359,7 +380,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* Writes the sale invoice journal entries.
|
||||
* @param {SaleInvoice} saleInvoice -
|
||||
*/
|
||||
async writeNonInventoryInvoiceJournals(tenantId: number, saleInvoice: ISaleInvoice, override: boolean) {
|
||||
async writeNonInventoryInvoiceJournals(
|
||||
tenantId: number,
|
||||
saleInvoice: ISaleInvoice,
|
||||
override: boolean
|
||||
) {
|
||||
const { Account, AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query();
|
||||
|
||||
15
server/src/services/Settings/SettingsStore.ts
Normal file
15
server/src/services/Settings/SettingsStore.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Knex from 'knex';
|
||||
import MetableStoreDB from '@/lib/Metable/MetableStoreDB';
|
||||
import Setting from '@/models/Setting';
|
||||
|
||||
export default class SettingsStore extends MetableStoreDB {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
constructor(knex: Knex) {
|
||||
super();
|
||||
this.setExtraColumns(['group']);
|
||||
this.setModel(Setting.bindKnex(knex));
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ export default class HasTenancyService {
|
||||
* @param {number} tenantId - The tenant id.
|
||||
*/
|
||||
models(tenantId: number) {
|
||||
console.log(tenantId);
|
||||
return this.tenantContainer(tenantId).get('models');
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,10 @@ export default {
|
||||
sendResetPassword: 'onSendResetPassword',
|
||||
resetPassword: 'onResetPassword',
|
||||
},
|
||||
}
|
||||
|
||||
inviteUser: {
|
||||
acceptInvite: 'onUserAcceptInvite',
|
||||
sendInvite: 'onUserSendInvite',
|
||||
checkInvite: 'onUserCheckInvite'
|
||||
}
|
||||
}
|
||||
|
||||
29
server/src/subscribers/inviteUser.ts
Normal file
29
server/src/subscribers/inviteUser.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Container } from 'typedi';
|
||||
import { EventSubscriber, On } from 'event-dispatch';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@EventSubscriber()
|
||||
export class InviteUserSubscriber {
|
||||
|
||||
@On(events.inviteUser.acceptInvite)
|
||||
public onAcceptInvite(payload) {
|
||||
const { inviteToken, user } = payload;
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
}
|
||||
|
||||
@On(events.inviteUser.checkInvite)
|
||||
public onCheckInvite(payload) {
|
||||
const { inviteToken, organizationOptions } = payload;
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
}
|
||||
|
||||
@On(events.inviteUser.sendInvite)
|
||||
public onSendInvite(payload) {
|
||||
const { invite } = payload;
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
|
||||
export default class TenantEnviroment {
|
||||
|
||||
static get currentTenant() {
|
||||
return this.currentTenantWebsite;
|
||||
}
|
||||
|
||||
static set currentTenant(website) {
|
||||
this.currentTenantWebsite = website;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ exports.up = function(knex) {
|
||||
return knex.schema.createTable('tenants', (table) => {
|
||||
table.bigIncrements();
|
||||
table.string('organization_id');
|
||||
table.boolean('initialized').defaultTo(false);
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
||||
|
||||
export default class Option extends SystemModel {
|
||||
/**
|
||||
@@ -9,21 +8,4 @@ export default class Option extends SystemModel {
|
||||
static get tableName() {
|
||||
return 'options';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the model query.
|
||||
* @param {...any} args -
|
||||
*/
|
||||
static query(...args) {
|
||||
return super.query(...args).runAfter((result) => {
|
||||
if (result instanceof MetableCollection) {
|
||||
result.setModel(Option);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
static get collection() {
|
||||
return MetableCollection;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user