├── .eslintrc ├── .gitignore ├── app.js ├── config └── default.json ├── controllers ├── booksListController.js ├── booksListController.test.js ├── controllerBase.js └── indexController.js ├── mock ├── books.js ├── db.js └── users.js ├── models ├── bookModel.js └── model.js ├── package-lock.json ├── package.json ├── repositories ├── bookRepository.js ├── repository.js ├── repositoryBase.js └── userRepository.js ├── routing ├── router.js ├── routesBuilders │ ├── booksListRoutesBuilder.js │ ├── indexRoutesBuilder.js │ └── routesBuilderBase.js ├── routesCollection.js └── uriGenerator.js ├── security ├── RBACAuthorization.js └── security.js ├── server.js └── services └── userService.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | // windows linebreaks when not in production environment 5 | "linebreak-style": ["error", "windows"], 6 | "indent": 0, 7 | "no-underscore-dangle": 0, 8 | "class-methods-use-this": 0, 9 | "consistent-return": 0, 10 | "no-cond-assign": 0, 11 | "no-param-reassign": 0, 12 | "object-curly-newline": 0, 13 | "eqeqeq": 0, 14 | "arrow-body-style": 0, 15 | "jest/no-disabled-tests": "warn", 16 | "jest/no-focused-tests": "error", 17 | "jest/no-identical-title": "error", 18 | "jest/prefer-to-have-length": "warn", 19 | "jest/valid-expect": "error" 20 | }, 21 | "plugins": ["jest"], 22 | "env": { 23 | "jest/globals": true 24 | } 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .idea 4 | *.log.* 5 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const config = require('config'); 4 | const URIGenerator = require('./routing/uriGenerator'); 5 | 6 | class App { 7 | constructor(router, repository, security) { 8 | this.router = router; 9 | this.repository = repository; 10 | this.security = security; 11 | this.port = config.get('api.port'); 12 | this.host = config.get('api.host'); 13 | this.express = express(); 14 | this.express.use(bodyParser.urlencoded({ extended: true })); 15 | this.express.use(bodyParser.json()); 16 | this.expressRouter = express.Router(); 17 | this._registerRoute = this._registerRoute.bind(this); 18 | this._createRouteBoundAction = this._createRouteBoundAction.bind(this); 19 | } 20 | 21 | _registerRoute(uri, httpMethod, boundAction) { 22 | this.expressRouter.route(uri)[httpMethod](boundAction); 23 | } 24 | 25 | _createRouteBoundAction(controllerClass, method, isSecure) { 26 | const result = [ 27 | (req, res) => { 28 | this._buildControllerInstance(controllerClass, req, res)[method](); 29 | }]; 30 | 31 | if (isSecure) { 32 | result.unshift( 33 | this.security.authenticate(), 34 | this.security.authorise(controllerClass.name, method), 35 | ); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | _buildControllerInstance(ControllerClass, req, res) { 42 | return new ControllerClass( 43 | { 44 | params: req.params, 45 | query: req.query, 46 | body: req.body, 47 | user: req.user, 48 | uriGenerator: new URIGenerator(this.security, req.user.role), 49 | repository: this.repository, 50 | send: (statusCode, resource) => { 51 | res.status(statusCode).send(resource); 52 | }, 53 | }, 54 | ); 55 | } 56 | 57 | run() { 58 | this.repository.registerRepositories(); 59 | this.router.registerRoutes(this._registerRoute, this._createRouteBoundAction); 60 | this.expressRouter.use('/auth/token', this.security.issueToken()); 61 | this.express.use('/api/v1', this.expressRouter); 62 | this.express.use((req, res) => { 63 | res.status(404).send({ url: `${req.originalUrl} not found` }); 64 | }); 65 | this.express.listen(this.port, this.host); 66 | console.log(`RESTful API server started on: ${this.port}`); 67 | } 68 | } 69 | 70 | module.exports = App; 71 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "api": { 4 | "host": "", 5 | "port": 3000, 6 | "security": { 7 | "jwtSecret": "secret" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /controllers/booksListController.js: -------------------------------------------------------------------------------- 1 | const ControllerBase = require('./controllerBase'); 2 | const BookModel = require('../models/bookModel'); 3 | 4 | class BooksListController extends ControllerBase { 5 | async getBook() { 6 | try { 7 | const { id } = this.params; 8 | const book = this.repository.book.getById(id); 9 | const bookModel = new BookModel(book); 10 | const resource = await bookModel.getResource(this.uriGenerator); 11 | this.ok(resource); 12 | } catch (err) { 13 | this.error(err); 14 | } 15 | } 16 | 17 | async rateBook() { 18 | try { 19 | const { id } = this.params; 20 | const { rating } = this.body; 21 | this.repository.book.rateBook(id, rating); 22 | const book = this.repository.book.getById(id); 23 | const bookModel = new BookModel(book); 24 | const resource = await bookModel.getResource(this.uriGenerator); 25 | this.ok(resource); 26 | } catch (err) { 27 | this.error(err); 28 | } 29 | } 30 | 31 | async removeBook() { 32 | try { 33 | const { id } = this.params; 34 | this.repository.book.removeById(id); 35 | this.noContent(); 36 | } catch (err) { 37 | this.error(err); 38 | } 39 | } 40 | } 41 | 42 | module.exports = BooksListController; 43 | -------------------------------------------------------------------------------- /controllers/booksListController.test.js: -------------------------------------------------------------------------------- 1 | const BookListController = require('./booksListController'); 2 | const Router = require('../routing/router'); 3 | const BooksListRoutesBuilder = require('../routing/routesBuilders/booksListRoutesBuilder'); 4 | const Repository = require('../repositories/repository'); 5 | const URIGenerator = require('../routing/uriGenerator'); 6 | const Security = require('../security/security'); 7 | 8 | let repository; 9 | let security; 10 | beforeEach(() => { 11 | const router = new Router([ 12 | new BooksListRoutesBuilder(), 13 | ]); 14 | router.registerRoutes(jest.fn(), jest.fn()); 15 | 16 | repository = new Repository({ 17 | books: [ 18 | { 19 | _id: 0, 20 | name: 'Letters from Father Christmas', 21 | author: 'John Ronald Ruel Tolkien', 22 | language: 'English', 23 | rating: 0, 24 | }], 25 | }); 26 | repository.registerRepositories(); 27 | security = new Security(repository, 'secret'); 28 | }); 29 | 30 | describe('testing BooksListController controller', () => { 31 | test('GetBook action success', async (done) => { 32 | const book = repository.book.getById(0); 33 | const sendFunc = (status, resource) => { 34 | expect(status).toBe(200); 35 | expect(resource.id).toBe(book._id); 36 | done(); 37 | }; 38 | const controller = new BookListController({ 39 | params: { id: book._id }, 40 | send: sendFunc, 41 | repository, 42 | uriGenerator: new URIGenerator(security, 'Guest'), 43 | }); 44 | await controller.getBook(); 45 | }); 46 | 47 | test('RateBook action success', async (done) => { 48 | const book = repository.book.getById(0); 49 | const sendFunc = (status, resource) => { 50 | expect(status).toBe(200); 51 | expect(resource.id).toBe(book._id); 52 | expect(resource.rating).toBe(5); 53 | done(); 54 | }; 55 | const controller = new BookListController({ 56 | params: { id: book._id }, 57 | body: { rating: 5 }, 58 | send: sendFunc, 59 | repository, 60 | uriGenerator: new URIGenerator(security, 'BasicUser'), 61 | }); 62 | await controller.rateBook(); 63 | }); 64 | 65 | test('RemoveBook action success', async (done) => { 66 | const book = repository.book.getById(0); 67 | const allBooksLength = repository.book.getAllBooks().length; 68 | const expectedResult = allBooksLength - 1; 69 | const sendFunc = (status) => { 70 | expect(status).toBe(204); 71 | expect(repository.book.getAllBooks().length).toBe(expectedResult); 72 | done(); 73 | }; 74 | const controller = new BookListController({ 75 | params: { id: book._id }, 76 | send: sendFunc, 77 | repository, 78 | uriGenerator: new URIGenerator(security, 'AdminUser'), 79 | }); 80 | await controller.removeBook(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /controllers/controllerBase.js: -------------------------------------------------------------------------------- 1 | class ControllerBase { 2 | constructor({ params, query, body, send, uriGenerator, repository }) { 3 | this.uriGenerator = uriGenerator; 4 | this.params = params; 5 | this.query = query; 6 | this.body = body; 7 | this.send = send; 8 | this.repository = repository; 9 | } 10 | 11 | error(err) { 12 | const status = err.statusCode || err.status; 13 | const statusCode = status || 500; 14 | this.send(statusCode, err); 15 | } 16 | 17 | created(location, data) { 18 | if (location) { 19 | this.res.location(location); 20 | } 21 | 22 | this.send(201, data); 23 | } 24 | 25 | ok(data) { 26 | this.send(200, data); 27 | } 28 | 29 | noContent() { 30 | this.send(204); 31 | } 32 | } 33 | 34 | module.exports = ControllerBase; 35 | -------------------------------------------------------------------------------- /controllers/indexController.js: -------------------------------------------------------------------------------- 1 | const halson = require('halson'); 2 | const ControllerBase = require('./controllerBase'); 3 | 4 | class IndexController extends ControllerBase { 5 | async index() { 6 | const getBookURI = this.uriGenerator.getURI( 7 | 'BooksListController_getBook', 8 | ); 9 | 10 | const rateBookURI = this.uriGenerator.getURI( 11 | 'BooksListController_rateBook', 12 | ); 13 | 14 | const removeBookURI = this.uriGenerator.getURI( 15 | 'BooksListController_removeBook', 16 | ); 17 | 18 | const resource = halson({ api: 'api v1' }); 19 | try { 20 | const links = await Promise.all([ 21 | getBookURI, 22 | rateBookURI, 23 | removeBookURI, 24 | ]); 25 | 26 | links.forEach((link) => { 27 | if (link) { 28 | resource.addLink(link.id, link); 29 | } 30 | }); 31 | 32 | super.ok(resource); 33 | } catch (err) { 34 | super.error(err); 35 | } 36 | } 37 | } 38 | 39 | module.exports = IndexController; 40 | -------------------------------------------------------------------------------- /mock/books.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | _id: 0, 4 | name: 'Letters from Father Christmas', 5 | author: 'John Ronald Ruel Tolkien', 6 | language: 'English', 7 | rating: 0, 8 | }, 9 | { 10 | _id: 1, 11 | name: 'Mrs. Dalloway', 12 | author: 'Virginia Woolf', 13 | language: 'English', 14 | rating: 0, 15 | }, 16 | { 17 | _id: 2, 18 | name: 'The Alice Network', 19 | author: 'Kate Quinn', 20 | language: 'English', 21 | rating: 0, 22 | }]; 23 | -------------------------------------------------------------------------------- /mock/db.js: -------------------------------------------------------------------------------- 1 | const books = require('./books'); 2 | const users = require('./users'); 3 | 4 | module.exports = { 5 | books, 6 | users, 7 | }; 8 | -------------------------------------------------------------------------------- /mock/users.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | _id: 0, 4 | name: 'John Doe', 5 | email: 'john.doe@gmail.com', 6 | role: 'BasicUser', 7 | salt: '0fca3e9edc11883ed2d6bded1169fcf848e5ab1abfc215f41782a0490707d8f2', 8 | hashedPassword: '6099051f343db260af74bfa4f6b3e53cec629621', // 123456 9 | }, 10 | { 11 | _id: 1, 12 | name: 'Mark Moe', 13 | email: 'mark.moe@gmail.com', 14 | role: 'AdminUser', 15 | salt: 'd7a172dd66d45036ba2d3ae1d41e53dad831c48dac61c01710536cbd34373d67', 16 | hashedPassword: 'd5279d36325e0027d82731b67688b016c04d832d', // qwerty 17 | }]; 18 | -------------------------------------------------------------------------------- /models/bookModel.js: -------------------------------------------------------------------------------- 1 | const Model = require('./model'); 2 | 3 | class BookModel extends Model { 4 | constructor(data) { 5 | super(); 6 | this.id = data._id; 7 | this.name = data.name; 8 | this.author = data.author; 9 | this.language = data.language; 10 | this.rating = data.rating; 11 | } 12 | 13 | async getResource(uriGenerator) { 14 | const resource = super.getResource({ 15 | id: this.id, 16 | name: this.name, 17 | author: this.author, 18 | language: this.language, 19 | rating: this.rating, 20 | }); 21 | 22 | await this.addLinks(resource, uriGenerator); 23 | return resource; 24 | } 25 | 26 | async addLinks(resource, uriGenerator) { 27 | const removeURI = await uriGenerator.getURI( 28 | 'BooksListController_removeBook', 29 | { id: this.id }, 30 | ); 31 | if (removeURI) { 32 | resource.addLink(removeURI.id, removeURI); 33 | } 34 | 35 | const rateURI = await uriGenerator.getURI( 36 | 'BooksListController_rateBook', 37 | { id: this.id }, 38 | ); 39 | if (rateURI) { 40 | resource.addLink(rateURI.id, rateURI); 41 | } 42 | } 43 | } 44 | 45 | module.exports = BookModel; 46 | -------------------------------------------------------------------------------- /models/model.js: -------------------------------------------------------------------------------- 1 | const halson = require('halson'); 2 | 3 | class Model { 4 | getResource(data) { 5 | return halson(data); 6 | } 7 | } 8 | 9 | module.exports = Model; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-api-mvc-jwt-rbac", 3 | "version": "1.0.0", 4 | "main": "api.js", 5 | "scripts": { 6 | "test": "jest", 7 | "start": "node server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "dependencies": { 13 | "body-parser": "^1.18.3", 14 | "config": "^2.0.1", 15 | "eslint-plugin-jest": "^21.21.0", 16 | "express": "^4.16.3", 17 | "halson": "^3.0.0", 18 | "jest": "^23.5.0", 19 | "jsonwebtoken": "^8.3.0", 20 | "passport": "^0.4.0", 21 | "passport-anonymous": "^1.0.1", 22 | "passport-http": "^0.3.0", 23 | "passport-jwt": "^4.0.0", 24 | "query-string": "^6.1.0", 25 | "rbac": "^4.0.2" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^4.19.1", 29 | "eslint-config-airbnb-base": "^13.0.0", 30 | "eslint-plugin-import": "^2.12.0" 31 | }, 32 | "jest": { 33 | "testEnvironment": "node", 34 | "transform": {}, 35 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(js?)$", 36 | "moduleFileExtensions": [ 37 | "js" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /repositories/bookRepository.js: -------------------------------------------------------------------------------- 1 | const RepositoryBase = require('./repositoryBase'); 2 | 3 | class BookRepository extends RepositoryBase { 4 | constructor(db) { 5 | super(); 6 | this.booksCollection = db.books; 7 | } 8 | 9 | getAllBooks() { 10 | return this.booksCollection; 11 | } 12 | 13 | getById(id) { 14 | return this.booksCollection.find(book => book._id == id); 15 | } 16 | 17 | removeById(id) { 18 | this.booksCollection = this.booksCollection.filter(book => book._id != id); 19 | } 20 | 21 | rateBook(id, rating) { 22 | const book = this.getById(id); 23 | book.rating = rating; 24 | } 25 | } 26 | 27 | module.exports = BookRepository; 28 | -------------------------------------------------------------------------------- /repositories/repository.js: -------------------------------------------------------------------------------- 1 | const BookRepository = require('./bookRepository'); 2 | const UserRepository = require('./userRepository'); 3 | 4 | class Repository { 5 | constructor(db) { 6 | this._db = db; 7 | } 8 | 9 | registerRepositories() { 10 | this.book = new BookRepository(this._db); 11 | this.user = new UserRepository(this._db); 12 | } 13 | } 14 | 15 | module.exports = Repository; 16 | -------------------------------------------------------------------------------- /repositories/repositoryBase.js: -------------------------------------------------------------------------------- 1 | class RepositoryBase { 2 | getById() { 3 | throw new Error('Not Implemented Exception'); 4 | } 5 | } 6 | 7 | module.exports = RepositoryBase; 8 | -------------------------------------------------------------------------------- /repositories/userRepository.js: -------------------------------------------------------------------------------- 1 | const RepositoryBase = require('./repositoryBase'); 2 | 3 | class UserRepository extends RepositoryBase { 4 | constructor(db) { 5 | super(); 6 | this.usersCollection = db.users; 7 | } 8 | 9 | getById(id) { 10 | return this.usersCollection.find(user => user._id == id); 11 | } 12 | 13 | getByEmail(email) { 14 | return this.usersCollection.find(user => user.email == email); 15 | } 16 | } 17 | 18 | module.exports = UserRepository; 19 | -------------------------------------------------------------------------------- /routing/router.js: -------------------------------------------------------------------------------- 1 | const RoutesCollection = require('./routesCollection'); 2 | 3 | class Router { 4 | constructor(routeBuilders) { 5 | this.routeBuilders = routeBuilders; 6 | } 7 | 8 | registerRoutes(registerRouteCallback, createRouteBoundAction) { 9 | this.routeBuilders.forEach((builder) => { 10 | const routes = builder.getRoutes(); 11 | routes.forEach((routeData) => { 12 | RoutesCollection.addRouteData(routeData.controllerClass, routeData.action, 13 | { uri: routeData.uri, httpMethod: routeData.httpMethod }); 14 | const boundAction = createRouteBoundAction(routeData.controllerClass, routeData.action, 15 | routeData.isSecure); 16 | registerRouteCallback(routeData.uri, routeData.httpMethod, boundAction); 17 | }); 18 | }); 19 | } 20 | } 21 | 22 | module.exports = Router; 23 | -------------------------------------------------------------------------------- /routing/routesBuilders/booksListRoutesBuilder.js: -------------------------------------------------------------------------------- 1 | const BooksListController = require('../../controllers/booksListController'); 2 | const RoutesBase = require('./routesBuilderBase'); 3 | 4 | class BooksListRoutesBuilder extends RoutesBase { 5 | constructor() { 6 | super(BooksListController); 7 | } 8 | 9 | getRoutes() { 10 | this.buildRoute('/books/:id', 'get', 'getBook'); 11 | this.buildRoute('/books/:id', 'delete', 'removeBook'); 12 | this.buildRoute('/books/:id/rate', 'post', 'rateBook'); 13 | 14 | return this.routes; 15 | } 16 | } 17 | 18 | module.exports = BooksListRoutesBuilder; 19 | -------------------------------------------------------------------------------- /routing/routesBuilders/indexRoutesBuilder.js: -------------------------------------------------------------------------------- 1 | const IndexController = require('../../controllers/indexController'); 2 | const RoutesBase = require('./routesBuilderBase'); 3 | 4 | class IndexRoutesBuilder extends RoutesBase { 5 | constructor() { 6 | super(IndexController); 7 | } 8 | 9 | getRoutes() { 10 | this.buildRoute('/', 'get', 'index'); 11 | return this.routes; 12 | } 13 | } 14 | 15 | module.exports = IndexRoutesBuilder; 16 | -------------------------------------------------------------------------------- /routing/routesBuilders/routesBuilderBase.js: -------------------------------------------------------------------------------- 1 | class RoutesBuilderBase { 2 | constructor(controllerClass) { 3 | this.routes = []; 4 | this.ControllerClass = controllerClass; 5 | } 6 | 7 | buildRoute(uri, httpMethod, action, isSecure = true) { 8 | this.routes.push({ 9 | controllerClass: this.ControllerClass, 10 | action, 11 | uri, 12 | httpMethod, 13 | isSecure, 14 | }); 15 | } 16 | } 17 | 18 | module.exports = RoutesBuilderBase; 19 | -------------------------------------------------------------------------------- /routing/routesCollection.js: -------------------------------------------------------------------------------- 1 | class RoutesCollection { 2 | static addRouteData(controller, action, routeData) { 3 | routeData.controller = controller.name; 4 | routeData.action = action; 5 | 6 | if (!RoutesCollection[controller.name]) { 7 | RoutesCollection[controller.name] = {}; 8 | } 9 | 10 | RoutesCollection[controller.name] = Object.assign({}, RoutesCollection[controller.name], { 11 | [action]: routeData, 12 | }); 13 | } 14 | } 15 | 16 | module.exports = RoutesCollection; 17 | -------------------------------------------------------------------------------- /routing/uriGenerator.js: -------------------------------------------------------------------------------- 1 | const queryString = require('query-string'); 2 | const RoutesCollection = require('./routesCollection'); 3 | 4 | class URIGenerator { 5 | constructor(security, role) { 6 | this.security = security; 7 | this.role = role; 8 | } 9 | 10 | getURI(controllerAction, params, id) { 11 | return new Promise((resolve) => { 12 | const routeMeta = controllerAction.split('_'); 13 | const routeData = RoutesCollection[routeMeta[0]][routeMeta[1]]; 14 | this.security.hasAccess(this.role, routeData.controller, routeData.action, (err, can) => { 15 | if (can) { 16 | const uri = params ? this._bindParams(routeData.uri, params) : routeData.uri; 17 | resolve({ id: id || routeData.action, method: routeData.method, uri }); 18 | } else { 19 | resolve(null); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | _bindParams(uri, params) { 26 | let match; 27 | let replacement; 28 | const replacedParams = []; 29 | 30 | while (match = /:([\w_]+)\??/ig.exec(uri)) { 31 | replacement = params[match[1]].toString() || ''; 32 | if (replacement === '') { 33 | uri = uri.replace(`/${match[0]}`); 34 | } else { 35 | uri = uri.replace(match[0], replacement); 36 | replacedParams.push(match[1]); 37 | } 38 | } 39 | 40 | const paramsForQueryString = {}; 41 | Object.keys(params).forEach((p) => { 42 | if (!replacedParams.includes(p)) { 43 | paramsForQueryString[p] = params[p]; 44 | } 45 | }); 46 | 47 | if (Object.keys(paramsForQueryString).length > 0) { 48 | uri = `${uri}?${queryString.stringify(paramsForQueryString)}`; 49 | } 50 | 51 | return uri; 52 | } 53 | } 54 | 55 | module.exports = URIGenerator; 56 | -------------------------------------------------------------------------------- /security/RBACAuthorization.js: -------------------------------------------------------------------------------- 1 | const RBAC = require('rbac').default; 2 | 3 | class RBACAuthorization { 4 | constructor() { 5 | this.roles = { 6 | guest: 'Guest', 7 | basic: 'BasicUser', 8 | admin: 'AdminUser', 9 | }; 10 | 11 | this.accessLevels = [ 12 | { 13 | role: this.roles.guest, 14 | level: 10, 15 | }, { 16 | role: this.roles.basic, 17 | level: 20, 18 | }, { 19 | role: this.roles.admin, 20 | level: 30, 21 | }]; 22 | 23 | this.rbac = new RBAC({ 24 | roles: this.accessLevels.map(al => al.role), 25 | permissions: { 26 | IndexController: [ 27 | 'index', 28 | ], 29 | BooksListController: [ 30 | 'getBook', 31 | 'removeBook', 32 | 'rateBook', 33 | ], 34 | }, 35 | grants: { 36 | Guest: [ 37 | 'index_IndexController', 38 | 'getBook_BooksListController', 39 | ], 40 | BasicUser: [ 41 | 'Guest', 42 | 'rateBook_BooksListController', 43 | ], 44 | AdminUser: [ 45 | 'BasicUser', 46 | 'removeBook_BooksListController', 47 | ], 48 | }, 49 | }, (err) => { 50 | if (err) { 51 | throw err; 52 | } 53 | }); 54 | } 55 | 56 | getGuestAccessLevel() { 57 | return this.accessLevels.find(lvl => lvl.role === this.roles.guest); 58 | } 59 | 60 | hasAccess(role, controller, action, cb) { 61 | this.rbac.can(role, action, controller, cb); 62 | } 63 | 64 | canAny(role, permissions, cb) { 65 | this.rbac.canAny(role, permissions, cb); 66 | } 67 | 68 | authorize(controller, action) { 69 | return (req, res, next) => this.rbac.can(req.user.role, action, controller, (err, can) => { 70 | if (err) return next(err); 71 | if (!can) { 72 | const errorResponse = { 73 | error_description: { 74 | type: 'access_denied', 75 | message: 'Access denied', 76 | }, 77 | }; 78 | const accessLevel = this._minNeededAccessLevel(controller, action); 79 | 80 | if (accessLevel != null) { 81 | errorResponse.accessLevel = accessLevel; 82 | } 83 | 84 | return res.status(403).send(errorResponse); 85 | } 86 | 87 | next(); 88 | }); 89 | } 90 | 91 | _minNeededAccessLevel(controller, action) { 92 | const roles = this._whoCan(controller, action); 93 | const rolesAccessLevels = this.accessLevels.filter(al => roles.includes(al.role)) 94 | .map(al => al.level); 95 | 96 | return rolesAccessLevels.length > 0 ? Math.min(...rolesAccessLevels) : null; 97 | } 98 | 99 | _whoCan(controller, action) { 100 | const rolesIncludePermission = []; 101 | this.rbac.getRoles((err, roles) => { 102 | if (err) throw err; 103 | 104 | roles.forEach((role) => { 105 | role.can(action, controller, (err2, can) => { 106 | if (err2) throw err2; 107 | 108 | if (can) { 109 | rolesIncludePermission.push(role.name); 110 | } 111 | }); 112 | }); 113 | }); 114 | 115 | return rolesIncludePermission; 116 | } 117 | } 118 | 119 | module.exports = RBACAuthorization; 120 | -------------------------------------------------------------------------------- /security/security.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const { BasicStrategy } = require('passport-http'); 3 | const { Strategy: AnonymousStrategy } = require('passport-anonymous'); 4 | const jwt = require('jsonwebtoken'); 5 | const { Strategy: JwtStrategy } = require('passport-jwt'); 6 | const { ExtractJwt } = require('passport-jwt'); 7 | const RBACAuthorization = require('./RBACAuthorization'); 8 | const UserService = require('../services/userService'); 9 | 10 | class Security { 11 | constructor(repository, jwtSecret) { 12 | this.repository = repository; 13 | this.rbacAuthorization = new RBACAuthorization(); 14 | this.jwtSecret = jwtSecret; 15 | this._setStrategies(); 16 | } 17 | 18 | issueToken() { 19 | return [ 20 | passport.authenticate('basic', { 21 | session: false, 22 | }), 23 | async (req, res) => { 24 | const { user } = req; 25 | const token = await jwt.sign({ 26 | _id: user._id, 27 | name: user.name, 28 | email: user.email, 29 | role: user.role, 30 | }, this.jwtSecret); 31 | 32 | res.json(token); 33 | }]; 34 | } 35 | 36 | authenticate() { 37 | return [ 38 | passport.authenticate(['jwt', 'anonymous'], { 39 | session: false, 40 | }), (req, res, next) => { 41 | if (!req.user) { 42 | req.user = { 43 | role: this.rbacAuthorization.getGuestAccessLevel().role, 44 | }; 45 | } 46 | 47 | next(); 48 | }]; 49 | } 50 | 51 | authorise(controller, action) { 52 | return this.rbacAuthorization.authorize(controller, action); 53 | } 54 | 55 | hasAccess(role, controller, action, cb) { 56 | return this.rbacAuthorization.hasAccess(role, controller, action, cb); 57 | } 58 | 59 | canAny(role, permissions, cb) { 60 | return this.rbacAuthorization.canAny(role, permissions, cb); 61 | } 62 | 63 | _setStrategies() { 64 | passport.use(new AnonymousStrategy()); 65 | 66 | passport.use(new BasicStrategy((email, password, done) => { 67 | let user; 68 | try { 69 | user = this.repository.user.getByEmail(email); 70 | 71 | if (!user) return done(null, false); 72 | 73 | if (!UserService.checkPassword(user.hashedPassword, user.salt, password)) { 74 | return done(null, false); 75 | } 76 | 77 | done(null, user); 78 | } catch (err) { 79 | done(err); 80 | } 81 | })); 82 | 83 | passport.use(new JwtStrategy({ 84 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 85 | secretOrKey: this.jwtSecret, 86 | }, (user, done) => done(null, user))); 87 | } 88 | } 89 | 90 | module.exports = Security; 91 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const config = require('config'); 2 | const App = require('./app'); 3 | const Router = require('./routing/router'); 4 | const Repository = require('./repositories/repository'); 5 | const IndexRoutesBuilder = require('./routing/routesBuilders/indexRoutesBuilder'); 6 | const BooksListRoutesBuilder = require('./routing/routesBuilders/booksListRoutesBuilder'); 7 | const db = require('./mock/db'); 8 | const Security = require('./security/security'); 9 | 10 | class Server { 11 | constructor() { 12 | this.repository = new Repository(db); 13 | this.security = new Security(this.repository, config.get('api.security.jwtSecret')); 14 | this.router = new Router([ 15 | new IndexRoutesBuilder(), 16 | new BooksListRoutesBuilder(), 17 | ], this.router); 18 | 19 | this.app = new App(this.router, this.repository, this.security); 20 | } 21 | 22 | start() { 23 | this.app.run(); 24 | } 25 | } 26 | 27 | const server = new Server(); 28 | server.start(); 29 | -------------------------------------------------------------------------------- /services/userService.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | class UserService { 4 | static encryptPassword(password, salt) { 5 | return crypto.createHmac('sha1', salt).update(password).digest('hex'); 6 | } 7 | 8 | static checkPassword(hashedPassword, salt, password) { 9 | return hashedPassword && this.encryptPassword(password, salt) === hashedPassword; 10 | } 11 | } 12 | 13 | module.exports = UserService; 14 | --------------------------------------------------------------------------------