UNPKG

ltijs

Version:

Easily turn your web application into a LTI 1.3 Learning Tool.

157 lines • 7.01 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExpressHttpHandler = void 0; const node_https_1 = require("node:https"); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const helmet_1 = __importDefault(require("helmet")); const errors_1 = require("#shared/errors"); const errors_2 = require("#services/request-handler/errors"); const express_http_response_1 = require("#services/http-handler/express/express-http-response"); const http_handler_types_1 = require("#services/http-handler/http-handler.types"); class ExpressHttpHandler { LOG_COMPONENT = 'expressHttpHandler'; INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; logger; port; ssl; server; app; constructor(logger, options) { this.logger = logger; this.port = options.port; this.ssl = options.ssl; this.app = (0, express_1.default)(); this.setupServer(options); } registerRoute(path, methods, handler) { const adapter = this.buildAdapter(handler); for (const method of methods) this.app[this.toExpressMethod(method)](path, adapter); } async listen() { await new Promise((resolve, reject) => { this.server = this.ssl === undefined ? this.app.listen(this.port) : (0, node_https_1.createServer)(this.ssl, this.app).listen(this.port); this.server.once('listening', () => { resolve(); }); this.server.once('error', (error) => { reject(error); }); }); } async close() { if (this.server === undefined) return; await new Promise((resolve, reject) => { this.server?.close(error => { if (error !== undefined) { reject(error); return; } resolve(); }); }); } setupServer(options) { this.app.use((0, helmet_1.default)({ frameguard: false, contentSecurityPolicy: false })); if (options.cors !== false) { const corsMiddleware = (0, cors_1.default)({ origin: options.cors?.origin ?? true, credentials: options.cors?.credentials ?? true, }); this.app.use(corsMiddleware); this.app.options('*splat', corsMiddleware); } this.app.use(express_1.default.json()); this.app.use(express_1.default.urlencoded({ extended: false })); // Catches errors thrown by the middleware above (a malformed JSON body, say) before any route ever // runs. Route handlers already get this same mapping via buildAdapter()'s own try/catch, so this // only needs to sit after the middleware registered here, regardless of what registerRoute() adds // later: Express only propagates an error forward through the stack from where it was thrown, and a // route handler's own errors never reach `next()` at all. this.app.use((err, _req, res, _next) => { this.handleRouteError(err, new express_http_response_1.ExpressHttpResponse(res)); }); } buildAdapter(handler) { return async (req, res) => { try { const request = this.buildRequestParameters(req); const response = new express_http_response_1.ExpressHttpResponse(res); await handler(request, response); } catch (error) { this.handleRouteError(error, new express_http_response_1.ExpressHttpResponse(res)); } }; } handleRouteError(error, response) { if (error instanceof errors_1.LtijsError) { this.logger.error(this.LOG_COMPONENT, error.message); response.status(400).json({ error: error.name, message: error.message }); return; } if (error instanceof errors_2.HttpError) { this.logger.error(this.LOG_COMPONENT, error.message); // The platform rejected the outbound call, so its own status best describes what went wrong (e.g. a // 400 for a bad score submission). 502 is only a fallback for when the call failed before a status // was ever received (e.g. a network error). `external: true` tells the caller this status came from // the platform, not from this tool validating the caller's own request. response.status(error.status ?? 502).json({ error: error.name, message: error.message, external: true, platformResponse: error.response, }); return; } const message = error instanceof Error ? error.message : String(error); this.logger.error(this.LOG_COMPONENT, message); // A handful of libraries (body-parser among them, for a malformed request body) throw a plain Error // carrying its own `.status`/`.statusCode`, deliberately meant to be shown to the caller. Trusting // only the 4xx range keeps a genuine 5xx-ish or absent status falling through to the generic case // below, rather than exposing an arbitrary internal error's shape to the client. const clientErrorStatus = this.resolveClientErrorStatus(error); if (clientErrorStatus !== undefined && error instanceof Error) { response.status(clientErrorStatus).json({ error: error.name, message }); return; } response.status(500).json({ error: this.INTERNAL_SERVER_ERROR }); } resolveClientErrorStatus(error) { if (typeof error !== 'object' || error === null) return undefined; const { status, statusCode } = error; const candidate = typeof status === 'number' ? status : statusCode; if (typeof candidate !== 'number' || candidate < 400 || candidate >= 500) return undefined; return candidate; } buildRequestParameters(req) { return { method: req.method, path: req.path, query: req.query, body: req.body ?? {}, headers: req.headers, }; } toExpressMethod(method) { const EXPRESS_METHOD_BY_HTTP_METHOD = { [http_handler_types_1.HttpMethod.Get]: 'get', [http_handler_types_1.HttpMethod.Post]: 'post', [http_handler_types_1.HttpMethod.Put]: 'put', [http_handler_types_1.HttpMethod.Delete]: 'delete', [http_handler_types_1.HttpMethod.All]: 'all', }; return EXPRESS_METHOD_BY_HTTP_METHOD[method]; } } exports.ExpressHttpHandler = ExpressHttpHandler; exports.default = ExpressHttpHandler; //# sourceMappingURL=express-http-handler.service.js.map