- 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:
Ahmed Bouhuolia
2020-09-03 16:51:48 +02:00
parent abefba22ee
commit 9ee7ed89ec
98 changed files with 1697 additions and 2052 deletions

View File

@@ -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 });
},
},
};

View File

@@ -1,17 +1,15 @@
import { Request, Response, Router } from 'express';
import { check, validationResult, matchedData, ValidationChain } from 'express-validator';
import { check, ValidationChain } from 'express-validator';
import { Service, Inject } from 'typedi';
import { camelCase, mapKeys } from 'lodash';
import BaseController from '@/http/controllers/BaseController';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import prettierMiddleware from '@/http/middleware/prettierMiddleware';
import AuthenticationService from '@/services/Authentication';
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
import { ServiceError, ServiceErrors } from "@/exceptions";
import { IRegisterDTO } from 'src/interfaces';
@Service()
export default class AuthenticationController {
export default class AuthenticationController extends BaseController{
@Inject()
authService: AuthenticationService;
@@ -88,6 +86,9 @@ export default class AuthenticationController {
]
}
/**
* Send reset password validation schema.
*/
get sendResetPasswordSchema(): ValidationChain[] {
return [
check('email').exists().isEmail().trim().escape(),
@@ -100,10 +101,7 @@ export default class AuthenticationController {
* @param {Response} res
*/
async login(req: Request, res: Response, next: Function): Response {
const userDTO: IUserOTD = mapKeys(matchedData(req, {
locations: ['body'],
includeOptionals: true,
}), (v, k) => camelCase(k));
const userDTO: IUserOTD = this.matchedBodyData(req);
try {
const { token, user } = await this.authService.signIn(
@@ -134,13 +132,10 @@ export default class AuthenticationController {
* @param {Response} res
*/
async register(req: Request, res: Response, next: Function) {
const registerDTO: IRegisterDTO = mapKeys(matchedData(req, {
locations: ['body'],
includeOptionals: true,
}), (v, k) => camelCase(k));
const registerDTO: IRegisterOTD = this.matchedBodyData(req);
try {
const registeredUser = await this.authService.register(registerDTO);
const registeredUser: ISystemUser = await this.authService.register(registerDTO);
return res.status(200).send({
code: 'REGISTER.SUCCESS',
@@ -170,7 +165,7 @@ export default class AuthenticationController {
* @param {Response} res
*/
async sendResetPassword(req: Request, res: Response, next: Function) {
const { email } = req.body;
const { email } = this.matchedBodyData(req);
try {
await this.authService.sendResetPassword(email);

View File

@@ -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) {
},
},
}

View File

@@ -1,3 +1,14 @@
import { matchedData } from "express-validator";
import { mapKeys, camelCase, omit } from "lodash";
export default class BaseController {
matchedBodyData(req: Request, options: any) {
const data = matchedData(req, {
locations: ['body'],
includeOptionals: true,
...omit(options, ['locations']), // override any propery except locations.
});
return mapKeys(data, (v, k) => camelCase(k));
}
}

View File

@@ -20,7 +20,6 @@ export default {
*/
router() {
const router = express.Router();
router.use(JWTAuth);
router.post(
'/',

View File

@@ -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,
},
});
},
},
}

View 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);
}
}
}

View File

@@ -1,4 +1,4 @@
import express from 'express';
import { Router, Request, Response } from 'express';
import {
check,
param,
@@ -17,22 +17,21 @@ import {
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
import BaseController from '@/http/controllers/BaseController';
@Service()
export default class ItemsCategoriesController {
export default class ItemsCategoriesController extends BaseController {
/**
* Router constructor method.
*/
constructor() {
const router = express.Router();
router() {
const router = Router();
router.post('/:id', [
...this.categoryValidationSchema,
...this.specificCategoryValidationSchema,
],
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateParentCategoryExistance),
asyncMiddleware(this.validateSellAccountExistance),
asyncMiddleware(this.validateCostAccountExistance),
@@ -42,7 +41,6 @@ export default class ItemsCategoriesController {
router.post('/',
this.categoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateParentCategoryExistance),
asyncMiddleware(this.validateSellAccountExistance),
asyncMiddleware(this.validateCostAccountExistance),
@@ -52,28 +50,24 @@ export default class ItemsCategoriesController {
router.delete('/bulk',
this.categoriesBulkValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateCategoriesIdsExistance),
asyncMiddleware(this.bulkDeleteCategories),
);
router.delete('/:id',
this.specificCategoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateItemCategoryExistance),
asyncMiddleware(this.deleteItem),
);
router.get('/:id',
this.specificCategoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateItemCategoryExistance),
asyncMiddleware(this.getCategory)
);
router.get('/',
this.categoriesListValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.getList)
);
return router;
@@ -164,7 +158,7 @@ export default class ItemsCategoriesController {
*/
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
const category: IItemCategoryOTD = this.matchedBodyData(req);
if (category.costAccountId) {
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
@@ -191,7 +185,7 @@ export default class ItemsCategoriesController {
*/
async validateSellAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
const category: IItemCategoryOTD = this.matchedBodyData(req);
if (category.sellAccountId) {
const incomeType = await AccountType.query().findOne('key', 'income');
@@ -218,7 +212,7 @@ export default class ItemsCategoriesController {
*/
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
const category: IItemCategoryOTD = this.matchedBodyData(req);
if (category.inventoryAccountId) {
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
@@ -244,7 +238,7 @@ export default class ItemsCategoriesController {
* @param {Function} next
*/
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
const category: IItemCategory = { ...req.body };
const category: IItemCategory = this.matchedBodyData(req);
const { ItemCategory } = req.models;
if (category.parentCategoryId) {
@@ -290,7 +284,7 @@ export default class ItemsCategoriesController {
*/
async newCategory(req: Request, res: Response) {
const { user } = req;
const category: IItemCategory = { ...req.body };
const category: IItemCategory = this.matchedBodyData(req);
const { ItemCategory } = req.models;
const storedCategory = await ItemCategory.query().insert({
@@ -308,7 +302,7 @@ export default class ItemsCategoriesController {
*/
async editCategory(req: Request, res: Response) {
const { id } = req.params;
const category: IItemCategory = { ...req.body };
const category: IItemCategory = this.matchedBodyData(req);
const { ItemCategory } = req.models;
const updateItemCategory = await ItemCategory.query()

View File

@@ -5,10 +5,10 @@ import {
query,
validationResult,
} from 'express-validator';
import Container from 'typedi';
import fs from 'fs';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Logger from '@/services/Logger';
const fsPromises = fs.promises;
@@ -70,6 +70,8 @@ export default {
// check('attachment').exists(),
],
async handler(req, res) {
const Logger = Container.get('logger');
if (!req.files.attachment) {
return res.status(400).send({
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
@@ -93,9 +95,9 @@ export default {
try {
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
Logger.log('info', 'Attachment uploaded successfully');
Logger.info('[attachment] uploaded successfully');
} catch (error) {
Logger.log('info', 'Attachment uploading failed.', { error });
Logger.info('[attachment] uploading failed.', { error });
}
const media = await Media.query().insert({
@@ -114,6 +116,7 @@ export default {
query('ids.*').exists().isNumeric().toInt(),
],
async handler(req, res) {
const Logger = Container.get('logger');
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
@@ -142,12 +145,12 @@ export default {
});
await Promise.all(unlinkOpers).then((resolved) => {
resolved.forEach(() => {
Logger.log('error', 'Attachment file has been deleted.');
Logger.info('[attachment] file has been deleted.');
});
})
.catch((errors) => {
errors.forEach((error) => {
Logger.log('error', 'Delete item attachment file delete failed.', { error });
Logger.info('[attachment] Delete item attachment file delete failed.', { error });
})
});

View File

@@ -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 });
},
},
};

View 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);
}
}
}

View File

@@ -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();
},
},
};

View 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 });
}
};

View File

@@ -1,8 +1,9 @@
import { Inject } from 'typedi';
import { Plan } from '@/system/models';
import BaseController from '@/http/controllers/BaseController';
import SubscriptionService from '@/services/Subscription/SubscriptionService';
export default class PaymentMethodController {
export default class PaymentMethodController extends BaseController {
@Inject()
subscriptionService: SubscriptionService;
@@ -16,7 +17,7 @@ export default class PaymentMethodController {
* @return {Response|void}
*/
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
const { planSlug } = req.body;
const { planSlug } = this.matchedBodyData(req);
const foundPlan = await Plan.query().where('slug', planSlug).first();
if (!foundPlan) {

View File

@@ -1,17 +1,19 @@
import { Container, Service } from 'typedi';
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check, param, query, ValidationSchema } from 'express-validator';
import { Voucher, Plan } from '@/system/models';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
import {
NotAllowedChangeSubscriptionPlan
} from '@/exceptions';
@Service()
export default class PaymentViaVoucherController extends PaymentMethodController {
@Inject('logger')
logger: any;
/**
* Router constructor.
*/
@@ -22,7 +24,6 @@ export default class PaymentViaVoucherController extends PaymentMethodController
'/payment',
this.paymentViaVoucherSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
@@ -48,7 +49,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
* @param {Response} res
*/
async validateVoucherCodeExistance(req: Request, res: Response, next: Function) {
const { voucherCode } = req.body;
const { voucherCode } = this.matchedBodyData(req);
this.logger.info('[voucher_payment] trying to validate voucher code.', { voucherCode });
const foundVoucher = await Voucher.query()
.modify('filterActiveVoucher')
@@ -70,7 +72,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
* @param {Function} next
*/
async validateVoucherAndPlan(req: Request, res: Response, next: Function) {
const { planSlug, voucherCode } = req.body;
const { planSlug, voucherCode } = this.matchedBodyData(req);
this.logger.info('[voucher_payment] trying to validate voucher with the plan.', { voucherCode });
const voucher = await Voucher.query().findOne('voucher_code', voucherCode);
const plan = await Plan.query().findOne('slug', planSlug);
@@ -90,11 +93,12 @@ export default class PaymentViaVoucherController extends PaymentMethodController
* @return {Response}
*/
async paymentViaVoucher(req: Request, res: Response, next: Function) {
const { planSlug, voucherCode } = req.body;
const { planSlug, voucherCode } = this.matchedBodyData(req);
const { tenant } = req;
try {
await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
await this.subscriptionService
.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
return res.status(200).send({
type: 'PAYMENT.SUCCESSFULLY.MADE',

View File

@@ -1,16 +1,15 @@
import { Router, Request, Response } from 'express'
import { repeat, times, orderBy } from 'lodash';
import { check, oneOf, param, query, ValidationChain } from 'express-validator';
import { Container, Service, Inject } from 'typedi';
import { check, oneOf, ValidationChain } from 'express-validator';
import { Service, Inject } from 'typedi';
import { Voucher, Plan } from '@/system/models';
import BaseController from '@/http/controllers/BaseController';
import VoucherService from '@/services/Payment/Voucher';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PrettierMiddleware from '@/http/middleware/prettierMiddleware';
import { IVouchersFilter } from '@/interfaces';
@Service()
export default class VouchersController {
export default class VouchersController extends BaseController {
@Inject()
voucherService: VoucherService;
@@ -24,14 +23,12 @@ export default class VouchersController {
'/generate',
this.generateVoucherSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validatePlanExistance.bind(this)),
asyncMiddleware(this.generateVoucher.bind(this)),
);
router.post(
'/disable/:voucherId',
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateVoucherExistance.bind(this)),
asyncMiddleware(this.validateNotDisabledVoucher.bind(this)),
asyncMiddleware(this.disableVoucher.bind(this)),
@@ -40,18 +37,15 @@ export default class VouchersController {
'/send',
this.sendVoucherSchemaValidation,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.sendVoucher.bind(this)),
);
router.delete(
'/:voucherId',
PrettierMiddleware,
asyncMiddleware(this.validateVoucherExistance.bind(this)),
asyncMiddleware(this.deleteVoucher.bind(this)),
);
router.get(
'/',
PrettierMiddleware,
asyncMiddleware(this.listVouchers.bind(this)),
);
return router;
@@ -106,7 +100,8 @@ export default class VouchersController {
* @param {Function} next
*/
async validatePlanExistance(req: Request, res: Response, next: Function) {
const planId: number = req.body.planId || req.params.planId;
const body = this.matchedBodyData(req);
const planId: number = body.planId || req.params.planId;
const foundPlan = await Plan.query().findById(planId);
if (!foundPlan) {
@@ -124,7 +119,9 @@ export default class VouchersController {
* @param {Function}
*/
async validateVoucherExistance(req: Request, res: Response, next: Function) {
const voucherId = req.body.voucherId || req.params.voucherId;
const body = this.matchedBodyData(req);
const voucherId = body.voucherId || req.params.voucherId;
const foundVoucher = await Voucher.query().findById(voucherId);
if (!foundVoucher) {
@@ -160,7 +157,7 @@ export default class VouchersController {
* @return {Response}
*/
async generateVoucher(req: Request, res: Response, next: Function) {
const { loop = 10, period, periodInterval, planId } = req.body;
const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(req);
try {
await this.voucherService.generateVouchers(
@@ -211,7 +208,7 @@ export default class VouchersController {
* @return {Response}
*/
async sendVoucher(req: Request, res: Response) {
const { phoneNumber, email, period, periodInterval, planId } = req.body;
const { phoneNumber, email, period, periodInterval, planId } = this.matchedBodyData(req);
const voucher = await Voucher.query()
.modify('filterActiveVoucher')

View File

@@ -2,6 +2,7 @@ import { Router } from 'express'
import { Container, Service } from 'typedi';
import JWTAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
@Service()
@@ -13,6 +14,7 @@ export default class SubscriptionController {
const router = Router();
router.use(JWTAuth);
router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware);
router.use('/voucher', Container.get(PaymentViaVoucherController).router());