mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
- feat: remove unnecessary migrations, controllers and models files.
- feat: metable store - feat: metable store with settings store. - feat: settings middleware to auto-save and load. - feat: DI db manager to master container. - feat: write some logs to sale invoices.
This commit is contained in:
@@ -1,147 +0,0 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult, oneOf } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.post('/',
|
||||
this.openingBalnace.validation,
|
||||
asyncMiddleware(this.openingBalnace.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opening balance to the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
openingBalnace: {
|
||||
validation: [
|
||||
check('date').optional(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('balance_adjustment_account').exists().isNumeric().toInt(),
|
||||
check('accounts').isArray({ min: 1 }),
|
||||
check('accounts.*.id').exists().isInt(),
|
||||
oneOf([
|
||||
check('accounts.*.debit').exists().isNumeric().toFloat(),
|
||||
check('accounts.*.credit').exists().isNumeric().toFloat(),
|
||||
]),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { accounts } = req.body;
|
||||
const { user } = req;
|
||||
const form = { ...req.body };
|
||||
const date = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
const accountsIds = accounts.map((account) => account.id);
|
||||
|
||||
const { Account, ManualJournal } = req.models;
|
||||
const storedAccounts = await Account.query()
|
||||
.select(['id']).whereIn('id', accountsIds)
|
||||
.withGraphFetched('type');
|
||||
|
||||
const accountsCollection = new Map(storedAccounts.map((i) => [i.id, i]));
|
||||
|
||||
// Get the stored accounts Ids and difference with submit accounts.
|
||||
const accountsStoredIds = storedAccounts.map((account) => account.id);
|
||||
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
|
||||
const errorReasons = [];
|
||||
|
||||
if (notFoundAccountsIds.length > 0) {
|
||||
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
|
||||
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
|
||||
}
|
||||
if (form.balance_adjustment_account) {
|
||||
const account = await Account.query().findById(form.balance_adjustment_account);
|
||||
|
||||
if (!account) {
|
||||
errorReasons.push({ type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300 });
|
||||
}
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badData(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
|
||||
accounts.forEach((account) => {
|
||||
const storedAccount = accountsCollection.get(account.id);
|
||||
|
||||
// Can't continue in case the stored account was not found.
|
||||
if (!storedAccount) { return; }
|
||||
|
||||
const entryModel = new JournalEntry({
|
||||
referenceType: 'OpeningBalance',
|
||||
account: account.id,
|
||||
accountNormal: storedAccount.type.normal,
|
||||
userId: user.id,
|
||||
});
|
||||
if (account.credit) {
|
||||
entryModel.entry.credit = account.credit;
|
||||
journalEntries.credit(entryModel);
|
||||
} else if (account.debit) {
|
||||
entryModel.entry.debit = account.debit;
|
||||
journalEntries.debit(entryModel);
|
||||
}
|
||||
});
|
||||
// Calculates the credit and debit balance of stacked entries.
|
||||
const trial = journalEntries.getTrialBalance();
|
||||
|
||||
if (trial.credit !== trial.debit) {
|
||||
const entryModel = new JournalEntry({
|
||||
referenceType: 'OpeningBalance',
|
||||
account: form.balance_adjustment_account,
|
||||
accountNormal: 'credit',
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (trial.credit > trial.debit) {
|
||||
entryModel.entry.credit = Math.abs(trial.credit);
|
||||
journalEntries.credit(entryModel);
|
||||
|
||||
} else if (trial.credit < trial.debit) {
|
||||
entryModel.entry.debit = Math.abs(trial.debit);
|
||||
journalEntries.debit(entryModel);
|
||||
}
|
||||
}
|
||||
const manualJournal = await ManualJournal.query().insert({
|
||||
amount: Math.max(trial.credit, trial.debit),
|
||||
transaction_type: 'OpeningBalance',
|
||||
date,
|
||||
note: form.note,
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
journalEntries.entries = journalEntries.entries.map((entry) => ({
|
||||
...entry,
|
||||
referenceId: manualJournal.id,
|
||||
}));
|
||||
await Promise.all([
|
||||
journalEntries.saveEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id: manualJournal.id });
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, validationResult, matchedData, ValidationChain } from 'express-validator';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import prettierMiddleware from '@/http/middleware/prettierMiddleware';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import { IRegisterDTO } from 'src/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationController {
|
||||
export default class AuthenticationController extends BaseController{
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
@@ -88,6 +86,9 @@ export default class AuthenticationController {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password validation schema.
|
||||
*/
|
||||
get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('email').exists().isEmail().trim().escape(),
|
||||
@@ -100,10 +101,7 @@ export default class AuthenticationController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async login(req: Request, res: Response, next: Function): Response {
|
||||
const userDTO: IUserOTD = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
const userDTO: IUserOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { token, user } = await this.authService.signIn(
|
||||
@@ -134,13 +132,10 @@ export default class AuthenticationController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async register(req: Request, res: Response, next: Function) {
|
||||
const registerDTO: IRegisterDTO = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
const registerDTO: IRegisterOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const registeredUser = await this.authService.register(registerDTO);
|
||||
const registeredUser: ISystemUser = await this.authService.register(registerDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'REGISTER.SUCCESS',
|
||||
@@ -170,7 +165,7 @@ export default class AuthenticationController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body;
|
||||
const { email } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
reconciliations: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
reconciliation: {
|
||||
validation: [
|
||||
body('from_date'),
|
||||
body('to_date'),
|
||||
body('closing_balance'),
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
import { matchedData } from "express-validator";
|
||||
import { mapKeys, camelCase, omit } from "lodash";
|
||||
|
||||
export default class BaseController {
|
||||
|
||||
matchedBodyData(req: Request, options: any) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
...omit(options, ['locations']), // override any propery except locations.
|
||||
});
|
||||
return mapKeys(data, (v, k) => camelCase(k));
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export default {
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import express from 'express';
|
||||
import uniqid from 'uniqid';
|
||||
import {
|
||||
check,
|
||||
body,
|
||||
param,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import moment from 'moment';
|
||||
import { hashPassword } from '@/utils';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import Invite from '@/system/models/Invite';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import Logger from '@/services/Logger';
|
||||
import Option from '@/models/Option';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/send', jwtAuth);
|
||||
router.use('/send', TenancyMiddleware);
|
||||
|
||||
router.post('/send',
|
||||
this.invite.validation,
|
||||
asyncMiddleware(this.invite.handler));
|
||||
|
||||
router.post('/accept/:token',
|
||||
this.accept.validation,
|
||||
asyncMiddleware(this.accept.handler));
|
||||
|
||||
router.get('/invited/:token',
|
||||
this.invited.validation,
|
||||
asyncMiddleware(this.invited.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
*/
|
||||
invite: {
|
||||
validation: [
|
||||
body('email').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const { user } = req;
|
||||
const { TenantUser } = req.models;
|
||||
const foundUser = await SystemUser.query()
|
||||
.where('email', form.email).first();
|
||||
|
||||
if (foundUser) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'USER.EMAIL.ALREADY.REGISTERED', code: 100 }],
|
||||
});
|
||||
}
|
||||
const token = uniqid();
|
||||
const invite = await Invite.query().insert({
|
||||
email: form.email,
|
||||
tenant_id: user.tenantId,
|
||||
token,
|
||||
});
|
||||
const tenantUser = await TenantUser.query().insert({
|
||||
first_name: form.email,
|
||||
email: form.email,
|
||||
});
|
||||
const { Option } = req.models;
|
||||
const organizationOptions = await Option.query()
|
||||
.where('key', 'organization_name');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const rendered = Mustache.render(template, {
|
||||
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
organizationName: organizationOptions.getMeta('organization_name'),
|
||||
});
|
||||
const mailOptions = {
|
||||
to: user.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: `${user.fullName} has invited you to join a Bigcapital`,
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send user invite mail', { error, form });
|
||||
}
|
||||
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
|
||||
});
|
||||
return res.status(200).send();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Acceprt the inviation.
|
||||
*/
|
||||
accept: {
|
||||
validation: [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { token } = req.params;
|
||||
const inviteToken = await Invite.query()
|
||||
.where('token', token).first();
|
||||
|
||||
if (!inviteToken) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
language: 'en',
|
||||
...req.body,
|
||||
};
|
||||
const systemUser = await SystemUser.query()
|
||||
.where('phone_number', form.phone_number)
|
||||
.first();
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate there is already registered phone number.
|
||||
if (systemUser && systemUser.phoneNumber === form.phone_number) {
|
||||
errorReasons.push({
|
||||
type: 'PHONE_MUMNER.ALREADY.EXISTS', code: 400,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query()
|
||||
.where('id', inviteToken.tenantId).first();
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
|
||||
const hashedPassword = await hashPassword(form.password);
|
||||
|
||||
const userForm = {
|
||||
first_name: form.first_name,
|
||||
last_name: form.last_name,
|
||||
email: inviteToken.email,
|
||||
phone_number: form.phone_number,
|
||||
language: form.language,
|
||||
active: 1,
|
||||
};
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
const foundTenantUser = await TenantUser.query()
|
||||
.where('phone_number', form.phone_number).first();
|
||||
|
||||
if (foundTenantUser) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PHONE_NUMBER.ALREADY.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
|
||||
const insertUserOper = TenantUser.bindKnex(tenantDb)
|
||||
.query()
|
||||
.where('email', userForm.email)
|
||||
.patch({
|
||||
...userForm,
|
||||
invite_accepted_at: moment().format('YYYY/MM/DD'),
|
||||
});
|
||||
|
||||
const insertSysUserOper = SystemUser.query().insert({
|
||||
...userForm,
|
||||
password: hashedPassword,
|
||||
tenant_id: inviteToken.tenantId,
|
||||
});
|
||||
|
||||
const deleteInviteTokenOper = Invite.query()
|
||||
.where('token', inviteToken.token).delete();
|
||||
|
||||
await Promise.all([
|
||||
insertUserOper,
|
||||
insertSysUserOper,
|
||||
deleteInviteTokenOper,
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Get
|
||||
*/
|
||||
invited: {
|
||||
validation: [
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { token } = req.params;
|
||||
const inviteToken = await Invite.query()
|
||||
.where('token', token).first();
|
||||
|
||||
if (!inviteToken) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query()
|
||||
.where('id', inviteToken.tenantId).first();
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
|
||||
const organizationOptions = await Option.bindKnex(tenantDb).query()
|
||||
.where('key', 'organization_name');
|
||||
|
||||
return res.status(200).send({
|
||||
data: {
|
||||
organization_name: organizationOptions.getMeta('organization_name', '') ,
|
||||
invited_email: inviteToken.email,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
140
server/src/http/controllers/InviteUsers.ts
Normal file
140
server/src/http/controllers/InviteUsers.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import {
|
||||
check,
|
||||
body,
|
||||
param,
|
||||
matchedData,
|
||||
} from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import InviteUserService from '@/services/InviteUsers';
|
||||
import { ServiceErrors, ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersController {
|
||||
@Inject()
|
||||
inviteUsersService: InviteUserService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/send', jwtAuth);
|
||||
router.use('/send', TenancyMiddleware);
|
||||
|
||||
router.post('/send', [
|
||||
body('email').exists().trim().escape(),
|
||||
],
|
||||
asyncMiddleware(this.sendInvite),
|
||||
);
|
||||
|
||||
router.post('/accept/:token', [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
asyncMiddleware(this.accept)
|
||||
);
|
||||
|
||||
router.get('/invited/:token', [
|
||||
param('token').exists().trim().escape(),
|
||||
],
|
||||
asyncMiddleware(this.invited)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async sendInvite(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body;
|
||||
const { tenantId } = req;
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.sendInvite(tenantId, email, user);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'email_already_invited') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.ALREADY.INVITED' }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the inviation.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async accept(req: Request, res: Response, next: Function) {
|
||||
const inviteUserInput: IInviteUserInput = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.acceptInvite(token, inviteUserInput);
|
||||
return res.status(200).send();
|
||||
} catch (error) {
|
||||
|
||||
if (error instanceof ServiceErrors) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (error.hasType('email_exists')) {
|
||||
errorReasons.push({ type: 'EMAIL.EXISTS' });
|
||||
}
|
||||
if (error.hasType('phone_number_exists')) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER.EXISTS' });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the invite token is valid.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async invited(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.checkInvite(token);
|
||||
|
||||
return res.status(200).send();
|
||||
} catch (error) {
|
||||
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'invite_token_invalid') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.INVALID' }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import express from 'express';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
@@ -17,22 +17,21 @@ import {
|
||||
mapFilterRolesToDynamicFilter,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
|
||||
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
|
||||
@Service()
|
||||
export default class ItemsCategoriesController {
|
||||
export default class ItemsCategoriesController extends BaseController {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
const router = express.Router();
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/:id', [
|
||||
...this.categoryValidationSchema,
|
||||
...this.specificCategoryValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateParentCategoryExistance),
|
||||
asyncMiddleware(this.validateSellAccountExistance),
|
||||
asyncMiddleware(this.validateCostAccountExistance),
|
||||
@@ -42,7 +41,6 @@ export default class ItemsCategoriesController {
|
||||
router.post('/',
|
||||
this.categoryValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateParentCategoryExistance),
|
||||
asyncMiddleware(this.validateSellAccountExistance),
|
||||
asyncMiddleware(this.validateCostAccountExistance),
|
||||
@@ -52,28 +50,24 @@ export default class ItemsCategoriesController {
|
||||
router.delete('/bulk',
|
||||
this.categoriesBulkValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateCategoriesIdsExistance),
|
||||
asyncMiddleware(this.bulkDeleteCategories),
|
||||
);
|
||||
router.delete('/:id',
|
||||
this.specificCategoryValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateItemCategoryExistance),
|
||||
asyncMiddleware(this.deleteItem),
|
||||
);
|
||||
router.get('/:id',
|
||||
this.specificCategoryValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateItemCategoryExistance),
|
||||
asyncMiddleware(this.getCategory)
|
||||
);
|
||||
router.get('/',
|
||||
this.categoriesListValidationSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.getList)
|
||||
);
|
||||
return router;
|
||||
@@ -164,7 +158,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { Account, AccountType } = req.models;
|
||||
const category: IItemCategoryOTD = { ...req.body };
|
||||
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.costAccountId) {
|
||||
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
|
||||
@@ -191,7 +185,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async validateSellAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { Account, AccountType } = req.models;
|
||||
const category: IItemCategoryOTD = { ...req.body };
|
||||
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.sellAccountId) {
|
||||
const incomeType = await AccountType.query().findOne('key', 'income');
|
||||
@@ -218,7 +212,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
|
||||
const { Account, AccountType } = req.models;
|
||||
const category: IItemCategoryOTD = { ...req.body };
|
||||
const category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.inventoryAccountId) {
|
||||
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
|
||||
@@ -244,7 +238,7 @@ export default class ItemsCategoriesController {
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
|
||||
const category: IItemCategory = { ...req.body };
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
if (category.parentCategoryId) {
|
||||
@@ -290,7 +284,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async newCategory(req: Request, res: Response) {
|
||||
const { user } = req;
|
||||
const category: IItemCategory = { ...req.body };
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
const storedCategory = await ItemCategory.query().insert({
|
||||
@@ -308,7 +302,7 @@ export default class ItemsCategoriesController {
|
||||
*/
|
||||
async editCategory(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
const category: IItemCategory = { ...req.body };
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
const updateItemCategory = await ItemCategory.query()
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import Container from 'typedi';
|
||||
import fs from 'fs';
|
||||
import { difference } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Logger from '@/services/Logger';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
@@ -70,6 +70,8 @@ export default {
|
||||
// check('attachment').exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
if (!req.files.attachment) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
|
||||
@@ -93,9 +95,9 @@ export default {
|
||||
|
||||
try {
|
||||
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
|
||||
Logger.log('info', 'Attachment uploaded successfully');
|
||||
Logger.info('[attachment] uploaded successfully');
|
||||
} catch (error) {
|
||||
Logger.log('info', 'Attachment uploading failed.', { error });
|
||||
Logger.info('[attachment] uploading failed.', { error });
|
||||
}
|
||||
|
||||
const media = await Media.query().insert({
|
||||
@@ -114,6 +116,7 @@ export default {
|
||||
query('ids.*').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const Logger = Container.get('logger');
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
@@ -142,12 +145,12 @@ export default {
|
||||
});
|
||||
await Promise.all(unlinkOpers).then((resolved) => {
|
||||
resolved.forEach(() => {
|
||||
Logger.log('error', 'Attachment file has been deleted.');
|
||||
Logger.info('[attachment] file has been deleted.');
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
errors.forEach((error) => {
|
||||
Logger.log('error', 'Delete item attachment file delete failed.', { error });
|
||||
Logger.info('[attachment] Delete item attachment file delete failed.', { error });
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import express from 'express';
|
||||
import { body, query, validationResult } from 'express-validator';
|
||||
import { pick } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.saveOptions.validation,
|
||||
asyncMiddleware(this.saveOptions.handler));
|
||||
|
||||
router.get('/',
|
||||
this.getOptions.validation,
|
||||
asyncMiddleware(this.getOptions.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the given options to the storage.
|
||||
*/
|
||||
saveOptions: {
|
||||
validation: [
|
||||
body('options').isArray({ min: 1 }),
|
||||
body('options.*.key').exists(),
|
||||
body('options.*.value').exists(),
|
||||
body('options.*.group').exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Option } = req.models;
|
||||
const form = { ...req.body };
|
||||
const optionsCollections = await Option.query();
|
||||
|
||||
const errorReasons = [];
|
||||
const notDefinedOptions = Option.validateDefined(form.options);
|
||||
|
||||
if (notDefinedOptions.length) {
|
||||
errorReasons.push({
|
||||
type: 'OPTIONS.KEY.NOT.DEFINED',
|
||||
code: 200,
|
||||
keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })),
|
||||
});
|
||||
}
|
||||
if (errorReasons.length) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
form.options.forEach((option) => {
|
||||
optionsCollections.setMeta({ ...option });
|
||||
});
|
||||
await optionsCollections.saveMeta();
|
||||
|
||||
return res.status(200).send({ options: form });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
getOptions: {
|
||||
validation: [
|
||||
query('key').optional(),
|
||||
query('group').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Option } = req.models;
|
||||
const filter = { ...req.query };
|
||||
const options = await Option.query().onBuild((builder) => {
|
||||
if (filter.key) {
|
||||
builder.where('key', filter.key);
|
||||
}
|
||||
if (filter.group) {
|
||||
builder.where('group', filter.group);
|
||||
}
|
||||
});
|
||||
return res.status(200).send({ options: options.metadata });
|
||||
},
|
||||
},
|
||||
};
|
||||
65
server/src/http/controllers/Organization.ts
Normal file
65
server/src/http/controllers/Organization.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { check, matchedData } from 'express-validator';
|
||||
import { mapKeys, camelCase } from 'lodash';
|
||||
import asyncMiddleware from "@/http/middleware/asyncMiddleware";
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import OrganizationService from '@/services/Organization';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/build', [
|
||||
check('organization_id').exists().trim().escape(),
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.build.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds tenant database and seed initial data.
|
||||
* @param {Request} req - Express request.
|
||||
* @param {Response} res - Express response.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async build(req: Request, res: Response, next: Function) {
|
||||
const buildOTD = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
|
||||
try {
|
||||
await this.organizationService.build(buildOTD.organizationId);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'ORGANIZATION.DATABASE.INITIALIZED',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'tenant_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'tenant_initialized') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.DATABASE.ALREADY.BUILT', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Role from '@/models/Role';
|
||||
import Permission from '@/models/Permission';
|
||||
import Resource from '@/models/Resource';
|
||||
import knex from '@/database/knex';
|
||||
|
||||
const AccessControllSchema = [
|
||||
{
|
||||
resource: 'items',
|
||||
label: 'products_services',
|
||||
permissions: ['create', 'edit', 'delete', 'view'],
|
||||
fullAccess: true,
|
||||
ownControl: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getResourceSchema = (resource) => AccessControllSchema
|
||||
.find((schema) => schema.resource === resource);
|
||||
|
||||
const getResourcePermissions = (resource) => {
|
||||
const foundResource = getResourceSchema(resource);
|
||||
return foundResource ? foundResource.permissions : [];
|
||||
};
|
||||
|
||||
const findNotFoundResources = (resourcesSlugs) => {
|
||||
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
|
||||
return difference(resourcesSlugs, schemaResourcesSlugs);
|
||||
};
|
||||
|
||||
const findNotFoundPermissions = (permissions, resourceSlug) => {
|
||||
const schemaPermissions = getResourcePermissions(resourceSlug);
|
||||
return difference(permissions, schemaPermissions);
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.newRole.validation,
|
||||
asyncMiddleware(this.newRole.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.editRole.validation,
|
||||
asyncMiddleware(this.editRole.handler.bind(this)));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteRole.validation,
|
||||
asyncMiddleware(this.deleteRole.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new role.
|
||||
*/
|
||||
newRole: {
|
||||
validation: [
|
||||
check('name').exists().trim().escape(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('permissions').isArray({ min: 0 }),
|
||||
check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'),
|
||||
check('permissions.*.permissions').isArray({ min: 1 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { name, description, permissions } = req.body;
|
||||
|
||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||
const permissionsSlugs = [];
|
||||
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||
|
||||
const errorReasons = [];
|
||||
const notFoundPermissions = [];
|
||||
|
||||
if (resourcesNotFound.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound,
|
||||
});
|
||||
}
|
||||
permissions.forEach((perm) => {
|
||||
const abilities = perm.permissions.map((ability) => ability);
|
||||
|
||||
// Gets the not found permissions in the schema.
|
||||
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
if (notFoundAbilities.length > 0) {
|
||||
notFoundPermissions.push({
|
||||
resource_slug: perm.resource_slug,
|
||||
permissions: notFoundAbilities,
|
||||
});
|
||||
} else {
|
||||
const perms = perm.permissions || [];
|
||||
perms.forEach((permission) => {
|
||||
if (perms.indexOf(permission) !== -1) {
|
||||
permissionsSlugs.push(permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (notFoundPermissions.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PERMISSIONS_SLUG_NOT_FOUND',
|
||||
code: 200,
|
||||
permissions: notFoundPermissions,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
// Permissions.
|
||||
const [resourcesCollection, permsCollection] = await Promise.all([
|
||||
Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const notStoredResources = difference(
|
||||
resourcesSlugs, resourcesCollection.map((s) => s.name),
|
||||
);
|
||||
const notStoredPermissions = difference(
|
||||
permissionsSlugs, permsCollection.map((perm) => perm.slug),
|
||||
);
|
||||
|
||||
const insertThread = [];
|
||||
|
||||
if (notStoredResources.length > 0) {
|
||||
insertThread.push(knex('resources').insert([
|
||||
...notStoredResources.map((resource) => ({ name: resource })),
|
||||
]));
|
||||
}
|
||||
if (notStoredPermissions.length > 0) {
|
||||
insertThread.push(knex('permissions').insert([
|
||||
...notStoredPermissions.map((permission) => ({ name: permission })),
|
||||
]));
|
||||
}
|
||||
|
||||
await Promise.all(insertThread);
|
||||
|
||||
const [storedPermissions, storedResources] = await Promise.all([
|
||||
Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const storedResourcesSet = new Map(storedResources.map((resource) => [
|
||||
resource.attributes.name, resource.attributes.id,
|
||||
]));
|
||||
const storedPermissionsSet = new Map(storedPermissions.map((perm) => [
|
||||
perm.attributes.name, perm.attributes.id,
|
||||
]));
|
||||
const role = Role.forge({ name, description });
|
||||
|
||||
await role.save();
|
||||
|
||||
const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({
|
||||
role_id: role.id,
|
||||
resource_id: storedResourcesSet.get(resource.resource_slug),
|
||||
permission_id: storedPermissionsSet.get(perm),
|
||||
})));
|
||||
|
||||
if (roleHasPerms.length > 0) {
|
||||
await knex('role_has_permissions').insert(roleHasPerms[0]);
|
||||
}
|
||||
return res.status(200).send({ id: role.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the give role.
|
||||
*/
|
||||
editRole: {
|
||||
validation: [
|
||||
check('name').exists().trim().escape(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('permissions').isArray({ min: 0 }),
|
||||
check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'),
|
||||
check('permissions.*.permissions').isArray({ min: 1 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const role = await Role.where('id', id).fetch();
|
||||
|
||||
if (!role) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
const { permissions } = req.body;
|
||||
const errorReasons = [];
|
||||
const permissionsSlugs = [];
|
||||
const notFoundPermissions = [];
|
||||
|
||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||
|
||||
if (resourcesNotFound.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RESOURCE_SLUG_NOT_FOUND',
|
||||
code: 100,
|
||||
resources: resourcesNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
permissions.forEach((perm) => {
|
||||
const abilities = perm.permissions.map((ability) => ability);
|
||||
// Gets the not found permissions in the schema.
|
||||
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
if (notFoundAbilities.length > 0) {
|
||||
notFoundPermissions.push({
|
||||
resource_slug: perm.resource_slug, permissions: notFoundAbilities,
|
||||
});
|
||||
} else {
|
||||
const perms = perm.permissions || [];
|
||||
perms.forEach((permission) => {
|
||||
if (perms.indexOf(permission) !== -1) {
|
||||
permissionsSlugs.push(permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (notFoundPermissions.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PERMISSIONS_SLUG_NOT_FOUND',
|
||||
code: 200,
|
||||
permissions: notFoundPermissions,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
// Permissions.
|
||||
const [resourcesCollection, permsCollection] = await Promise.all([
|
||||
Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const notStoredResources = difference(
|
||||
resourcesSlugs, resourcesCollection.map((s) => s.name),
|
||||
);
|
||||
const notStoredPermissions = difference(
|
||||
permissionsSlugs, permsCollection.map((perm) => perm.slug),
|
||||
);
|
||||
const insertThread = [];
|
||||
|
||||
if (notStoredResources.length > 0) {
|
||||
insertThread.push(knex('resources').insert([
|
||||
...notStoredResources.map((resource) => ({ name: resource })),
|
||||
]));
|
||||
}
|
||||
if (notStoredPermissions.length > 0) {
|
||||
insertThread.push(knex('permissions').insert([
|
||||
...notStoredPermissions.map((permission) => ({ name: permission })),
|
||||
]));
|
||||
}
|
||||
|
||||
await Promise.all(insertThread);
|
||||
|
||||
const [storedPermissions, storedResources] = await Promise.all([
|
||||
Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(),
|
||||
Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(),
|
||||
]);
|
||||
|
||||
const storedResourcesSet = new Map(storedResources.map((resource) => [
|
||||
resource.attributes.name, resource.attributes.id,
|
||||
]));
|
||||
const storedPermissionsSet = new Map(storedPermissions.map((perm) => [
|
||||
perm.attributes.name, perm.attributes.id,
|
||||
]));
|
||||
|
||||
await role.save();
|
||||
|
||||
|
||||
const savedRoleHasPerms = await knex('role_has_permissions').where({
|
||||
role_id: role.id,
|
||||
});
|
||||
|
||||
console.log(savedRoleHasPerms);
|
||||
|
||||
// const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({
|
||||
// role_id: role.id,
|
||||
// resource_id: storedResourcesSet.get(resource.resource_slug),
|
||||
// permission_id: storedPermissionsSet.get(perm),
|
||||
// })));
|
||||
|
||||
// if (roleHasPerms.length > 0) {
|
||||
// await knex('role_has_permissions').insert(roleHasPerms[0]);
|
||||
// }
|
||||
return res.status(200).send({ id: role.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
deleteRole: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const role = await Role.where('id', id).fetch();
|
||||
|
||||
if (!role) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
if (role.attributes.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ROLE_PREDEFINED', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
await knex('role_has_permissions')
|
||||
.where('role_id', role.id).delete({ require: false });
|
||||
|
||||
await role.destroy();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
getRole: {
|
||||
validation: [],
|
||||
handler(req, res) {
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
};
|
||||
89
server/src/http/controllers/Settings.ts
Normal file
89
server/src/http/controllers/Settings.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { body, query, validationResult } from 'express-validator';
|
||||
import { pick } from 'lodash';
|
||||
import { IOptionDTO, IOptionsDTO } from '@/interfaces';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default class SettingsController extends BaseController{
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/',
|
||||
this.saveSettingsValidationSchema,
|
||||
asyncMiddleware(this.saveSettings.bind(this)));
|
||||
|
||||
router.get('/',
|
||||
this.getSettingsSchema,
|
||||
asyncMiddleware(this.getSettings.bind(this)));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings validation schema.
|
||||
*/
|
||||
get saveSettingsValidationSchema() {
|
||||
return [
|
||||
body('options').isArray({ min: 1 }),
|
||||
body('options.*.key').exists(),
|
||||
body('options.*.value').exists(),
|
||||
body('options.*.group').exists(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
get getSettingsSchema() {
|
||||
return [
|
||||
query('key').optional(),
|
||||
query('group').optional(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given options to the storage.
|
||||
*/
|
||||
saveSettings(req: Request, res: Response) {
|
||||
const { Option } = req.models;
|
||||
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
|
||||
const { settings } = req;
|
||||
|
||||
const errorReasons: { type: string, code: number, keys: [] }[] = [];
|
||||
const notDefinedOptions = Option.validateDefined(optionsDTO.options);
|
||||
|
||||
if (notDefinedOptions.length) {
|
||||
errorReasons.push({
|
||||
type: 'OPTIONS.KEY.NOT.DEFINED',
|
||||
code: 200,
|
||||
keys: notDefinedOptions.map((o) => ({
|
||||
...pick(o, ['key', 'group'])
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (errorReasons.length) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
optionsDTO.options.forEach((option: IOptionDTO) => {
|
||||
settings.set({ ...option });
|
||||
});
|
||||
|
||||
return res.status(200).send({ });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve settings.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
getSettings(req: Request, res: Response) {
|
||||
const { settings } = req;
|
||||
const allSettings = settings.all();
|
||||
|
||||
return res.status(200).send({ settings: allSettings });
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { Plan } from '@/system/models';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||
|
||||
export default class PaymentMethodController {
|
||||
export default class PaymentMethodController extends BaseController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
@@ -16,7 +17,7 @@ export default class PaymentMethodController {
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
|
||||
const { planSlug } = req.body;
|
||||
const { planSlug } = this.matchedBodyData(req);
|
||||
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
||||
|
||||
if (!foundPlan) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { check, param, query, ValidationSchema } from 'express-validator';
|
||||
import { Voucher, Plan } from '@/system/models';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
|
||||
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
|
||||
import {
|
||||
NotAllowedChangeSubscriptionPlan
|
||||
} from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class PaymentViaVoucherController extends PaymentMethodController {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -22,7 +24,6 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
'/payment',
|
||||
this.paymentViaVoucherSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
|
||||
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
||||
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
|
||||
@@ -48,7 +49,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
* @param {Response} res
|
||||
*/
|
||||
async validateVoucherCodeExistance(req: Request, res: Response, next: Function) {
|
||||
const { voucherCode } = req.body;
|
||||
const { voucherCode } = this.matchedBodyData(req);
|
||||
this.logger.info('[voucher_payment] trying to validate voucher code.', { voucherCode });
|
||||
|
||||
const foundVoucher = await Voucher.query()
|
||||
.modify('filterActiveVoucher')
|
||||
@@ -70,7 +72,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateVoucherAndPlan(req: Request, res: Response, next: Function) {
|
||||
const { planSlug, voucherCode } = req.body;
|
||||
const { planSlug, voucherCode } = this.matchedBodyData(req);
|
||||
this.logger.info('[voucher_payment] trying to validate voucher with the plan.', { voucherCode });
|
||||
|
||||
const voucher = await Voucher.query().findOne('voucher_code', voucherCode);
|
||||
const plan = await Plan.query().findOne('slug', planSlug);
|
||||
@@ -90,11 +93,12 @@ export default class PaymentViaVoucherController extends PaymentMethodController
|
||||
* @return {Response}
|
||||
*/
|
||||
async paymentViaVoucher(req: Request, res: Response, next: Function) {
|
||||
const { planSlug, voucherCode } = req.body;
|
||||
const { planSlug, voucherCode } = this.matchedBodyData(req);
|
||||
const { tenant } = req;
|
||||
|
||||
try {
|
||||
await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
|
||||
await this.subscriptionService
|
||||
.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'PAYMENT.SUCCESSFULLY.MADE',
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import { repeat, times, orderBy } from 'lodash';
|
||||
import { check, oneOf, param, query, ValidationChain } from 'express-validator';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import { check, oneOf, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Voucher, Plan } from '@/system/models';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import VoucherService from '@/services/Payment/Voucher';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import PrettierMiddleware from '@/http/middleware/prettierMiddleware';
|
||||
import { IVouchersFilter } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class VouchersController {
|
||||
export default class VouchersController extends BaseController {
|
||||
@Inject()
|
||||
voucherService: VoucherService;
|
||||
|
||||
@@ -24,14 +23,12 @@ export default class VouchersController {
|
||||
'/generate',
|
||||
this.generateVoucherSchema,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validatePlanExistance.bind(this)),
|
||||
asyncMiddleware(this.generateVoucher.bind(this)),
|
||||
);
|
||||
router.post(
|
||||
'/disable/:voucherId',
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
||||
asyncMiddleware(this.validateNotDisabledVoucher.bind(this)),
|
||||
asyncMiddleware(this.disableVoucher.bind(this)),
|
||||
@@ -40,18 +37,15 @@ export default class VouchersController {
|
||||
'/send',
|
||||
this.sendVoucherSchemaValidation,
|
||||
validateMiddleware,
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.sendVoucher.bind(this)),
|
||||
);
|
||||
router.delete(
|
||||
'/:voucherId',
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.validateVoucherExistance.bind(this)),
|
||||
asyncMiddleware(this.deleteVoucher.bind(this)),
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
PrettierMiddleware,
|
||||
asyncMiddleware(this.listVouchers.bind(this)),
|
||||
);
|
||||
return router;
|
||||
@@ -106,7 +100,8 @@ export default class VouchersController {
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validatePlanExistance(req: Request, res: Response, next: Function) {
|
||||
const planId: number = req.body.planId || req.params.planId;
|
||||
const body = this.matchedBodyData(req);
|
||||
const planId: number = body.planId || req.params.planId;
|
||||
const foundPlan = await Plan.query().findById(planId);
|
||||
|
||||
if (!foundPlan) {
|
||||
@@ -124,7 +119,9 @@ export default class VouchersController {
|
||||
* @param {Function}
|
||||
*/
|
||||
async validateVoucherExistance(req: Request, res: Response, next: Function) {
|
||||
const voucherId = req.body.voucherId || req.params.voucherId;
|
||||
const body = this.matchedBodyData(req);
|
||||
|
||||
const voucherId = body.voucherId || req.params.voucherId;
|
||||
const foundVoucher = await Voucher.query().findById(voucherId);
|
||||
|
||||
if (!foundVoucher) {
|
||||
@@ -160,7 +157,7 @@ export default class VouchersController {
|
||||
* @return {Response}
|
||||
*/
|
||||
async generateVoucher(req: Request, res: Response, next: Function) {
|
||||
const { loop = 10, period, periodInterval, planId } = req.body;
|
||||
const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.voucherService.generateVouchers(
|
||||
@@ -211,7 +208,7 @@ export default class VouchersController {
|
||||
* @return {Response}
|
||||
*/
|
||||
async sendVoucher(req: Request, res: Response) {
|
||||
const { phoneNumber, email, period, periodInterval, planId } = req.body;
|
||||
const { phoneNumber, email, period, periodInterval, planId } = this.matchedBodyData(req);
|
||||
|
||||
const voucher = await Voucher.query()
|
||||
.modify('filterActiveVoucher')
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
||||
import { Container, Service } from 'typedi';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
|
||||
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
|
||||
|
||||
@Service()
|
||||
@@ -13,6 +14,7 @@ export default class SubscriptionController {
|
||||
const router = Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/voucher', Container.get(PaymentViaVoucherController).router());
|
||||
|
||||
Reference in New Issue
Block a user