import { createError } from '@cirrusct/error'; import { Route, RouteGroup, RouteHandler, RouteArg } from './types'; import { IncomingMessage, ServerResponse, MiddlewareCallback, HttpMethod, Middleware } from '../types'; import { isArray, isBoolean } from 'lodash'; import { Trouter, Polka } from '../types'; import { WebServerContext } from '../middleware/context'; const DefaultRouteIsPublic = false; type RouteArgType = 'route' | 'group'; interface RouteGroupInstance extends RouteGroup { parent: RouteGroupInstance; } interface RouteInstance extends Route { group?: RouteGroupInstance; } export class Router { private _authenticateMiddlewareMap: { [strategy: string]: Middleware }; private _authenticateMiddleware: Middleware; constructor(private polka: Polka, private context: WebServerContext) {} private get trouter(): Trouter { return this.polka as any; } private routeInterceptHandler = (route: RouteInstance, req: IncomingMessage, res: ServerResponse) => { if (!route.isPublic) { if (!this._authenticateMiddleware) { res.statusCode = 500; res.end('Internal Error. No auth handler for private route'); } this._authenticateMiddleware(req, res, () => { route.handler(this.context); }); } else { route.handler(this.context); } }; public routeIsPublicDefault = DefaultRouteIsPublic; private getArgType = (routeArg: RouteArg): RouteArgType => { const routeArgs = isArray(routeArg) ? routeArg : [routeArg]; if (routeArgs.length > 0) { if (routeArgs[0].hasOwnProperty('path')) { return 'route'; } else if (routeArgs[0].hasOwnProperty('routes')) { return 'group'; } else { throw createError( 'Invalid Argument in Router.add(): Expected Route, Route[], RouteGroup, or RouteGroup[]' ); } } }; public add = (routeArg: RouteArg) => { this._add(routeArg); }; private _add = (routeArg: RouteArg, parent?: RouteGroupInstance) => { const routeArgs = isArray(routeArg) ? routeArg : [routeArg]; const type = this.getArgType(routeArg); if (type === 'group') { this._addRouteGroups(routeArgs as RouteGroup[], parent); } else { this._addRoutes(routeArgs as Route[], parent); } }; private computeIsPublic = (route: RouteInstance) => { // if route isPublic is explicitly set, use its value if (isBoolean(route.isPublic)) { return route.isPublic; } // otherwise, walk parents until we find isPublic that's explicitly set let group = route.group; while (group) { if (isBoolean(group.isPublic)) { return group.isPublic; } group = group.parent; } // otherwise, use default isPublic setting for routes return this.routeIsPublicDefault; }; private _addRoutes = (routeOrRoutes: Route | Route[], group?: RouteGroupInstance) => { const routes = isArray(routeOrRoutes) ? routeOrRoutes : [routeOrRoutes]; routes.map(route => { const routeInstance = route as RouteInstance; routeInstance.group = group; routeInstance.isPublic = this.computeIsPublic(routeInstance); const pattern = this.appendPath(group && group.pathPrefix, route.path, ''); const interceptHandler = (req: IncomingMessage, res: ServerResponse) => this.routeInterceptHandler(routeInstance, req, res); this.context.logger.debug({ method: route.method, pattern: pattern }, 'Adding Route'); this.trouter.add(route.method, pattern, interceptHandler); }); }; private appendPath = (base: string = '', path: string = '', tailChar = '/') => { base = base.replace(/\/$/, ''); path = path.replace(/^\//, '').replace(/\/$/, ''); return base.length + path.length === 0 ? tailChar : `${base}/${path}${tailChar}`; }; private _addRouteGroups = (routeGroupOrGroups: RouteGroup | RouteGroup[], parent?: RouteGroupInstance) => { const groups = isArray(routeGroupOrGroups) ? routeGroupOrGroups : [routeGroupOrGroups]; groups.map(group => { const groupInstance = group as RouteGroupInstance; groupInstance.parent = parent; const newPrefix = this.appendPath(parent && parent.pathPrefix, group.pathPrefix); if (group.routes) { this._add(group.routes, groupInstance); } }); }; public setAuthMiddleware = (strategy: string, mw: Middleware) => { this._authenticateMiddlewareMap = this._authenticateMiddlewareMap || {}; this._authenticateMiddlewareMap[strategy] = mw; // for now just using whatever was set last, until there's a strategy (doh) to deal with multiple strategies.. this._authenticateMiddleware = mw; }; }