mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
Permissions authorization middleware.
This commit is contained in:
@@ -11,4 +11,6 @@ DB_CLIENT=mysql
|
|||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=root
|
||||||
DB_NAME=moosher
|
DB_NAME=moosher
|
||||||
|
|
||||||
|
JWT_SECRET_KEY=ahmedmohamked
|
||||||
@@ -21,12 +21,14 @@
|
|||||||
"bookshelf-json-columns": "^2.1.1",
|
"bookshelf-json-columns": "^2.1.1",
|
||||||
"bookshelf-modelbase": "^2.10.4",
|
"bookshelf-modelbase": "^2.10.4",
|
||||||
"bookshelf-paranoia": "^0.13.1",
|
"bookshelf-paranoia": "^0.13.1",
|
||||||
|
"csurf": "^1.10.0",
|
||||||
"dotenv": "^8.1.0",
|
"dotenv": "^8.1.0",
|
||||||
"errorhandler": "^1.5.1",
|
"errorhandler": "^1.5.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-boom": "^3.0.0",
|
"express-boom": "^3.0.0",
|
||||||
"express-oauth-server": "^2.0.0",
|
"express-oauth-server": "^2.0.0",
|
||||||
"express-validator": "^6.2.0",
|
"express-validator": "^6.2.0",
|
||||||
|
"helmet": "^3.21.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"knex": "^0.19.2",
|
"knex": "^0.19.2",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"mustache": "^3.0.3",
|
"mustache": "^3.0.3",
|
||||||
"mysql2": "^1.6.5",
|
"mysql2": "^1.6.5",
|
||||||
|
"node-cache": "^4.2.1",
|
||||||
"nodemailer": "^6.3.0",
|
"nodemailer": "^6.3.0",
|
||||||
"nodemon": "^1.19.1"
|
"nodemon": "^1.19.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
import boom from 'express-boom';
|
import boom from 'express-boom';
|
||||||
import '../config';
|
import '../config';
|
||||||
import routes from '@/http';
|
import routes from '@/http';
|
||||||
@@ -8,6 +9,7 @@ const app = express();
|
|||||||
// Express configuration
|
// Express configuration
|
||||||
app.set('port', process.env.PORT || 3000);
|
app.set('port', process.env.PORT || 3000);
|
||||||
|
|
||||||
|
app.use(helmet());
|
||||||
app.use(boom());
|
app.use(boom());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { check, validationResult, oneOf } from 'express-validator';
|
import { check, validationResult, oneOf } from 'express-validator';
|
||||||
import { difference } from 'lodash';
|
import { difference } from 'lodash';
|
||||||
|
import knex from 'knex';
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
// import AccountBalance from '@/models/AccountBalance';
|
import '@/models/AccountBalance';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +36,7 @@ export default {
|
|||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
// const defaultCurrency = 'USD';
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
return res.boom.badData(null, {
|
return res.boom.badData(null, {
|
||||||
@@ -48,11 +50,12 @@ export default {
|
|||||||
const accountsCollection = await Account.query((query) => {
|
const accountsCollection = await Account.query((query) => {
|
||||||
query.select(['id']);
|
query.select(['id']);
|
||||||
query.whereIn('id', accountsIds);
|
query.whereIn('id', accountsIds);
|
||||||
}).fetchAll();
|
}).fetchAll({
|
||||||
|
withRelated: ['balances'],
|
||||||
|
});
|
||||||
|
|
||||||
const accountsStoredIds = accountsCollection.map((account) => account.attributes.id);
|
const accountsStoredIds = accountsCollection.map((account) => account.attributes.id);
|
||||||
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
|
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
|
||||||
|
|
||||||
const errorReasons = [];
|
const errorReasons = [];
|
||||||
|
|
||||||
if (notFoundAccountsIds.length > 0) {
|
if (notFoundAccountsIds.length > 0) {
|
||||||
@@ -64,6 +67,29 @@ export default {
|
|||||||
return res.boom.badData(null, { errors: errorReasons });
|
return res.boom.badData(null, { errors: errorReasons });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storedAccountsBalances = accountsCollection.related('balances');
|
||||||
|
|
||||||
|
const submitBalancesMap = new Map(accounts.map((account) => [account, account.id]));
|
||||||
|
const storedBalancesMap = new Map(storedAccountsBalances.map((balance) => [
|
||||||
|
balance.attributes, balance.attributes.id,
|
||||||
|
]));
|
||||||
|
|
||||||
|
// const updatedStoredBalanced = [];
|
||||||
|
const notStoredBalances = [];
|
||||||
|
|
||||||
|
accountsIds.forEach((id) => {
|
||||||
|
if (!storedBalancesMap.get(id)) {
|
||||||
|
notStoredBalances.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex('accounts_balances').insert([
|
||||||
|
...notStoredBalances.map((id) => {
|
||||||
|
const account = submitBalancesMap.get(id);
|
||||||
|
return { ...account };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ export default {
|
|||||||
if (!account) {
|
if (!account) {
|
||||||
return res.boom.notFound();
|
return res.boom.notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({ item: { ...account.attributes } });
|
return res.status(200).send({ item: { ...account.attributes } });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { check, validationResult } from 'express-validator';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
import User from '@/models/User';
|
import User from '@/models/User';
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||||
import PasswordReset from '@/models/PasswordReset';
|
import PasswordReset from '@/models/PasswordReset';
|
||||||
@@ -49,6 +50,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { crediential, password } = req.body;
|
const { crediential, password } = req.body;
|
||||||
|
const { JWT_SECRET_KEY } = process.env;
|
||||||
|
|
||||||
const user = await User.query({
|
const user = await User.query({
|
||||||
where: { email: crediential },
|
where: { email: crediential },
|
||||||
@@ -70,8 +72,15 @@ export default {
|
|||||||
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
user.save({ alst_login_at: new Date() });
|
user.save({ last_login_at: new Date() });
|
||||||
return res.status(200).send({});
|
|
||||||
|
const token = jwt.sign({
|
||||||
|
email: user.attributes.email,
|
||||||
|
_id: user.id,
|
||||||
|
}, JWT_SECRET_KEY, {
|
||||||
|
expiresIn: '1d',
|
||||||
|
});
|
||||||
|
return res.status(200).send({ token });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import express from 'express';
|
|||||||
import { check, param, validationResult } from 'express-validator';
|
import { check, param, validationResult } from 'express-validator';
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||||
import ItemCategory from '@/models/ItemCategory';
|
import ItemCategory from '@/models/ItemCategory';
|
||||||
// import JWTAuth from '@/http/middleware/jwtAuth';
|
import Authorization from '@/http/middleware/authorization';
|
||||||
|
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -10,24 +11,32 @@ export default {
|
|||||||
*/
|
*/
|
||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const permit = Authorization('items_categories');
|
||||||
|
|
||||||
|
router.use(JWTAuth);
|
||||||
|
|
||||||
router.post('/:id',
|
router.post('/:id',
|
||||||
|
permit('create', 'edit'),
|
||||||
this.editCategory.validation,
|
this.editCategory.validation,
|
||||||
asyncMiddleware(this.editCategory.handler));
|
asyncMiddleware(this.editCategory.handler));
|
||||||
|
|
||||||
router.post('/',
|
router.post('/',
|
||||||
|
permit('create'),
|
||||||
this.newCategory.validation,
|
this.newCategory.validation,
|
||||||
asyncMiddleware(this.newCategory.handler));
|
asyncMiddleware(this.newCategory.handler));
|
||||||
|
|
||||||
router.delete('/:id',
|
router.delete('/:id',
|
||||||
|
permit('create', 'edit', 'delete'),
|
||||||
this.deleteItem.validation,
|
this.deleteItem.validation,
|
||||||
asyncMiddleware(this.deleteItem.handler));
|
asyncMiddleware(this.deleteItem.handler));
|
||||||
|
|
||||||
// router.get('/:id',
|
router.get('/:id',
|
||||||
// this.getCategory.validation,
|
permit('view'),
|
||||||
// asyncMiddleware(this.getCategory.handler));
|
this.getCategory.validation,
|
||||||
|
asyncMiddleware(this.getCategory.handler));
|
||||||
|
|
||||||
router.get('/',
|
router.get('/',
|
||||||
|
permit('view'),
|
||||||
this.getList.validation,
|
this.getList.validation,
|
||||||
asyncMiddleware(this.getList.validation));
|
asyncMiddleware(this.getList.validation));
|
||||||
|
|
||||||
@@ -152,6 +161,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve details of the given category.
|
||||||
|
*/
|
||||||
getCategory: {
|
getCategory: {
|
||||||
validation: [
|
validation: [
|
||||||
param('category_id').toInt(),
|
param('category_id').toInt(),
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { check, validationResult } from 'express-validator';
|
import { check, validationResult } from 'express-validator';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import { difference } from 'lodash';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||||
import Item from '@/models/Item';
|
import Item from '@/models/Item';
|
||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
import ItemCategory from '@/models/ItemCategory';
|
import ItemCategory from '@/models/ItemCategory';
|
||||||
|
import Resource from '@/models/Resource';
|
||||||
|
import ResourceField from '@/models/ResourceField';
|
||||||
|
import Authorization from '@/http/middleware/authorization';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const permit = Authorization('items');
|
||||||
|
|
||||||
|
router.use(jwtAuth);
|
||||||
|
|
||||||
// router.post('/:id',
|
// router.post('/:id',
|
||||||
// this.editItem.validation,
|
// this.editItem.validation,
|
||||||
// asyncMiddleware(this.editCategory.handler));
|
// asyncMiddleware(this.editCategory.handler));
|
||||||
|
|
||||||
router.post('/',
|
router.post('/',
|
||||||
|
permit('create'),
|
||||||
this.newItem.validation,
|
this.newItem.validation,
|
||||||
asyncMiddleware(this.newItem.handler));
|
asyncMiddleware(this.newItem.handler));
|
||||||
|
|
||||||
@@ -46,6 +55,11 @@ export default {
|
|||||||
check('cost_account_id').exists().isInt(),
|
check('cost_account_id').exists().isInt(),
|
||||||
check('sell_account_id').exists().isInt(),
|
check('sell_account_id').exists().isInt(),
|
||||||
check('category_id').optional().isInt(),
|
check('category_id').optional().isInt(),
|
||||||
|
|
||||||
|
check('custom_fields').isArray({ min: 1 }),
|
||||||
|
check('custom_fields.*.key').exists().isNumeric().toInt(),
|
||||||
|
check('custom_fields.*.value').exists(),
|
||||||
|
|
||||||
check('note').optional(),
|
check('note').optional(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
@@ -58,19 +72,38 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
|
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
|
||||||
const { category_id: categoryId } = req.body;
|
const { category_id: categoryId, custom_fields: customFields } = req.body;
|
||||||
|
const errorReasons = [];
|
||||||
|
|
||||||
const costAccountPromise = Account.where('id', costAccountId).fetch();
|
const costAccountPromise = Account.where('id', costAccountId).fetch();
|
||||||
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
|
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
|
||||||
const itemCategoryPromise = (categoryId)
|
const itemCategoryPromise = (categoryId)
|
||||||
? ItemCategory.where('id', categoryId).fetch() : null;
|
? ItemCategory.where('id', categoryId).fetch() : null;
|
||||||
|
|
||||||
|
// Validate the custom fields key and value type.
|
||||||
|
if (customFields.length > 0) {
|
||||||
|
const customFieldsKeys = customFields.map((field) => field.key);
|
||||||
|
|
||||||
|
// Get resource id than get all resource fields.
|
||||||
|
const resource = await Resource.where('name', 'items').fetch();
|
||||||
|
const fields = await ResourceField.query((query) => {
|
||||||
|
query.where('resource_id', resource.id);
|
||||||
|
query.whereIn('key', customFieldsKeys);
|
||||||
|
}).fetchAll();
|
||||||
|
|
||||||
|
const storedFieldsKey = fields.map((f) => f.attributes.key);
|
||||||
|
|
||||||
|
// Get all not defined resource fields.
|
||||||
|
const notFoundFields = difference(customFieldsKeys, storedFieldsKey);
|
||||||
|
|
||||||
|
if (notFoundFields.length > 0) {
|
||||||
|
errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [costAccount, sellAccount, itemCategory] = await Promise.all([
|
const [costAccount, sellAccount, itemCategory] = await Promise.all([
|
||||||
costAccountPromise, sellAccountPromise, itemCategoryPromise,
|
costAccountPromise, sellAccountPromise, itemCategoryPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const errorReasons = [];
|
|
||||||
|
|
||||||
if (!costAccount) {
|
if (!costAccount) {
|
||||||
errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 });
|
errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 });
|
||||||
}
|
}
|
||||||
@@ -92,7 +125,6 @@ export default {
|
|||||||
currency_code: req.body.currency_code,
|
currency_code: req.body.currency_code,
|
||||||
note: req.body.note,
|
note: req.body.note,
|
||||||
});
|
});
|
||||||
|
|
||||||
await item.save();
|
await item.save();
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import express from 'express';
|
|||||||
import { check, validationResult } from 'express-validator';
|
import { check, validationResult } from 'express-validator';
|
||||||
import User from '@/models/User';
|
import User from '@/models/User';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||||
|
import Authorization from '@/http/middleware/authorization';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
@@ -10,24 +12,32 @@ export default {
|
|||||||
*/
|
*/
|
||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const permit = Authorization('users');
|
||||||
|
|
||||||
|
router.use(jwtAuth);
|
||||||
|
|
||||||
router.post('/',
|
router.post('/',
|
||||||
|
permit('create'),
|
||||||
this.newUser.validation,
|
this.newUser.validation,
|
||||||
asyncMiddleware(this.newUser.handler));
|
asyncMiddleware(this.newUser.handler));
|
||||||
|
|
||||||
router.post('/:id',
|
router.post('/:id',
|
||||||
|
permit('create', 'edit'),
|
||||||
this.editUser.validation,
|
this.editUser.validation,
|
||||||
asyncMiddleware(this.editUser.handler));
|
asyncMiddleware(this.editUser.handler));
|
||||||
|
|
||||||
// router.get('/',
|
router.get('/',
|
||||||
// this.listUsers.validation,
|
permit('view'),
|
||||||
// asyncMiddleware(this.listUsers.handler));
|
this.listUsers.validation,
|
||||||
|
asyncMiddleware(this.listUsers.handler));
|
||||||
|
|
||||||
// router.get('/:id',
|
router.get('/:id',
|
||||||
// this.getUser.validation,
|
permit('view'),
|
||||||
// asyncMiddleware(this.getUser.handler));
|
this.getUser.validation,
|
||||||
|
asyncMiddleware(this.getUser.handler));
|
||||||
|
|
||||||
router.delete('/:id',
|
router.delete('/:id',
|
||||||
|
permit('create', 'edit', 'delete'),
|
||||||
this.deleteUser.validation,
|
this.deleteUser.validation,
|
||||||
asyncMiddleware(this.deleteUser.handler));
|
asyncMiddleware(this.deleteUser.handler));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
|
/* eslint-disable consistent-return */
|
||||||
const authorization = (req, res, next) => {
|
const authorization = (resourceName) => (...permissions) => (req, res, next) => {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
};
|
const onError = () => {
|
||||||
|
res.boom.unauthorized();
|
||||||
|
};
|
||||||
|
user.hasPermissions(resourceName, permissions)
|
||||||
|
.then((authorized) => {
|
||||||
|
if (!authorized) {
|
||||||
|
return onError();
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}).catch(onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authorization;
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
/* eslint-disable consistent-return */
|
/* eslint-disable consistent-return */
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import User from '@/models/User';
|
import User from '@/models/User';
|
||||||
import Auth from '@/models/Auth';
|
// import Auth from '@/models/Auth';
|
||||||
|
|
||||||
const authMiddleware = (req, res, next) => {
|
const authMiddleware = (req, res, next) => {
|
||||||
|
const { JWT_SECRET_KEY } = process.env;
|
||||||
const token = req.headers['x-access-token'] || req.query.token;
|
const token = req.headers['x-access-token'] || req.query.token;
|
||||||
|
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
Auth.loggedOut();
|
// Auth.loggedOut();
|
||||||
res.status(401).send({
|
res.status(401).send({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'unauthorized',
|
message: 'unauthorized',
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return onError();
|
return onError();
|
||||||
}
|
}
|
||||||
const { JWT_SECRET_KEY } = process.env;
|
|
||||||
|
|
||||||
const verify = new Promise((resolve, reject) => {
|
const verify = new Promise((resolve, reject) => {
|
||||||
jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => {
|
jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => {
|
||||||
@@ -26,7 +26,7 @@ const authMiddleware = (req, res, next) => {
|
|||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
req.user = await User.where('id', decoded._id).fetch();
|
req.user = await User.where('id', decoded._id).fetch();
|
||||||
Auth.setAuthenticatedUser(req.user);
|
// Auth.setAuthenticatedUser(req.user);
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return onError();
|
return onError();
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ const Account = bookshelf.Model.extend({
|
|||||||
return this.belongsTo('AccountType', 'account_type_id');
|
return this.belongsTo('AccountType', 'account_type_id');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account model may has many balances accounts.
|
||||||
|
*/
|
||||||
balances() {
|
balances() {
|
||||||
return this.hasMany('AccountBalance', 'accounnt_id');
|
return this.hasMany('AccountBalance', 'account_id');
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import bookshelf from './bookshelf';
|
import bookshelf from './bookshelf';
|
||||||
|
|
||||||
const Item = bookshelf.Model.extend({
|
const Item = bookshelf.Model.extend({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -25,6 +24,11 @@ const Item = bookshelf.Model.extend({
|
|||||||
category() {
|
category() {
|
||||||
return this.belongsTo('ItemCategory', 'category_id');
|
return this.belongsTo('ItemCategory', 'category_id');
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
/**
|
||||||
|
* Cascade delete dependents.
|
||||||
|
*/
|
||||||
|
dependents: ['ItemMetadata'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default bookshelf.model('Item', Item);
|
export default bookshelf.model('Item', Item);
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ const Resource = bookshelf.Model.extend({
|
|||||||
fields() {
|
fields() {
|
||||||
return this.hasMany('ResourceField', 'resource_id');
|
return this.hasMany('ResourceField', 'resource_id');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
permissions() {
|
||||||
|
return this.belongsToMany('Permission', 'role_has_permissions', 'resource_id', 'permission_id');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default bookshelf.model('Resource', Resource);
|
export default bookshelf.model('Resource', Resource);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import bookshelf from './bookshelf';
|
import bookshelf from './bookshelf';
|
||||||
|
import PermissionsService from '@/services/PermissionsService';
|
||||||
|
|
||||||
const User = bookshelf.Model.extend({
|
const User = bookshelf.Model.extend({
|
||||||
|
...PermissionsService,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
@@ -13,6 +15,10 @@ const User = bookshelf.Model.extend({
|
|||||||
*/
|
*/
|
||||||
hasTimestamps: ['created_at', 'updated_at'],
|
hasTimestamps: ['created_at', 'updated_at'],
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.initializeCache();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify the password of the user.
|
* Verify the password of the user.
|
||||||
* @param {String} password - The given password.
|
* @param {String} password - The given password.
|
||||||
|
|||||||
75
server/src/services/PermissionsService.js
Normal file
75
server/src/services/PermissionsService.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import cache from 'memory-cache';
|
||||||
|
import { difference } from 'lodash';
|
||||||
|
import Role from '@/models/Role';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
cacheKey: 'ratteb.cache,',
|
||||||
|
cacheExpirationTime: null,
|
||||||
|
permissions: [],
|
||||||
|
cache: null,
|
||||||
|
|
||||||
|
initializeCache() {
|
||||||
|
if (!this.cache) {
|
||||||
|
this.cache = new cache.Cache();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge all cached permissions.
|
||||||
|
*/
|
||||||
|
forgetCachePermissions() {
|
||||||
|
this.cache.del(this.cacheKey);
|
||||||
|
this.permissions = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all stored permissions.
|
||||||
|
*/
|
||||||
|
async getPermissions() {
|
||||||
|
if (this.permissions.length <= 0) {
|
||||||
|
const cachedPerms = this.cache.get(this.cacheKey);
|
||||||
|
|
||||||
|
if (!cachedPerms) {
|
||||||
|
this.permissions = await this.getPermissionsFromStorage();
|
||||||
|
this.cache.put(this.cacheKey, this.permissions);
|
||||||
|
} else {
|
||||||
|
this.permissions = cachedPerms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.permissions;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all roles and permissions from the storage.
|
||||||
|
*/
|
||||||
|
async getPermissionsFromStorage() {
|
||||||
|
const roles = await Role.fetchAll({
|
||||||
|
withRelated: ['resources.permissions'],
|
||||||
|
});
|
||||||
|
return roles.toJSON();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmine the given resource has the permissions.
|
||||||
|
* @param {String} resource -
|
||||||
|
* @param {Array} permissions -
|
||||||
|
*/
|
||||||
|
async hasPermissions(resource, permissions) {
|
||||||
|
await this.getPermissions();
|
||||||
|
|
||||||
|
const userRoles = this.permissions.filter((role) => role.id === this.id);
|
||||||
|
const perms = [];
|
||||||
|
|
||||||
|
userRoles.forEach((role) => {
|
||||||
|
const roleResources = role.resources || [];
|
||||||
|
const foundResource = roleResources.find((r) => r.name === resource);
|
||||||
|
|
||||||
|
if (foundResource && foundResource.permissions) {
|
||||||
|
foundResource.permissions.forEach((p) => perms.push(p.name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const notAllowedPerms = difference(permissions, perms);
|
||||||
|
return (notAllowedPerms.length <= 0);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,13 +3,44 @@ import User from '@/models/User';
|
|||||||
import '@/models/Role';
|
import '@/models/Role';
|
||||||
|
|
||||||
describe('Model: User', () => {
|
describe('Model: User', () => {
|
||||||
it('User model may has many associated roles.', async () => {
|
describe('relations', () => {
|
||||||
const userHasRole = await create('user_has_role');
|
it('User model may has many associated roles.', async () => {
|
||||||
await create('user_has_role', { user_id: userHasRole.user_id });
|
const userHasRole = await create('user_has_role');
|
||||||
|
await create('user_has_role', { user_id: userHasRole.user_id });
|
||||||
|
|
||||||
const userModel = await User.where('id', userHasRole.user_id).fetch();
|
const userModel = await User.where('id', userHasRole.user_id).fetch();
|
||||||
const userRoles = await userModel.roles().fetch();
|
const userRoles = await userModel.roles().fetch();
|
||||||
|
|
||||||
expect(userRoles).to.have.lengthOf(2);
|
expect(userRoles).to.have.lengthOf(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasPermissions', () => {
|
||||||
|
it('Should return true in case user has the given permissions.', async () => {
|
||||||
|
const resource = await create('resource');
|
||||||
|
const permission = await create('permission');
|
||||||
|
const roleHasPerms = await create('role_has_permission', {
|
||||||
|
resource_id: resource.id,
|
||||||
|
permission_id: permission.id,
|
||||||
|
});
|
||||||
|
const userHasRole = await create('user_has_role', { role_id: roleHasPerms.role_id });
|
||||||
|
await create('user_has_role', { user_id: userHasRole.user_id });
|
||||||
|
|
||||||
|
const userModel = await User.where('id', userHasRole.user_id).fetch();
|
||||||
|
const hasPermission = await userModel.hasPermissions(resource.name, [permission.name]);
|
||||||
|
|
||||||
|
expect(hasPermission).to.equals(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return false in case user has no the given permissions.', async () => {
|
||||||
|
const roleHasPerms = await create('role_has_permission');
|
||||||
|
const userHasRole = await create('user_has_role', { role_id: roleHasPerms.role_id });
|
||||||
|
await create('user_has_role', { user_id: userHasRole.user_id });
|
||||||
|
|
||||||
|
const userModel = await User.where('id', userHasRole.user_id).fetch();
|
||||||
|
const hasPermission = await userModel.hasPermissions('resource', ['permission']);
|
||||||
|
|
||||||
|
expect(hasPermission).to.equals(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { request, expect } from '~/testInit';
|
import { request, expect, create } from '~/testInit';
|
||||||
|
|
||||||
describe('routes: `/accountOpeningBalance`', () => {
|
describe('routes: `/accountOpeningBalance`', () => {
|
||||||
describe('POST `/accountOpeningBalance`', () => {
|
describe('POST `/accountOpeningBalance`', () => {
|
||||||
@@ -40,5 +40,16 @@ describe('routes: `/accountOpeningBalance`', () => {
|
|||||||
type: 'NOT_FOUND_ACCOUNT', code: 100, ids: [100],
|
type: 'NOT_FOUND_ACCOUNT', code: 100, ids: [100],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should store the given credit and debit to the account balance in the storage.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const res = await request().post('/api/accountOpeningBalance').send({
|
||||||
|
accounts: [
|
||||||
|
{ id: account.id, credit: 100, debit: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(res.status);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ describe('routes: /auth/', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should autheticate success with correct phone number and password.', async () => {
|
it('Should autheticate success with correct phone number and password.', async () => {
|
||||||
const password = hashPassword('admin');
|
const password = await hashPassword('admin');
|
||||||
const user = await create('user', {
|
const user = await create('user', {
|
||||||
phone_number: '0920000000',
|
phone_number: '0920000000',
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
const res = await request().post('/api/auth/login').send({
|
const res = await request().post('/api/auth/login').send({
|
||||||
crediential: user.phone_number,
|
crediential: user.email,
|
||||||
password,
|
password: 'admin',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).equals(200);
|
expect(res.status).equals(200);
|
||||||
|
|||||||
10
server/tests/routes/authorization.test.js
Normal file
10
server/tests/routes/authorization.test.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
describe('Authorization', () => {
|
||||||
|
it('Should response unauthorized in case use has no role has permissions to the given resource.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should response authorized in case user has role has all permissions.', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
import { request, expect, create } from '~/testInit';
|
import {
|
||||||
|
request,
|
||||||
|
expect,
|
||||||
|
create,
|
||||||
|
login,
|
||||||
|
} from '~/testInit';
|
||||||
import knex from '@/database/knex';
|
import knex from '@/database/knex';
|
||||||
|
|
||||||
describe('routes: `/items`', () => {
|
describe.only('routes: `/items`', () => {
|
||||||
describe('POST: `/items`', () => {
|
describe.only('POST: `/items`', () => {
|
||||||
it('Should not create a new item if the user was not authorized.', async () => {
|
it('Should not create a new item if the user was not authorized.', async () => {
|
||||||
|
const res = await request().post('/api/items').send();
|
||||||
|
|
||||||
|
expect(res.status).equals(401);
|
||||||
|
expect(res.body.message).equals('unauthorized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should user have create permission to create a new item.', async () => {
|
||||||
|
const loginRes = await login();
|
||||||
|
const res = await request().post('/api/items')
|
||||||
|
.set('x-access-token', loginRes.body.token).send();
|
||||||
|
|
||||||
|
expect(res.status).equals(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should `name` be required.', async () => {
|
it('Should `name` be required.', async () => {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ describe('routes: `/views`', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.only('POST: `/views/:view_id`', () => {
|
describe('POST: `/views/:view_id`', () => {
|
||||||
it('Should `label` be required.', async () => {
|
it('Should `label` be required.', async () => {
|
||||||
const view = await create('view');
|
const view = await create('view');
|
||||||
const res = await request().post(`/api/views/${view.id}`);
|
const res = await request().post(`/api/views/${view.id}`);
|
||||||
@@ -251,7 +251,7 @@ describe('routes: `/views`', () => {
|
|||||||
expect(res.status).equals(404);
|
expect(res.status).equals(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only('Should response the roles fields not exist in case role field was not exist.', async () => {
|
it('Should response the roles fields not exist in case role field was not exist.', async () => {
|
||||||
const view = await create('view');
|
const view = await create('view');
|
||||||
await create('resource_field', {
|
await create('resource_field', {
|
||||||
resource_id: view.resource_id,
|
resource_id: view.resource_id,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import chaiThings from 'chai-things';
|
|||||||
import app from '@/app';
|
import app from '@/app';
|
||||||
import knex from '@/database/knex';
|
import knex from '@/database/knex';
|
||||||
import factory from '@/database/factories';
|
import factory from '@/database/factories';
|
||||||
import { hashPassword } from '@/utils';
|
// import { hashPassword } from '@/utils';
|
||||||
|
|
||||||
const request = () => chai.request(app);
|
const request = () => chai.request(app);
|
||||||
const { expect } = chai;
|
const { expect } = chai;
|
||||||
@@ -22,13 +22,13 @@ chai.use(chaiHttp);
|
|||||||
chai.use(chaiThings);
|
chai.use(chaiThings);
|
||||||
|
|
||||||
const login = async (givenUser) => {
|
const login = async (givenUser) => {
|
||||||
const user = givenUser === null ? await factory.create('user') : givenUser;
|
const user = !givenUser ? await factory.create('user') : givenUser;
|
||||||
|
|
||||||
const response = await request()
|
const response = await request()
|
||||||
.post('/api/auth/login')
|
.post('/api/auth/login')
|
||||||
.send({
|
.send({
|
||||||
crediential: user.email,
|
crediential: user.email,
|
||||||
password: hashPassword('secret'),
|
password: 'admin',
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
Reference in New Issue
Block a user