UNPKG

bookmonkey-api

Version:

A simple backend for the BookMonkey example application.

219 lines (192 loc) 5.9 kB
const Router = require('express').Router const jwt = require('jsonwebtoken') const jsonServer = require('json-server') const stringify = require('querystring').stringify const JWT_SECRET_KEY = require ('../constants.js').JWT_SECRET_KEY const sharedMiddleware = require('./shared.js') /** * Logged Guard. * Check JWT. */ const loggedOnly = (req, res, next) => { if ( req.method !== 'GET' && req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH' && req.method !== 'DELETE' ) { // We let pass the other methods (HEAD, OPTIONS) // as they are not handled by json-server router, // but maybe by another user-defined middleware next() return } const { authorization } = req.headers if (!authorization) { res.status(401).jsonp('Missing authorization header') return } const [scheme, token] = authorization.split(' ') if (scheme !== 'Bearer') { res.status(401).jsonp('Incorrect authorization scheme') return } if (!token) { res.status(401).jsonp('Missing token') return } try { jwt.verify(token, JWT_SECRET_KEY) // Add claims to request req.claims = jwt.decode(token) next() } catch (err) { res.status(401).jsonp(err.message) } } /** * Owner Guard. * Checking userId reference in the request or the resource. * Inherits from logged guard. */ const privateOnly = (req, res, next) => { loggedOnly(req, res, () => { const { db } = req.app if (db == null) { throw Error('You must bind the router db to the app') } // TODO: handle query params instead of removing them const path = req.url.replace(`?${stringify(req.query)}`, '') const [, mod, resource, id] = path.split('/') // Creation and replacement // check userId on the request body if (req.method === 'POST' || req.method === 'PUT') { // TODO: use foreignKeySuffix instead of assuming the default "Id" const hasRightUserId = String(req.body.userId) === req.claims.sub // No userId reference when creating a new user (duh) const isUserResource = resource === 'users' if (hasRightUserId || isUserResource) { next() } else { res.status(403).jsonp( 'Private resource creation: request body must have a reference to the owner id' ) } return } // Query and update // check userId on the resource if (req.method === 'GET' || req.method === 'PATCH' || req.method === 'DELETE') { let hasRightUserId // TODO: use foreignKeySuffix instead of assuming the default "Id" if (id) { const entity = db.get(resource).getById(id).value() // get id if we are in the users collection const userId = resource === 'users' ? entity.id : entity.userId hasRightUserId = String(userId) === req.claims.sub } else { const entities = db.get(resource).value() // TODO: Array.every() for properly secured access. // Array.some() is too relax, but maybe useful for prototyping usecase. // But first we must handle the query params. hasRightUserId = entities.some( (entity) => String(entity.userId) === req.claims.sub ) } if (hasRightUserId) { next() } else { res.status(403).jsonp( 'Private resource access: entity must have a reference to the owner id' ) } return } // We let pass the other methods (HEAD, OPTIONS) // as they are not handled by json-server router, // but maybe by another user-defined middleware next() }) } // tslint:enable /** * Forbid all methods except GET. */ const readOnly = (req, res, next) => { if (req.method === 'GET') { next() } else { res.status(403).jsonp('Read only') } } /** * Allow applying a different middleware for GET request (read) and others (write) * (middleware returning a middleware) */ const branch = ({ read, write }) => (req, res, next) => { if (req.method === 'GET') { read(req, res, next) } else { write(req, res, next) } } /** * Remove guard mod from baseUrl, so lowdb can handle the resource. */ const flattenUrl = (req, res, next) => { // req.url is writable and used for redirection, // but app.use() already trim baseUrl from req.url, // so we use app.all() that leaves the baseUrl with req.url, // so we can rewrite it. // https://stackoverflow.com/questions/14125997/ req.url = req.url.replace(/\/[640]{3}/, '') next() } /** * Guards router */ const router = Router() .use(sharedMiddleware.bodyParsingHandler) .all('/666/*', flattenUrl) .all('/664/*', branch({ read: sharedMiddleware.goNext, write: loggedOnly }), flattenUrl) .all('/660/*', loggedOnly, flattenUrl) .all('/644/*', branch({ read: sharedMiddleware.goNext, write: privateOnly }), flattenUrl) .all('/640/*', branch({ read: loggedOnly, write: privateOnly }), flattenUrl) .all('/600/*', privateOnly, flattenUrl) .all('/444/*', readOnly, flattenUrl) .all('/440/*', loggedOnly, readOnly, flattenUrl) .all('/400/*', privateOnly, readOnly, flattenUrl) .use(sharedMiddleware.errorHandler) /** * Transform resource-guard mapping to proper rewrite rule supported by express-urlrewrite. * Return other rewrite rules as is, so we can use both types in routes.json. * @example * { 'users': 600 } => { '/users*': '/600/users$1' } */ const parseGuardsRules = (resourceGuardMap) => { return Object.entries(resourceGuardMap).reduce((routes, [resource, guard]) => { const isGuard = /^[640]{3}$/m.test(String(guard)) if (isGuard) { routes[`/${resource}*`] = `/${guard}/${resource}$1` } else { // Return as is if not a guard routes[resource] = guard } return routes }, {}) } /** * Conveniant method to use directly resource-guard mapping * with JSON Server rewriter (which itself uses express-urlrewrite). * Works with normal rewrite rules as well. */ const rewriter = (resourceGuardMap) => { const routes = parseGuardsRules(resourceGuardMap) return jsonServer.rewriter(routes) } module.exports = { router, parseGuardsRules, rewriter, }