mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 07:40:32 +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:
@@ -10,7 +10,6 @@ const {
|
|||||||
success,
|
success,
|
||||||
log,
|
log,
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
const lincenseCommander = require('./license');
|
|
||||||
|
|
||||||
// - bigcapital system:migrate:latest
|
// - bigcapital system:migrate:latest
|
||||||
// - bigcapital system:migrate:rollback
|
// - bigcapital system:migrate:rollback
|
||||||
@@ -21,9 +20,6 @@ const lincenseCommander = require('./license');
|
|||||||
// - bigcapital tenants:migrate:make
|
// - bigcapital tenants:migrate:make
|
||||||
// - bigcapital system:migrate:make
|
// - bigcapital system:migrate:make
|
||||||
// - bigcapital tenants:list
|
// - bigcapital tenants:list
|
||||||
//
|
|
||||||
// - bigcapital license:generate
|
|
||||||
// - bigcapital licenses:list
|
|
||||||
|
|
||||||
commander
|
commander
|
||||||
.command('system:migrate:rollback')
|
.command('system:migrate:rollback')
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
const commander = require('commander');
|
|
||||||
const color = require('colorette');
|
|
||||||
const argv = require('getopts')(process.argv.slice(2));
|
|
||||||
const cryptoRandomString = require('crypto-random-string');
|
|
||||||
const {
|
|
||||||
initSystemKnex,
|
|
||||||
getAllSystemTenants,
|
|
||||||
initTenantKnex,
|
|
||||||
exit,
|
|
||||||
success,
|
|
||||||
log,
|
|
||||||
} = require('./utils');
|
|
||||||
|
|
||||||
// License generate key.
|
|
||||||
commander
|
|
||||||
.command('license:generate <license_period>')
|
|
||||||
.description('Generates a new license key.')
|
|
||||||
.action(async (interval) => {
|
|
||||||
try {
|
|
||||||
const sysDb = initSystemKnex();
|
|
||||||
let repeat = true;
|
|
||||||
|
|
||||||
while(repeat) {
|
|
||||||
key = cryptoRandomString(16).toUpperCase();
|
|
||||||
const license = await sysDb('subscription_licenses').where('key', key);
|
|
||||||
|
|
||||||
if (license.length === 0) {
|
|
||||||
repeat = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const licenseIds = await sysDb('subscription_licenses').insert({
|
|
||||||
key,
|
|
||||||
license_period: interval ? parseInt(interval, 10) : 1,
|
|
||||||
license_interval: 'month',
|
|
||||||
});
|
|
||||||
const license = await sysDb('subscription_licenses').where('id', licenseIds[0]).first();
|
|
||||||
success(`ID: ${license.id} | License: ${license.key} | Interval: ${license.licenseInterval} | Period: ${license.licensePeriod}`);
|
|
||||||
} catch(error) {
|
|
||||||
exit(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieve licenses list.
|
|
||||||
commander
|
|
||||||
.command('licenses:list')
|
|
||||||
.description('Retrieve a list of subscription licenses.')
|
|
||||||
.action(async () => {
|
|
||||||
const sysDb = initSystemKnex();
|
|
||||||
const licenses = await sysDb('subscription_licenses');
|
|
||||||
|
|
||||||
licenses.forEach((license) => {
|
|
||||||
log(`ID: ${license.id} | Key: ${license.key} | Interval: ${license.licenseInterval} | Period: ${license.licensePeriod}`);
|
|
||||||
});
|
|
||||||
exit();
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Your favorite port
|
||||||
|
*/
|
||||||
|
port: parseInt(process.env.PORT, 10),
|
||||||
|
|
||||||
system: {
|
system: {
|
||||||
db_client: 'mysql',
|
db_client: 'mysql',
|
||||||
db_host: '127.0.0.1',
|
db_host: '127.0.0.1',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import config from '@/../config/config';
|
|||||||
|
|
||||||
const knexConfig = knexfile[process.env.NODE_ENV];
|
const knexConfig = knexfile[process.env.NODE_ENV];
|
||||||
|
|
||||||
const dbManager = knexManager.databaseManagerFactory({
|
export default () => knexManager.databaseManagerFactory({
|
||||||
knex: knexConfig,
|
knex: knexConfig,
|
||||||
dbManager: {
|
dbManager: {
|
||||||
collate: [],
|
collate: [],
|
||||||
@@ -12,5 +12,3 @@ const dbManager = knexManager.databaseManagerFactory({
|
|||||||
superPassword: config.manager.superPassword,
|
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 { EventDispatcher as EventDispatcherClass } from 'event-dispatch';
|
||||||
import { Container } from 'typedi';
|
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 { 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 { Service, Inject } from 'typedi';
|
||||||
import { camelCase, mapKeys } from 'lodash';
|
import BaseController from '@/http/controllers/BaseController';
|
||||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import prettierMiddleware from '@/http/middleware/prettierMiddleware';
|
|
||||||
import AuthenticationService from '@/services/Authentication';
|
import AuthenticationService from '@/services/Authentication';
|
||||||
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
|
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
|
||||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||||
import { IRegisterDTO } from 'src/interfaces';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationController {
|
export default class AuthenticationController extends BaseController{
|
||||||
@Inject()
|
@Inject()
|
||||||
authService: AuthenticationService;
|
authService: AuthenticationService;
|
||||||
|
|
||||||
@@ -88,6 +86,9 @@ export default class AuthenticationController {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send reset password validation schema.
|
||||||
|
*/
|
||||||
get sendResetPasswordSchema(): ValidationChain[] {
|
get sendResetPasswordSchema(): ValidationChain[] {
|
||||||
return [
|
return [
|
||||||
check('email').exists().isEmail().trim().escape(),
|
check('email').exists().isEmail().trim().escape(),
|
||||||
@@ -100,10 +101,7 @@ export default class AuthenticationController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async login(req: Request, res: Response, next: Function): Response {
|
async login(req: Request, res: Response, next: Function): Response {
|
||||||
const userDTO: IUserOTD = mapKeys(matchedData(req, {
|
const userDTO: IUserOTD = this.matchedBodyData(req);
|
||||||
locations: ['body'],
|
|
||||||
includeOptionals: true,
|
|
||||||
}), (v, k) => camelCase(k));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { token, user } = await this.authService.signIn(
|
const { token, user } = await this.authService.signIn(
|
||||||
@@ -134,13 +132,10 @@ export default class AuthenticationController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async register(req: Request, res: Response, next: Function) {
|
async register(req: Request, res: Response, next: Function) {
|
||||||
const registerDTO: IRegisterDTO = mapKeys(matchedData(req, {
|
const registerDTO: IRegisterOTD = this.matchedBodyData(req);
|
||||||
locations: ['body'],
|
|
||||||
includeOptionals: true,
|
|
||||||
}), (v, k) => camelCase(k));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registeredUser = await this.authService.register(registerDTO);
|
const registeredUser: ISystemUser = await this.authService.register(registerDTO);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
code: 'REGISTER.SUCCESS',
|
code: 'REGISTER.SUCCESS',
|
||||||
@@ -170,7 +165,7 @@ export default class AuthenticationController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||||
const { email } = req.body;
|
const { email } = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.authService.sendResetPassword(email);
|
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 {
|
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() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(JWTAuth);
|
|
||||||
|
|
||||||
router.post(
|
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 {
|
import {
|
||||||
check,
|
check,
|
||||||
param,
|
param,
|
||||||
@@ -17,22 +17,21 @@ import {
|
|||||||
mapFilterRolesToDynamicFilter,
|
mapFilterRolesToDynamicFilter,
|
||||||
} from '@/lib/ViewRolesBuilder';
|
} from '@/lib/ViewRolesBuilder';
|
||||||
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
|
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
|
||||||
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
|
import BaseController from '@/http/controllers/BaseController';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ItemsCategoriesController {
|
export default class ItemsCategoriesController extends BaseController {
|
||||||
/**
|
/**
|
||||||
* Router constructor method.
|
* Router constructor method.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
router() {
|
||||||
const router = express.Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post('/:id', [
|
router.post('/:id', [
|
||||||
...this.categoryValidationSchema,
|
...this.categoryValidationSchema,
|
||||||
...this.specificCategoryValidationSchema,
|
...this.specificCategoryValidationSchema,
|
||||||
],
|
],
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateParentCategoryExistance),
|
asyncMiddleware(this.validateParentCategoryExistance),
|
||||||
asyncMiddleware(this.validateSellAccountExistance),
|
asyncMiddleware(this.validateSellAccountExistance),
|
||||||
asyncMiddleware(this.validateCostAccountExistance),
|
asyncMiddleware(this.validateCostAccountExistance),
|
||||||
@@ -42,7 +41,6 @@ export default class ItemsCategoriesController {
|
|||||||
router.post('/',
|
router.post('/',
|
||||||
this.categoryValidationSchema,
|
this.categoryValidationSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateParentCategoryExistance),
|
asyncMiddleware(this.validateParentCategoryExistance),
|
||||||
asyncMiddleware(this.validateSellAccountExistance),
|
asyncMiddleware(this.validateSellAccountExistance),
|
||||||
asyncMiddleware(this.validateCostAccountExistance),
|
asyncMiddleware(this.validateCostAccountExistance),
|
||||||
@@ -52,28 +50,24 @@ export default class ItemsCategoriesController {
|
|||||||
router.delete('/bulk',
|
router.delete('/bulk',
|
||||||
this.categoriesBulkValidationSchema,
|
this.categoriesBulkValidationSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateCategoriesIdsExistance),
|
asyncMiddleware(this.validateCategoriesIdsExistance),
|
||||||
asyncMiddleware(this.bulkDeleteCategories),
|
asyncMiddleware(this.bulkDeleteCategories),
|
||||||
);
|
);
|
||||||
router.delete('/:id',
|
router.delete('/:id',
|
||||||
this.specificCategoryValidationSchema,
|
this.specificCategoryValidationSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateItemCategoryExistance),
|
asyncMiddleware(this.validateItemCategoryExistance),
|
||||||
asyncMiddleware(this.deleteItem),
|
asyncMiddleware(this.deleteItem),
|
||||||
);
|
);
|
||||||
router.get('/:id',
|
router.get('/:id',
|
||||||
this.specificCategoryValidationSchema,
|
this.specificCategoryValidationSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateItemCategoryExistance),
|
asyncMiddleware(this.validateItemCategoryExistance),
|
||||||
asyncMiddleware(this.getCategory)
|
asyncMiddleware(this.getCategory)
|
||||||
);
|
);
|
||||||
router.get('/',
|
router.get('/',
|
||||||
this.categoriesListValidationSchema,
|
this.categoriesListValidationSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.getList)
|
asyncMiddleware(this.getList)
|
||||||
);
|
);
|
||||||
return router;
|
return router;
|
||||||
@@ -164,7 +158,7 @@ export default class ItemsCategoriesController {
|
|||||||
*/
|
*/
|
||||||
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
|
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
|
||||||
const { Account, AccountType } = req.models;
|
const { Account, AccountType } = req.models;
|
||||||
const category: IItemCategoryOTD = { ...req.body };
|
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||||
|
|
||||||
if (category.costAccountId) {
|
if (category.costAccountId) {
|
||||||
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
|
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) {
|
async validateSellAccountExistance(req: Request, res: Response, next: Function) {
|
||||||
const { Account, AccountType } = req.models;
|
const { Account, AccountType } = req.models;
|
||||||
const category: IItemCategoryOTD = { ...req.body };
|
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||||
|
|
||||||
if (category.sellAccountId) {
|
if (category.sellAccountId) {
|
||||||
const incomeType = await AccountType.query().findOne('key', 'income');
|
const incomeType = await AccountType.query().findOne('key', 'income');
|
||||||
@@ -218,7 +212,7 @@ export default class ItemsCategoriesController {
|
|||||||
*/
|
*/
|
||||||
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
|
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
|
||||||
const { Account, AccountType } = req.models;
|
const { Account, AccountType } = req.models;
|
||||||
const category: IItemCategoryOTD = { ...req.body };
|
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||||
|
|
||||||
if (category.inventoryAccountId) {
|
if (category.inventoryAccountId) {
|
||||||
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
|
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
|
||||||
@@ -244,7 +238,7 @@ export default class ItemsCategoriesController {
|
|||||||
* @param {Function} next
|
* @param {Function} next
|
||||||
*/
|
*/
|
||||||
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
|
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
|
||||||
const category: IItemCategory = { ...req.body };
|
const category: IItemCategory = this.matchedBodyData(req);
|
||||||
const { ItemCategory } = req.models;
|
const { ItemCategory } = req.models;
|
||||||
|
|
||||||
if (category.parentCategoryId) {
|
if (category.parentCategoryId) {
|
||||||
@@ -290,7 +284,7 @@ export default class ItemsCategoriesController {
|
|||||||
*/
|
*/
|
||||||
async newCategory(req: Request, res: Response) {
|
async newCategory(req: Request, res: Response) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const category: IItemCategory = { ...req.body };
|
const category: IItemCategory = this.matchedBodyData(req);
|
||||||
const { ItemCategory } = req.models;
|
const { ItemCategory } = req.models;
|
||||||
|
|
||||||
const storedCategory = await ItemCategory.query().insert({
|
const storedCategory = await ItemCategory.query().insert({
|
||||||
@@ -308,7 +302,7 @@ export default class ItemsCategoriesController {
|
|||||||
*/
|
*/
|
||||||
async editCategory(req: Request, res: Response) {
|
async editCategory(req: Request, res: Response) {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const category: IItemCategory = { ...req.body };
|
const category: IItemCategory = this.matchedBodyData(req);
|
||||||
const { ItemCategory } = req.models;
|
const { ItemCategory } = req.models;
|
||||||
|
|
||||||
const updateItemCategory = await ItemCategory.query()
|
const updateItemCategory = await ItemCategory.query()
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
query,
|
query,
|
||||||
validationResult,
|
validationResult,
|
||||||
} from 'express-validator';
|
} from 'express-validator';
|
||||||
|
import Container from 'typedi';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { difference } from 'lodash';
|
import { difference } from 'lodash';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import Logger from '@/services/Logger';
|
|
||||||
|
|
||||||
const fsPromises = fs.promises;
|
const fsPromises = fs.promises;
|
||||||
|
|
||||||
@@ -70,6 +70,8 @@ export default {
|
|||||||
// check('attachment').exists(),
|
// check('attachment').exists(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
|
const Logger = Container.get('logger');
|
||||||
|
|
||||||
if (!req.files.attachment) {
|
if (!req.files.attachment) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
|
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
|
||||||
@@ -93,9 +95,9 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
|
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
|
||||||
Logger.log('info', 'Attachment uploaded successfully');
|
Logger.info('[attachment] uploaded successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.log('info', 'Attachment uploading failed.', { error });
|
Logger.info('[attachment] uploading failed.', { error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = await Media.query().insert({
|
const media = await Media.query().insert({
|
||||||
@@ -114,6 +116,7 @@ export default {
|
|||||||
query('ids.*').exists().isNumeric().toInt(),
|
query('ids.*').exists().isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
|
const Logger = Container.get('logger');
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
@@ -142,12 +145,12 @@ export default {
|
|||||||
});
|
});
|
||||||
await Promise.all(unlinkOpers).then((resolved) => {
|
await Promise.all(unlinkOpers).then((resolved) => {
|
||||||
resolved.forEach(() => {
|
resolved.forEach(() => {
|
||||||
Logger.log('error', 'Attachment file has been deleted.');
|
Logger.info('[attachment] file has been deleted.');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((errors) => {
|
.catch((errors) => {
|
||||||
errors.forEach((error) => {
|
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 { Inject } from 'typedi';
|
||||||
import { Plan } from '@/system/models';
|
import { Plan } from '@/system/models';
|
||||||
|
import BaseController from '@/http/controllers/BaseController';
|
||||||
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||||
|
|
||||||
export default class PaymentMethodController {
|
export default class PaymentMethodController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
subscriptionService: SubscriptionService;
|
subscriptionService: SubscriptionService;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ export default class PaymentMethodController {
|
|||||||
* @return {Response|void}
|
* @return {Response|void}
|
||||||
*/
|
*/
|
||||||
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
|
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();
|
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
||||||
|
|
||||||
if (!foundPlan) {
|
if (!foundPlan) {
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Container, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { check, param, query, ValidationSchema } from 'express-validator';
|
import { check, param, query, ValidationSchema } from 'express-validator';
|
||||||
import { Voucher, Plan } from '@/system/models';
|
import { Voucher, Plan } from '@/system/models';
|
||||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
|
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
|
||||||
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
|
|
||||||
import {
|
import {
|
||||||
NotAllowedChangeSubscriptionPlan
|
NotAllowedChangeSubscriptionPlan
|
||||||
} from '@/exceptions';
|
} from '@/exceptions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class PaymentViaVoucherController extends PaymentMethodController {
|
export default class PaymentViaVoucherController extends PaymentMethodController {
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +24,6 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
|||||||
'/payment',
|
'/payment',
|
||||||
this.paymentViaVoucherSchema,
|
this.paymentViaVoucherSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
|
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
|
||||||
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
||||||
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
|
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
|
||||||
@@ -48,7 +49,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async validateVoucherCodeExistance(req: Request, res: Response, next: Function) {
|
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()
|
const foundVoucher = await Voucher.query()
|
||||||
.modify('filterActiveVoucher')
|
.modify('filterActiveVoucher')
|
||||||
@@ -70,7 +72,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
|||||||
* @param {Function} next
|
* @param {Function} next
|
||||||
*/
|
*/
|
||||||
async validateVoucherAndPlan(req: Request, res: Response, next: Function) {
|
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 voucher = await Voucher.query().findOne('voucher_code', voucherCode);
|
||||||
const plan = await Plan.query().findOne('slug', planSlug);
|
const plan = await Plan.query().findOne('slug', planSlug);
|
||||||
@@ -90,11 +93,12 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
|||||||
* @return {Response}
|
* @return {Response}
|
||||||
*/
|
*/
|
||||||
async paymentViaVoucher(req: Request, res: Response, next: Function) {
|
async paymentViaVoucher(req: Request, res: Response, next: Function) {
|
||||||
const { planSlug, voucherCode } = req.body;
|
const { planSlug, voucherCode } = this.matchedBodyData(req);
|
||||||
const { tenant } = req;
|
const { tenant } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
|
await this.subscriptionService
|
||||||
|
.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'PAYMENT.SUCCESSFULLY.MADE',
|
type: 'PAYMENT.SUCCESSFULLY.MADE',
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Router, Request, Response } from 'express'
|
import { Router, Request, Response } from 'express'
|
||||||
import { repeat, times, orderBy } from 'lodash';
|
import { check, oneOf, ValidationChain } from 'express-validator';
|
||||||
import { check, oneOf, param, query, ValidationChain } from 'express-validator';
|
import { Service, Inject } from 'typedi';
|
||||||
import { Container, Service, Inject } from 'typedi';
|
|
||||||
import { Voucher, Plan } from '@/system/models';
|
import { Voucher, Plan } from '@/system/models';
|
||||||
|
import BaseController from '@/http/controllers/BaseController';
|
||||||
import VoucherService from '@/services/Payment/Voucher';
|
import VoucherService from '@/services/Payment/Voucher';
|
||||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import PrettierMiddleware from '@/http/middleware/prettierMiddleware';
|
|
||||||
import { IVouchersFilter } from '@/interfaces';
|
import { IVouchersFilter } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class VouchersController {
|
export default class VouchersController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
voucherService: VoucherService;
|
voucherService: VoucherService;
|
||||||
|
|
||||||
@@ -24,14 +23,12 @@ export default class VouchersController {
|
|||||||
'/generate',
|
'/generate',
|
||||||
this.generateVoucherSchema,
|
this.generateVoucherSchema,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validatePlanExistance.bind(this)),
|
asyncMiddleware(this.validatePlanExistance.bind(this)),
|
||||||
asyncMiddleware(this.generateVoucher.bind(this)),
|
asyncMiddleware(this.generateVoucher.bind(this)),
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/disable/:voucherId',
|
'/disable/:voucherId',
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
||||||
asyncMiddleware(this.validateNotDisabledVoucher.bind(this)),
|
asyncMiddleware(this.validateNotDisabledVoucher.bind(this)),
|
||||||
asyncMiddleware(this.disableVoucher.bind(this)),
|
asyncMiddleware(this.disableVoucher.bind(this)),
|
||||||
@@ -40,18 +37,15 @@ export default class VouchersController {
|
|||||||
'/send',
|
'/send',
|
||||||
this.sendVoucherSchemaValidation,
|
this.sendVoucherSchemaValidation,
|
||||||
validateMiddleware,
|
validateMiddleware,
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.sendVoucher.bind(this)),
|
asyncMiddleware(this.sendVoucher.bind(this)),
|
||||||
);
|
);
|
||||||
router.delete(
|
router.delete(
|
||||||
'/:voucherId',
|
'/:voucherId',
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
||||||
asyncMiddleware(this.deleteVoucher.bind(this)),
|
asyncMiddleware(this.deleteVoucher.bind(this)),
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
PrettierMiddleware,
|
|
||||||
asyncMiddleware(this.listVouchers.bind(this)),
|
asyncMiddleware(this.listVouchers.bind(this)),
|
||||||
);
|
);
|
||||||
return router;
|
return router;
|
||||||
@@ -106,7 +100,8 @@ export default class VouchersController {
|
|||||||
* @param {Function} next
|
* @param {Function} next
|
||||||
*/
|
*/
|
||||||
async validatePlanExistance(req: Request, res: Response, next: Function) {
|
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);
|
const foundPlan = await Plan.query().findById(planId);
|
||||||
|
|
||||||
if (!foundPlan) {
|
if (!foundPlan) {
|
||||||
@@ -124,7 +119,9 @@ export default class VouchersController {
|
|||||||
* @param {Function}
|
* @param {Function}
|
||||||
*/
|
*/
|
||||||
async validateVoucherExistance(req: Request, res: Response, next: 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);
|
const foundVoucher = await Voucher.query().findById(voucherId);
|
||||||
|
|
||||||
if (!foundVoucher) {
|
if (!foundVoucher) {
|
||||||
@@ -160,7 +157,7 @@ export default class VouchersController {
|
|||||||
* @return {Response}
|
* @return {Response}
|
||||||
*/
|
*/
|
||||||
async generateVoucher(req: Request, res: Response, next: Function) {
|
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 {
|
try {
|
||||||
await this.voucherService.generateVouchers(
|
await this.voucherService.generateVouchers(
|
||||||
@@ -211,7 +208,7 @@ export default class VouchersController {
|
|||||||
* @return {Response}
|
* @return {Response}
|
||||||
*/
|
*/
|
||||||
async sendVoucher(req: Request, res: 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()
|
const voucher = await Voucher.query()
|
||||||
.modify('filterActiveVoucher')
|
.modify('filterActiveVoucher')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
|||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||||
|
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
|
||||||
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
|
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -13,6 +14,7 @@ export default class SubscriptionController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(JWTAuth);
|
router.use(JWTAuth);
|
||||||
|
router.use(AttachCurrentTenantUser);
|
||||||
router.use(TenancyMiddleware);
|
router.use(TenancyMiddleware);
|
||||||
|
|
||||||
router.use('/voucher', Container.get(PaymentViaVoucherController).router());
|
router.use('/voucher', Container.get(PaymentViaVoucherController).router());
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { Container } from 'typedi';
|
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 Authentication from '@/http/controllers/Authentication';
|
||||||
import InviteUsers from '@/http/controllers/InviteUsers';
|
import InviteUsers from '@/http/controllers/InviteUsers';
|
||||||
|
import Organization from '@/http/controllers/Organization';
|
||||||
import Users from '@/http/controllers/Users';
|
import Users from '@/http/controllers/Users';
|
||||||
import Items from '@/http/controllers/Items';
|
import Items from '@/http/controllers/Items';
|
||||||
import ItemCategories from '@/http/controllers/ItemCategories';
|
import ItemCategories from '@/http/controllers/ItemCategories';
|
||||||
@@ -11,7 +22,7 @@ import Views from '@/http/controllers/Views';
|
|||||||
import Accounting from '@/http/controllers/Accounting';
|
import Accounting from '@/http/controllers/Accounting';
|
||||||
import FinancialStatements from '@/http/controllers/FinancialStatements';
|
import FinancialStatements from '@/http/controllers/FinancialStatements';
|
||||||
import Expenses from '@/http/controllers/Expenses';
|
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 Currencies from '@/http/controllers/Currencies';
|
||||||
import Customers from '@/http/controllers/Customers';
|
import Customers from '@/http/controllers/Customers';
|
||||||
import Vendors from '@/http/controllers/Vendors';
|
import Vendors from '@/http/controllers/Vendors';
|
||||||
@@ -20,19 +31,16 @@ import Purchases from '@/http/controllers/Purchases';
|
|||||||
import Resources from './controllers/Resources';
|
import Resources from './controllers/Resources';
|
||||||
import ExchangeRates from '@/http/controllers/ExchangeRates';
|
import ExchangeRates from '@/http/controllers/ExchangeRates';
|
||||||
import Media from '@/http/controllers/Media';
|
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 Ping from '@/http/controllers/Ping';
|
||||||
import Agendash from '@/http/controllers/Agendash';
|
import Agendash from '@/http/controllers/Agendash';
|
||||||
import Subscription from '@/http/controllers/Subscription';
|
import Subscription from '@/http/controllers/Subscription';
|
||||||
import VouchersController from '@/http/controllers/Subscription/Vouchers';
|
import VouchersController from '@/http/controllers/Subscription/Vouchers';
|
||||||
|
|
||||||
import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjection';
|
|
||||||
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
|
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
app.use('/api/auth', Container.get(Authentication).router());
|
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/vouchers', Container.get(VouchersController).router());
|
||||||
app.use('/api/subscription', Container.get(Subscription).router());
|
app.use('/api/subscription', Container.get(Subscription).router());
|
||||||
app.use('/api/ping', Container.get(Ping).router());
|
app.use('/api/ping', Container.get(Ping).router());
|
||||||
@@ -40,20 +48,23 @@ export default (app) => {
|
|||||||
const dashboard = express.Router();
|
const dashboard = express.Router();
|
||||||
|
|
||||||
dashboard.use(JWTAuth);
|
dashboard.use(JWTAuth);
|
||||||
|
dashboard.use(AttachCurrentTenantUser)
|
||||||
dashboard.use(TenancyMiddleware);
|
dashboard.use(TenancyMiddleware);
|
||||||
dashboard.use(SubscriptionMiddleware('main'));
|
dashboard.use(SubscriptionMiddleware('main'));
|
||||||
|
dashboard.use(EnsureTenantIsInitialized);
|
||||||
|
dashboard.use(SettingsMiddleware);
|
||||||
|
|
||||||
dashboard.use('/api/currencies', Currencies.router());
|
|
||||||
dashboard.use('/api/users', Users.router());
|
dashboard.use('/api/users', Users.router());
|
||||||
|
dashboard.use('/api/currencies', Currencies.router());
|
||||||
dashboard.use('/api/accounts', Accounts.router());
|
dashboard.use('/api/accounts', Accounts.router());
|
||||||
dashboard.use('/api/account_types', AccountTypes.router());
|
dashboard.use('/api/account_types', AccountTypes.router());
|
||||||
dashboard.use('/api/accounting', Accounting.router());
|
dashboard.use('/api/accounting', Accounting.router());
|
||||||
dashboard.use('/api/views', Views.router());
|
dashboard.use('/api/views', Views.router());
|
||||||
dashboard.use('/api/items', Container.get(Items).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/expenses', Expenses.router());
|
||||||
dashboard.use('/api/financial_statements', FinancialStatements.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/sales', Sales.router());
|
||||||
dashboard.use('/api/customers', Customers.router());
|
dashboard.use('/api/customers', Customers.router());
|
||||||
dashboard.use('/api/vendors', Vendors.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';
|
import { Container } from 'typedi';
|
||||||
|
import TenantsManager from '@/system/TenantsManager';
|
||||||
function loadModelsFromDirectory() {
|
import tenantModelsLoader from '@/loaders/tenantModels';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (req, res, next) => {
|
export default async (req, res, next) => {
|
||||||
|
const Logger = Container.get('logger');
|
||||||
const organizationId = req.headers['organization-id'] || req.query.organization;
|
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) {
|
if (!organizationId) {
|
||||||
return notFoundOrganization();
|
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.
|
// When the given organization id not found on the system storage.
|
||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
@@ -36,30 +31,22 @@ export default async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
// When user tenant not match the given organization id.
|
// When user tenant not match the given organization id.
|
||||||
if (tenant.id !== req.user.tenantId) {
|
if (tenant.id !== req.user.tenantId) {
|
||||||
|
Logger.info('[tenancy_middleware] authorized user not match org. tenant.');
|
||||||
return res.boom.unauthorized();
|
return res.boom.unauthorized();
|
||||||
}
|
}
|
||||||
const knex = TenantsManager.knexInstance(organizationId);
|
const models = tenantModelsLoader(tenantKnex);
|
||||||
const models = loadModelsFromDirectory();
|
|
||||||
|
|
||||||
TenantModel.knexBinded = knex;
|
req.knex = tenantKnex;
|
||||||
|
|
||||||
req.knex = knex;
|
|
||||||
req.organizationId = organizationId;
|
req.organizationId = organizationId;
|
||||||
req.tenant = tenant;
|
req.tenant = tenant;
|
||||||
req.tenantId = tenant.id;
|
req.models = models;
|
||||||
req.models = {
|
|
||||||
...Object.values(models).reduce((acc, model) => {
|
const tenantContainer = Container.of(`tenant-${tenant.id}`);
|
||||||
if (typeof model.resource.default !== 'undefined' &&
|
|
||||||
typeof model.resource.default.requestModel === 'function' &&
|
tenantContainer.set('models', models);
|
||||||
model.resource.default.requestModel() &&
|
tenantContainer.set('knex', tenantKnex);
|
||||||
model.name !== 'TenantModel') {
|
tenantContainer.set('tenant', tenant);
|
||||||
acc[model.name] = model.resource.default.bindKnex(knex);
|
Logger.info('[tenancy_middleware] tenant dependencies injected to container.');
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
};
|
|
||||||
Container.of(`tenant-${tenant.id}`).set('models', {
|
|
||||||
...req.models,
|
|
||||||
});
|
|
||||||
next();
|
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 jwt from 'jsonwebtoken';
|
||||||
import SystemUser from '@/system/models/SystemUser';
|
import config from '@/../config/config';
|
||||||
|
|
||||||
const authMiddleware = (req, res, next) => {
|
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 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(); }
|
if (!token) { return onError(); }
|
||||||
|
|
||||||
const verify = new Promise((resolve, reject) => {
|
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) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
req.user = await SystemUser.query().findById(decoded._id);
|
|
||||||
|
|
||||||
if (!req.user) {
|
|
||||||
return onError();
|
|
||||||
}
|
|
||||||
resolve(decoded);
|
resolve(decoded);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
verify.then(onSuccess).catch(onError);
|
||||||
verify.then(() => { next(); }).catch(onError);
|
|
||||||
};
|
};
|
||||||
export default authMiddleware;
|
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[],
|
||||||
|
};
|
||||||
@@ -7,3 +7,7 @@ export interface ISystemUser {
|
|||||||
export interface ISystemUserDTO {
|
export interface ISystemUserDTO {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IInviteUserInput {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -34,7 +34,18 @@ import {
|
|||||||
import {
|
import {
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
ISystemUserDTO,
|
ISystemUserDTO,
|
||||||
|
IInviteUserInput,
|
||||||
} from './User';
|
} from './User';
|
||||||
|
import {
|
||||||
|
IMetadata,
|
||||||
|
IMetaQuery,
|
||||||
|
IMetableStore,
|
||||||
|
IMetableStoreStorage,
|
||||||
|
} from './Metable';
|
||||||
|
import {
|
||||||
|
IOptionDTO,
|
||||||
|
IOptionsDTO,
|
||||||
|
} from './Options';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IBillPaymentEntry,
|
IBillPaymentEntry,
|
||||||
@@ -69,4 +80,13 @@ export {
|
|||||||
IRegisterDTO,
|
IRegisterDTO,
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
ISystemUserDTO,
|
ISystemUserDTO,
|
||||||
|
IInviteUserInput,
|
||||||
|
|
||||||
|
IMetadata,
|
||||||
|
IMetaQuery,
|
||||||
|
IMetableStore,
|
||||||
|
IMetableStoreStorage,
|
||||||
|
|
||||||
|
IOptionDTO,
|
||||||
|
IOptionsDTO,
|
||||||
};
|
};
|
||||||
@@ -25,15 +25,15 @@ export default class ComputeItemCostJob {
|
|||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
|
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 {
|
try {
|
||||||
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
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();
|
done();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(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);
|
done(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,9 +58,8 @@ export default class ComputeItemCostJob {
|
|||||||
async onJobFinished() {
|
async onJobFinished() {
|
||||||
const agenda = Container.get('agenda');
|
const agenda = Container.get('agenda');
|
||||||
const startingDate = this.startingDate;
|
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) {
|
if (this.depends === 0) {
|
||||||
this.startingDate = null;
|
this.startingDate = null;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import SubscriptionService from '@/services/Subscription/Subscription';
|
|||||||
|
|
||||||
export default class MailNotificationSubscribeEnd {
|
export default class MailNotificationSubscribeEnd {
|
||||||
/**
|
/**
|
||||||
*
|
* Job handler.
|
||||||
* @param {Job} job -
|
* @param {Job} job -
|
||||||
*/
|
*/
|
||||||
handler(job) {
|
handler(job) {
|
||||||
@@ -12,15 +12,15 @@ export default class MailNotificationSubscribeEnd {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const Logger = Container.get('logger');
|
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 {
|
try {
|
||||||
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
||||||
phoneNumber, remainingDays,
|
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) {
|
} 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);
|
done(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ export default class MailNotificationTrialEnd {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const Logger = Container.get('logger');
|
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 {
|
try {
|
||||||
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
||||||
phoneNumber, remainingDays,
|
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) {
|
} 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);
|
done(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,28 @@
|
|||||||
import fs from 'fs';
|
import { Container, Inject } from 'typedi';
|
||||||
import path from 'path';
|
import AuthenticationService from '@/services/Authentication';
|
||||||
import Mustache from 'mustache';
|
|
||||||
import { Container } from 'typedi';
|
export default class WelcomeEmailJob {
|
||||||
|
@Inject()
|
||||||
|
authService: AuthenticationService;
|
||||||
|
|
||||||
export default class ResetPasswordMailJob {
|
|
||||||
/**
|
/**
|
||||||
*
|
* Handle send welcome mail job.
|
||||||
* @param job
|
* @param {Job} job
|
||||||
* @param done
|
* @param {Function} done
|
||||||
*/
|
*/
|
||||||
handler(job, done) {
|
public async handler(job, done: Function): Promise<void> {
|
||||||
const { user, token } = job.attrs.data;
|
const { email, organizationName, firstName } = job.attrs.data;
|
||||||
|
|
||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const Mail = Container.get('mail');
|
|
||||||
|
|
||||||
const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html');
|
Logger.info(`Send reset password mail - started: ${job.attrs.data}`);
|
||||||
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 = {
|
try {
|
||||||
to: user.email,
|
await this.authService.mailMessages.sendResetPasswordMessage();
|
||||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
Logger.info(`Send reset password mail - finished: ${job.attrs.data}`);
|
||||||
subject: 'Bigcapital - Password Reset',
|
done()
|
||||||
html: rendered,
|
} catch (error) {
|
||||||
};
|
Logger.info(`Send reset password mail - error: ${job.attrs.data}, error: ${error}`);
|
||||||
Mail.sendMail(mailOptions, (error) => {
|
done(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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,15 +13,15 @@ export default class SMSNotificationSubscribeEnd {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const Logger = Container.get('logger');
|
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 {
|
try {
|
||||||
subscriptionService.smsMessages.sendRemainingSubscriptionPeriod(
|
subscriptionService.smsMessages.sendRemainingSubscriptionPeriod(
|
||||||
phoneNumber, remainingDays,
|
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) {
|
} 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);
|
done(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ export default class SMSNotificationTrialEnd {
|
|||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const Logger = Container.get('logger');
|
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 {
|
try {
|
||||||
subscriptionService.smsMessages.sendRemainingTrialPeriod(
|
subscriptionService.smsMessages.sendRemainingTrialPeriod(
|
||||||
phoneNumber, remainingDays,
|
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) {
|
} 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);
|
done(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import VoucherService from '@/services/Payment/Voucher';
|
|||||||
|
|
||||||
export default class SendVoucherViaPhoneJob {
|
export default class SendVoucherViaPhoneJob {
|
||||||
public async handler(job, done: Function): Promise<void> {
|
public async handler(job, done: Function): Promise<void> {
|
||||||
|
const { phoneNumber, voucherCode } = job.attrs.data;
|
||||||
|
|
||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const voucherService = Container.get(VoucherService);
|
const voucherService = Container.get(VoucherService);
|
||||||
const { phoneNumber, voucherCode } = job.attrs.data;
|
|
||||||
|
|
||||||
Logger.debug(`Send voucher via phone number - started: ${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 {
|
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 { Container, Inject } from 'typedi';
|
||||||
import Mustache from 'mustache';
|
import AuthenticationService from '@/services/Authentication';
|
||||||
import path from 'path';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
export default class WelcomeEmailJob {
|
export default class WelcomeEmailJob {
|
||||||
|
@Inject()
|
||||||
|
authService: AuthenticationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Handle send welcome mail job.
|
||||||
* @param {Job} job
|
* @param {Job} job
|
||||||
* @param {Function} done
|
* @param {Function} done
|
||||||
*/
|
*/
|
||||||
public async handler(job, done: Function): Promise<void> {
|
public async handler(job, done: Function): Promise<void> {
|
||||||
const { email, organizationName, firstName } = job.attrs.data;
|
const { email, organizationName, firstName } = job.attrs.data;
|
||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const Mail = Container.get('mail');
|
|
||||||
|
|
||||||
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
|
Logger.info(`Send welcome mail message - started: ${job.attrs.data}`);
|
||||||
const template = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const rendered = Mustache.render(template, {
|
try {
|
||||||
email, organizationName, firstName,
|
await this.authService.mailMessages.sendWelcomeMessage();
|
||||||
});
|
Logger.info(`Send welcome mail message - finished: ${job.attrs.data}`);
|
||||||
const mailOptions = {
|
done()
|
||||||
to: email,
|
} catch (error) {
|
||||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
Logger.info(`Send welcome mail message - error: ${job.attrs.data}, error: ${error}`);
|
||||||
subject: 'Welcome to Bigcapital',
|
done(error);
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ export default class WriteInvoicesJournalEntries {
|
|||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const { startingDate } = job.attrs.data;
|
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 {
|
try {
|
||||||
await SalesInvoicesCost.writeJournalEntries(startingDate, true);
|
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();
|
done();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(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);
|
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 { Container } from 'typedi';
|
||||||
import LoggerInstance from '@/services/Logger';
|
import LoggerInstance from '@/loaders/Logger';
|
||||||
import agendaFactory from '@/loaders/agenda';
|
import agendaFactory from '@/loaders/agenda';
|
||||||
import SmsClientLoader from '@/loaders/smsClient';
|
import SmsClientLoader from '@/loaders/smsClient';
|
||||||
import mailInstance from '@/loaders/mail';
|
import mailInstance from '@/loaders/mail';
|
||||||
|
import dbManagerFactory from '@/loaders/dbManager';
|
||||||
|
|
||||||
export default ({ mongoConnection, knex }) => {
|
export default ({ mongoConnection, knex }) => {
|
||||||
try {
|
try {
|
||||||
const agendaInstance = agendaFactory({ mongoConnection });
|
const agendaInstance = agendaFactory({ mongoConnection });
|
||||||
const smsClientInstance = SmsClientLoader();
|
const smsClientInstance = SmsClientLoader();
|
||||||
|
const dbManager = dbManagerFactory();
|
||||||
|
|
||||||
Container.set('agenda', agendaInstance);
|
Container.set('agenda', agendaInstance);
|
||||||
LoggerInstance.info('Agenda has been injected into container');
|
LoggerInstance.info('Agenda has been injected into container');
|
||||||
@@ -24,6 +26,9 @@ export default ({ mongoConnection, knex }) => {
|
|||||||
Container.set('mail', mailInstance);
|
Container.set('mail', mailInstance);
|
||||||
LoggerInstance.info('Mail instance has been injected into container');
|
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 };
|
return { agenda: agendaInstance };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LoggerInstance.error('Error on dependency injector loader: %o', 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 mongooseLoader from '@/loaders/mongoose';
|
||||||
import jobsLoader from '@/loaders/jobs';
|
import jobsLoader from '@/loaders/jobs';
|
||||||
import expressLoader from '@/loaders/express';
|
import expressLoader from '@/loaders/express';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Agenda from 'agenda';
|
import Agenda from 'agenda';
|
||||||
import WelcomeEmailJob from '@/Jobs/welcomeEmail';
|
import WelcomeEmailJob from '@/jobs/WelcomeEmail';
|
||||||
import ResetPasswordMailJob from '@/Jobs/ResetPasswordMail';
|
import WelcomeSMSJob from '@/jobs/WelcomeSMS';
|
||||||
import ComputeItemCost from '@/Jobs/ComputeItemCost';
|
import ResetPasswordMailJob from '@/jobs/ResetPasswordMail';
|
||||||
|
import ComputeItemCost from '@/jobs/ComputeItemCost';
|
||||||
import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries';
|
import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries';
|
||||||
import SendVoucherViaPhoneJob from '@/jobs/SendVoucherPhone';
|
import SendVoucherViaPhoneJob from '@/jobs/SendVoucherPhone';
|
||||||
import SendVoucherViaEmailJob from '@/jobs/SendVoucherEmail';
|
import SendVoucherViaEmailJob from '@/jobs/SendVoucherEmail';
|
||||||
@@ -12,16 +13,24 @@ import SendMailNotificationTrialEnd from '@/jobs/MailNotificationTrialEnd';
|
|||||||
import UserInviteMailJob from '@/jobs/UserInviteMail';
|
import UserInviteMailJob from '@/jobs/UserInviteMail';
|
||||||
|
|
||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
|
// Welcome mail and SMS message.
|
||||||
agenda.define(
|
agenda.define(
|
||||||
'welcome-email',
|
'welcome-email',
|
||||||
{ priority: 'high' },
|
{ priority: 'high' },
|
||||||
new WelcomeEmailJob().handler,
|
new WelcomeEmailJob().handler,
|
||||||
);
|
);
|
||||||
|
agenda.define(
|
||||||
|
'welcome-sms',
|
||||||
|
{ priority: 'high' },
|
||||||
|
new WelcomeSMSJob().handler
|
||||||
|
);
|
||||||
|
// Reset password mail.
|
||||||
agenda.define(
|
agenda.define(
|
||||||
'reset-password-mail',
|
'reset-password-mail',
|
||||||
{ priority: 'high' },
|
{ priority: 'high' },
|
||||||
new ResetPasswordMailJob().handler,
|
new ResetPasswordMailJob().handler,
|
||||||
);
|
);
|
||||||
|
// User invite mail.
|
||||||
agenda.define(
|
agenda.define(
|
||||||
'user-invite-mail',
|
'user-invite-mail',
|
||||||
{ priority: 'high' },
|
{ 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 TenantModel from '@/models/TenantModel';
|
||||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
|
||||||
import definedOptions from '@/data/options';
|
import definedOptions from '@/data/options';
|
||||||
|
|
||||||
|
|
||||||
@@ -11,27 +10,6 @@ export default class Option extends TenantModel {
|
|||||||
return 'options';
|
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.
|
* Validates the given options is defined or either not.
|
||||||
* @param {Array} options
|
* @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() {
|
static get relationMappings() {
|
||||||
const View = require('@/models/View');
|
const View = require('@/models/View');
|
||||||
const ResourceField = require('@/models/ResourceField');
|
const ResourceField = require('@/models/ResourceField');
|
||||||
const Permission = require('@/models/Permission');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@@ -57,22 +56,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) {
|
|||||||
to: 'resource_fields.resourceId',
|
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 TenantModel from '@/models/TenantModel';
|
||||||
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
|
|
||||||
|
|
||||||
export default class ResourceFieldMetadata extends TenantModel {
|
export default class ResourceFieldMetadata extends TenantModel {
|
||||||
/**
|
/**
|
||||||
@@ -8,11 +7,4 @@ export default class ResourceFieldMetadata extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'resource_custom_fields_metadata';
|
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 bcrypt from 'bcryptjs';
|
||||||
import { Model } from 'objection';
|
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
// import PermissionsService from '@/services/PermissionsService';
|
|
||||||
|
|
||||||
export default class TenantUser extends TenantModel {
|
export default class TenantUser extends TenantModel {
|
||||||
/**
|
/**
|
||||||
@@ -25,28 +23,6 @@ export default class TenantUser extends TenantModel {
|
|||||||
return ['createdAt', 'updatedAt'];
|
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.
|
* Verify the password of the user.
|
||||||
* @param {String} password - The given password.
|
* @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 uniqid from 'uniqid';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import {
|
import {
|
||||||
EventDispatcher
|
EventDispatcher,
|
||||||
EventDispatcherInterface
|
EventDispatcherInterface,
|
||||||
} from '@/decorators/eventDispatcher';
|
} from '@/decorators/eventDispatcher';
|
||||||
import {
|
import {
|
||||||
SystemUser,
|
SystemUser,
|
||||||
@@ -22,6 +22,8 @@ import { hashPassword } from '@/utils';
|
|||||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||||
import config from '@/../config/config';
|
import config from '@/../config/config';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages';
|
||||||
|
import AuthenticationSMSMessages from '@/services/Authentication/AuthenticationSMSMessages';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationService {
|
export default class AuthenticationService {
|
||||||
@@ -34,6 +36,12 @@ export default class AuthenticationService {
|
|||||||
@EventDispatcher()
|
@EventDispatcher()
|
||||||
eventDispatcher: EventDispatcherInterface;
|
eventDispatcher: EventDispatcherInterface;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
smsMessages: AuthenticationSMSMessages;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
mailMessages: AuthenticationMailMessages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signin and generates JWT token.
|
* Signin and generates JWT token.
|
||||||
* @throws {ServiceError}
|
* @throws {ServiceError}
|
||||||
@@ -70,6 +78,7 @@ export default class AuthenticationService {
|
|||||||
|
|
||||||
this.logger.info('[login] Logging success.', { user, token });
|
this.logger.info('[login] Logging success.', { user, token });
|
||||||
|
|
||||||
|
// Triggers `onLogin` event.
|
||||||
this.eventDispatcher.dispatch(events.auth.login, {
|
this.eventDispatcher.dispatch(events.auth.login, {
|
||||||
emailOrPhone, password,
|
emailOrPhone, password,
|
||||||
});
|
});
|
||||||
@@ -191,6 +200,7 @@ export default class AuthenticationService {
|
|||||||
const passwordReset = await PasswordReset.query().insert({ email, token });
|
const passwordReset = await PasswordReset.query().insert({ email, token });
|
||||||
const user = await SystemUser.query().findOne('email', email);
|
const user = await SystemUser.query().findOne('email', email);
|
||||||
|
|
||||||
|
// Triggers `onSendResetPassword` event.
|
||||||
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token });
|
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token });
|
||||||
|
|
||||||
return passwordReset;
|
return passwordReset;
|
||||||
@@ -225,25 +235,26 @@ export default class AuthenticationService {
|
|||||||
// Delete the reset password token.
|
// Delete the reset password token.
|
||||||
await PasswordReset.query().where('email', user.email).delete();
|
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.');
|
this.logger.info('[reset_password] reset password success.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates JWT token for the given user.
|
* Generates JWT token for the given user.
|
||||||
* @param {IUser} user
|
* @param {ISystemUser} user
|
||||||
* @return {string} token
|
* @return {string} token
|
||||||
*/
|
*/
|
||||||
generateToken(user: IUser): string {
|
generateToken(user: ISystemUser): string {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const exp = new Date(today);
|
const exp = new Date(today);
|
||||||
exp.setDate(today.getDate() + 60);
|
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(
|
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,
|
exp: exp.getTime() / 1000,
|
||||||
},
|
},
|
||||||
config.jwtSecret,
|
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()
|
@Inject()
|
||||||
journalService: JournalPosterService;
|
journalService: JournalPosterService;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new payment receive and store it to the storage
|
* Creates a new payment receive and store it to the storage
|
||||||
* with associated invoices payment and journal transactions.
|
* with associated invoices payment and journal transactions.
|
||||||
@@ -43,6 +46,8 @@ export default class PaymentReceiveService {
|
|||||||
} = this.tenancy.models(tenantId);
|
} = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
|
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
|
||||||
|
|
||||||
|
this.logger.info('[payment_receive] inserting to the storage.');
|
||||||
const storedPaymentReceive = await PaymentReceive.query()
|
const storedPaymentReceive = await PaymentReceive.query()
|
||||||
.insert({
|
.insert({
|
||||||
amount: paymentAmount,
|
amount: paymentAmount,
|
||||||
@@ -50,12 +55,15 @@ export default class PaymentReceiveService {
|
|||||||
});
|
});
|
||||||
const storeOpers: Array<any> = [];
|
const storeOpers: Array<any> = [];
|
||||||
|
|
||||||
|
this.logger.info('[payment_receive] inserting associated entries to the storage.');
|
||||||
paymentReceive.entries.forEach((entry: any) => {
|
paymentReceive.entries.forEach((entry: any) => {
|
||||||
const oper = PaymentReceiveEntry.query()
|
const oper = PaymentReceiveEntry.query()
|
||||||
.insert({
|
.insert({
|
||||||
payment_receive_id: storedPaymentReceive.id,
|
payment_receive_id: storedPaymentReceive.id,
|
||||||
...entry,
|
...entry,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.logger.info('[payment_receive] increment the sale invoice payment amount.');
|
||||||
// Increment the invoice payment amount.
|
// Increment the invoice payment amount.
|
||||||
const invoice = SaleInvoice.query()
|
const invoice = SaleInvoice.query()
|
||||||
.where('id', entry.invoice_id)
|
.where('id', entry.invoice_id)
|
||||||
@@ -64,6 +72,8 @@ export default class PaymentReceiveService {
|
|||||||
storeOpers.push(oper);
|
storeOpers.push(oper);
|
||||||
storeOpers.push(invoice);
|
storeOpers.push(invoice);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.logger.info('[payment_receive] decrementing customer balance.');
|
||||||
const customerIncrementOper = Customer.decrementBalance(
|
const customerIncrementOper = Customer.decrementBalance(
|
||||||
paymentReceive.customer_id,
|
paymentReceive.customer_id,
|
||||||
paymentAmount,
|
paymentAmount,
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export default class SaleEstimateService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
itemsEntriesService: HasItemsEntries;
|
itemsEntriesService: HasItemsEntries;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new estimate with associated entries.
|
* Creates a new estimate with associated entries.
|
||||||
* @async
|
* @async
|
||||||
@@ -31,12 +34,15 @@ export default class SaleEstimateService {
|
|||||||
amount,
|
amount,
|
||||||
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
|
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
|
||||||
const storedEstimate = await SaleEstimate.query()
|
const storedEstimate = await SaleEstimate.query()
|
||||||
.insert({
|
.insert({
|
||||||
...omit(estimate, ['entries']),
|
...omit(estimate, ['entries']),
|
||||||
});
|
});
|
||||||
const storeEstimateEntriesOpers: any[] = [];
|
const storeEstimateEntriesOpers: any[] = [];
|
||||||
|
|
||||||
|
this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.');
|
||||||
estimate.entries.forEach((entry: any) => {
|
estimate.entries.forEach((entry: any) => {
|
||||||
const oper = ItemEntry.query()
|
const oper = ItemEntry.query()
|
||||||
.insert({
|
.insert({
|
||||||
@@ -48,6 +54,8 @@ export default class SaleEstimateService {
|
|||||||
});
|
});
|
||||||
await Promise.all([...storeEstimateEntriesOpers]);
|
await Promise.all([...storeEstimateEntriesOpers]);
|
||||||
|
|
||||||
|
this.logger.info('[sale_estimate] insert sale estimated success.');
|
||||||
|
|
||||||
return storedEstimate;
|
return storedEstimate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +75,7 @@ export default class SaleEstimateService {
|
|||||||
amount,
|
amount,
|
||||||
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
|
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
|
||||||
};
|
};
|
||||||
|
this.logger.info('[sale_estimate] editing sale estimate on the storage.');
|
||||||
const updatedEstimate = await SaleEstimate.query()
|
const updatedEstimate = await SaleEstimate.query()
|
||||||
.update({
|
.update({
|
||||||
...omit(estimate, ['entries']),
|
...omit(estimate, ['entries']),
|
||||||
@@ -96,14 +105,14 @@ export default class SaleEstimateService {
|
|||||||
*/
|
*/
|
||||||
async deleteEstimate(tenantId: number, estimateId: number) {
|
async deleteEstimate(tenantId: number, estimateId: number) {
|
||||||
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.');
|
||||||
await ItemEntry.query()
|
await ItemEntry.query()
|
||||||
.where('reference_id', estimateId)
|
.where('reference_id', estimateId)
|
||||||
.where('reference_type', 'SaleEstimate')
|
.where('reference_type', 'SaleEstimate')
|
||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
await SaleEstimate.query()
|
await SaleEstimate.query().where('id', estimateId).delete();
|
||||||
.where('id', estimateId)
|
|
||||||
.delete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,10 +122,10 @@ export default class SaleEstimateService {
|
|||||||
* @param {Numeric} estimateId
|
* @param {Numeric} estimateId
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
async isEstimateExists(estimateId: number) {
|
async isEstimateExists(tenantId: number, estimateId: number) {
|
||||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||||
const foundEstimate = await SaleEstimate.query()
|
const foundEstimate = await SaleEstimate.query().where('id', estimateId);
|
||||||
.where('id', estimateId);
|
|
||||||
return foundEstimate.length !== 0;
|
return foundEstimate.length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +201,6 @@ export default class SaleEstimateService {
|
|||||||
const foundEstimates = await SaleEstimate.query()
|
const foundEstimates = await SaleEstimate.query()
|
||||||
.onBuild((query: any) => {
|
.onBuild((query: any) => {
|
||||||
query.where('estimate_number', estimateNumber);
|
query.where('estimate_number', estimateNumber);
|
||||||
|
|
||||||
if (excludeEstimateId) {
|
if (excludeEstimateId) {
|
||||||
query.whereNot('id', excludeEstimateId);
|
query.whereNot('id', excludeEstimateId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
@Inject()
|
@Inject()
|
||||||
itemsEntriesService: HasItemsEntries;
|
itemsEntriesService: HasItemsEntries;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
logger: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale invoices and store it to the storage
|
* Creates a new sale invoices and store it to the storage
|
||||||
* with associated to entries and journal transactions.
|
* with associated to entries and journal transactions.
|
||||||
@@ -43,12 +46,15 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
paymentAmount: 0,
|
paymentAmount: 0,
|
||||||
invLotNumber,
|
invLotNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
|
||||||
const storedInvoice = await SaleInvoice.query()
|
const storedInvoice = await SaleInvoice.query()
|
||||||
.insert({
|
.insert({
|
||||||
...omit(saleInvoice, ['entries']),
|
...omit(saleInvoice, ['entries']),
|
||||||
});
|
});
|
||||||
const opers: Array<any> = [];
|
const opers: Array<any> = [];
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] inserting sale invoice entries to the storage.');
|
||||||
saleInvoice.entries.forEach((entry: any) => {
|
saleInvoice.entries.forEach((entry: any) => {
|
||||||
const oper = ItemEntry.query()
|
const oper = ItemEntry.query()
|
||||||
.insertAndFetch({
|
.insertAndFetch({
|
||||||
@@ -61,15 +67,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
opers.push(oper);
|
opers.push(oper);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] trying to increment the customer balance.');
|
||||||
// Increment the customer balance after deliver the sale invoice.
|
// Increment the customer balance after deliver the sale invoice.
|
||||||
const incrementOper = Customer.incrementBalance(
|
const incrementOper = Customer.incrementBalance(
|
||||||
saleInvoice.customer_id,
|
saleInvoice.customer_id,
|
||||||
balance,
|
balance,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Await all async operations.
|
// Await all async operations.
|
||||||
await Promise.all([
|
await Promise.all([ ...opers, incrementOper ]);
|
||||||
...opers, incrementOper,
|
|
||||||
]);
|
|
||||||
// Records the inventory transactions for inventory items.
|
// Records the inventory transactions for inventory items.
|
||||||
await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id);
|
await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id);
|
||||||
|
|
||||||
@@ -100,6 +107,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
balance,
|
balance,
|
||||||
invLotNumber: oldSaleInvoice.invLotNumber,
|
invLotNumber: oldSaleInvoice.invLotNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] trying to update sale invoice.');
|
||||||
const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query()
|
const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query()
|
||||||
.where('id', saleInvoiceId)
|
.where('id', saleInvoiceId)
|
||||||
.update({
|
.update({
|
||||||
@@ -114,6 +123,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
|
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
|
||||||
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
|
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.
|
// Changes the diff customer balance between old and new amount.
|
||||||
const changeCustomerBalanceOper = Customer.changeDiffBalance(
|
const changeCustomerBalanceOper = Customer.changeDiffBalance(
|
||||||
saleInvoice.customer_id,
|
saleInvoice.customer_id,
|
||||||
@@ -155,12 +166,14 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
.findById(saleInvoiceId)
|
.findById(saleInvoiceId)
|
||||||
.withGraphFetched('entries');
|
.withGraphFetched('entries');
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] delete sale invoice with entries.');
|
||||||
await SaleInvoice.query().where('id', saleInvoiceId).delete();
|
await SaleInvoice.query().where('id', saleInvoiceId).delete();
|
||||||
await ItemEntry.query()
|
await ItemEntry.query()
|
||||||
.where('reference_id', saleInvoiceId)
|
.where('reference_id', saleInvoiceId)
|
||||||
.where('reference_type', 'SaleInvoice')
|
.where('reference_type', 'SaleInvoice')
|
||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] revert the customer balance.');
|
||||||
const revertCustomerBalanceOper = Customer.changeBalance(
|
const revertCustomerBalanceOper = Customer.changeBalance(
|
||||||
oldSaleInvoice.customerId,
|
oldSaleInvoice.customerId,
|
||||||
oldSaleInvoice.balance * -1,
|
oldSaleInvoice.balance * -1,
|
||||||
@@ -203,7 +216,13 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
* @param {number} saleInvoiceId -
|
* @param {number} saleInvoiceId -
|
||||||
* @param {boolean} override -
|
* @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
|
const inventortyTransactions = saleInvoice.entries
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
...pick(entry, ['item_id', 'quantity', 'rate',]),
|
...pick(entry, ['item_id', 'quantity', 'rate',]),
|
||||||
@@ -229,6 +248,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
||||||
const opers: Promise<[]>[] = [];
|
const opers: Promise<[]>[] = [];
|
||||||
|
|
||||||
|
this.logger.info('[sale_invoice] reverting inventory transactions');
|
||||||
|
|
||||||
inventoryTransactions.forEach((trans: any) => {
|
inventoryTransactions.forEach((trans: any) => {
|
||||||
switch(trans.direction) {
|
switch(trans.direction) {
|
||||||
case 'OUT':
|
case 'OUT':
|
||||||
@@ -359,7 +380,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
* Writes the sale invoice journal entries.
|
* Writes the sale invoice journal entries.
|
||||||
* @param {SaleInvoice} saleInvoice -
|
* @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 { Account, AccountTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const accountsDepGraph = await Account.depGraph().query();
|
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.
|
* @param {number} tenantId - The tenant id.
|
||||||
*/
|
*/
|
||||||
models(tenantId: number) {
|
models(tenantId: number) {
|
||||||
console.log(tenantId);
|
|
||||||
return this.tenantContainer(tenantId).get('models');
|
return this.tenantContainer(tenantId).get('models');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,10 @@ export default {
|
|||||||
sendResetPassword: 'onSendResetPassword',
|
sendResetPassword: 'onSendResetPassword',
|
||||||
resetPassword: 'onResetPassword',
|
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) => {
|
return knex.schema.createTable('tenants', (table) => {
|
||||||
table.bigIncrements();
|
table.bigIncrements();
|
||||||
table.string('organization_id');
|
table.string('organization_id');
|
||||||
|
table.boolean('initialized').defaultTo(false);
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { mixin } from 'objection';
|
import { mixin } from 'objection';
|
||||||
import SystemModel from '@/system/models/SystemModel';
|
import SystemModel from '@/system/models/SystemModel';
|
||||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
|
||||||
|
|
||||||
export default class Option extends SystemModel {
|
export default class Option extends SystemModel {
|
||||||
/**
|
/**
|
||||||
@@ -9,21 +8,4 @@ export default class Option extends SystemModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'options';
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
39
server/tests/lib/MetableStore.test.ts
Normal file
39
server/tests/lib/MetableStore.test.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { expect } from '~/testInit';
|
||||||
|
import MetableStore from '@/lib/MetableStore';
|
||||||
|
|
||||||
|
describe('MetableStore()', () => {
|
||||||
|
|
||||||
|
describe('find', () => {
|
||||||
|
it('Find metadata by the given key.', () => {
|
||||||
|
const store = new MetableStore();
|
||||||
|
store.metadata = [{ key: 'first-key', value: 'first-value' }];
|
||||||
|
|
||||||
|
const meta = store.find('first-key');
|
||||||
|
|
||||||
|
expect(meta.value).equals('first-value');
|
||||||
|
expect(meta.key).equals('first-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Find metadata by the key as payload.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Find metadata by the given key and extra columns.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('all()', () => {
|
||||||
|
it('Should retrieve all metadata in the store.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get()', () => {
|
||||||
|
it('Should retrieve data of the given metadata query.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeMeta')
|
||||||
|
});
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { create, expect } from '~/testInit';
|
|
||||||
import Option from '@/models/Option';
|
|
||||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
|
||||||
import {
|
|
||||||
tenantFactory,
|
|
||||||
tenantWebsite,
|
|
||||||
} from '~/dbInit';
|
|
||||||
|
|
||||||
|
|
||||||
describe('Model: Option', () => {
|
|
||||||
it('Should result collection be instance of `MetableCollection` class.', async () => {
|
|
||||||
await tenantFactory.create('option');
|
|
||||||
await tenantFactory.create('option');
|
|
||||||
const options = await Option.tenant().query();
|
|
||||||
|
|
||||||
expect(options).to.be.an.instanceof(MetableCollection);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
loginRes
|
loginRes
|
||||||
} from '~/dbInit';
|
} from '~/dbInit';
|
||||||
|
|
||||||
describe.only('route: `/api/purchases/bills`', () => {
|
describe('route: `/api/purchases/bills`', () => {
|
||||||
describe('POST: `/api/purchases/bills`', () => {
|
describe('POST: `/api/purchases/bills`', () => {
|
||||||
it('Should `bill_number` be required.', async () => {
|
it('Should `bill_number` be required.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.spec.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user