UNPKG

nss-json-server

Version:
193 lines (192 loc) 6.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const jwt = require("jsonwebtoken"); const jsonServer = require("json-server"); const querystring_1 = require("querystring"); const constants_1 = require("./constants"); const shared_middlewares_1 = require("./shared-middlewares"); /** * Logged Guard. * Check JWT. */ const loggedOnly = (req, res, next) => { 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, constants_1.JWT_SECRET_KEY); 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; const { authorization } = req.headers; if (!authorization) { res.status(401).jsonp('Missing authorization header'); return; } 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(`?${querystring_1.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 isUserResource = resource === 'users'; if (!isUserResource) { try { req.body.userId = parseInt(req.claims.sub, 10); } catch (err) { res.status(401).jsonp(err.message); } } if ("userId" in req.body || 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) { // prettier-ignore const entity = db.get(resource).getById(id).value(); // get id if we are in the users collection | userId if not const comparer = (resource === "users") ? String(entity.id) : String(entity.userId); hasRightUserId = comparer === 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 }) => { return (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 */ exports.default = express_1.Router() .use(shared_middlewares_1.bodyParsingHandler) .all('/666/*', flattenUrl) .all('/664/*', branch({ read: shared_middlewares_1.goNext, write: loggedOnly }), flattenUrl) .all('/660/*', loggedOnly, flattenUrl) .all('/644/*', branch({ read: shared_middlewares_1.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(shared_middlewares_1.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' } */ function 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; }, {}); } exports.parseGuardsRules = parseGuardsRules; /** * Conveniant method to use directly resource-guard mapping * with JSON Server rewriter (which itself uses express-urlrewrite). * Works with normal rewrite rules as well. */ function rewriter(resourceGuardMap) { const routes = parseGuardsRules(resourceGuardMap); return jsonServer.rewriter(routes); } exports.rewriter = rewriter;