const http = require('http'); const express = require('express'); express.yaml ??= require('body-parser').yaml; // NOTE: Use own customised body-parser const cookieParser = require('cookie-parser') const maybeSendAlert = require("../lib/alerts"); const mw = require('./middleware'); const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename); const verbose = process.env.NODE_ENV != 'test'; const app = express(); app.locals.version = "0.3.1"; // API version app.map = function(a, route){ route = route || ''; for (var key in a) { switch (typeof a[key]) { // { '/path': { ... }} case 'object': if (!Array.isArray(a[key])) { app.map(a[key], route + key); break; } // else drop through // get: function(){ ... } case 'function': if (verbose) INFO('%s %s', key, route); app[key](route, a[key]); break; } } }; app.use(express.json({type: "application/json", strict: false, limit: '10mb'})); app.use(express.yaml({type: "application/yaml", limit: '10mb'})); app.use(express.urlencoded({ type: "application/x-www-form-urlencoded", extended: true })); app.use(express.text({type: "text/*", limit: '10mb'})); app.use((req, res, next) => { res.set("Access-Control-Allow-Origin", "*"); res.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE"); res.set("Access-Control-Allow-Headers", "Content-Type"); next(); }); app.use(cookieParser()); app.use(mw.auth.jwt); // app.use(mw.auth.access({path: {allow:["^/login", "^/user$"]}})); // Adds arbitrary information to the request object const meta = (key, value) => { return (req, res, next) => { if (!req.meta) { req.meta = {}; } if (key) { req.meta[key] = value; } next(); } }; // Short for adding meta to all methods const allMeta = (key, value) => { return { all: [ meta(key, value) ] }; }; // These routes do not require authentication app.map({ '*': { all: [ meta() ] }, // Create the req.meta object '/login': { post: [ mw.user.login ] }, '/logout': { get: [ mw.user.logout ], post: [ mw.user.logout ] }, '/': { get: [ mw.openapi.get ] } }); app.use(mw.auth.authentify); // Don't process the request if the data hasn't changed app.use(mw.etag.ifNoneMatch); // We must be authenticated before we can access these app.map({ '/project': { get: [ mw.project.get ], // Get list of projects post: [ mw.auth.access.admin, mw.project.post ], // Create a new project }, '/project/:project': { get: [ mw.project.summary.get ], // Get project data delete: [ mw.auth.access.admin, mw.project.delete ], // Delete a project (only if empty) }, '/project/:project/summary': { get: [ mw.project.summary.get ], }, '/project/:project/configuration': { get: [ mw.auth.access.write, mw.project.configuration.get ], // Get project configuration patch: [ mw.auth.access.write, mw.project.configuration.patch ], // Modify project configuration }, /* * GIS endpoints */ '/project/:project/gis': { get: [ mw.gis.project.bbox ] }, '/project/:project/gis/preplot': { get: [ mw.gis.project.preplot ] }, '/project/:project/gis/preplot/:featuretype(line|point)': { get: [ mw.gis.project.preplot ] }, '/project/:project/gis/raw/:featuretype(line|point)': { get: [ mw.gis.project.raw ] }, '/project/:project/gis/final/:featuretype(line|point)': { get: [ mw.gis.project.final ] }, /* * Line endpoints */ '/project/:project/line/': { get: [ mw.line.list ], }, '/project/:project/line/:line': { // get: [ mw.line.get ], patch: [ mw.auth.access.write, mw.line.patch ], }, /* * Sequence endpoints */ '/project/:project/sequence/': { get: [ mw.sequence.list ], }, '/project/:project/sequence/:sequence': { get: [ mw.sequence.get ], patch: [ mw.auth.access.write, mw.sequence.patch ], '/:point': { get: [ mw.sequence.point.get ] } }, /* * Planner endpoints */ '/project/:project/plan/': { get: [ mw.plan.list ], put: [ mw.auth.access.write, mw.plan.put ], post: [ mw.auth.access.write, mw.plan.post ] }, '/project/:project/plan/:sequence': { // get: [ mw.plan.get ], patch: [ mw.auth.access.write, mw.plan.patch ], delete: [ mw.auth.access.write, mw.plan.delete ] }, /* * Event log endpoints */ '/project/:project/event/': { get: [ mw.event.list ], post: [ mw.auth.access.write, mw.event.post ], put: [ mw.auth.access.write, mw.event.put ], delete: [ mw.auth.access.write, mw.event.delete ], // TODO Rename -/:sequence → sequence/:sequence '-/:sequence/': { // NOTE: We need to avoid conflict with the next endpoint ☹ get: [ mw.event.sequence.get ], }, ':id/': { get: [ mw.event.get ], put: [ mw.auth.access.write, mw.event.put ], patch: [ mw.auth.access.write, mw.event.patch ], delete: [mw.auth.access.write, mw.event.delete ] }, }, /* * QC endpoints */ '/project/:project/qc': { '/results': { // Get all QC results for :project get: [ mw.qc.results.get ], // Delete all QC results for :project delete: [ mw.auth.access.write, mw.qc.results.delete ], '/accept': { post: [ mw.auth.access.write, mw.qc.results.accept ] }, '/unaccept': { post: [ mw.auth.access.write, mw.qc.results.unaccept ] }, '/sequence/:sequence': { // Get QC results for :project, :sequence get: [ mw.qc.results.get ], // Delete QC results for :project, :sequence delete: [ mw.auth.access.write, mw.qc.results.delete ] } } }, /* * Other miscellaneous endpoints */ '/project/:project/label/': { get: [ mw.label.list ], // post: [ mw.label.post ], }, '/project/:project/configuration/:path(*)?': { get: [ mw.configuration.get ], // post: [ mw.auth.access.admin, mw.label.post ], }, '/project/:project/info/:path(*)': { get: [ mw.info.get ], post: [ mw.auth.access.write, mw.info.post ], put: [ mw.auth.access.write, mw.info.put ], delete: [ mw.auth.access.write, mw.info.delete ] }, '/project/:project/meta/': { put: [ mw.auth.access.write, mw.meta.put ], }, '/project/:project/meta/:path(*)': { // Path examples: // GET: // `/raw/sequences/qc/missing_shots`, // `/final/points/qc/sync_warn/results get: [ mw.meta.get ], // // PUT: // // `/raw/qc/missing_shots` ← { sequence: …, value: … } // put: [ mw.meta.put ] }, // // '/project/:id/permissions/:mode(read|write)?': { // get: [ mw.permissions.get ], // put: [ mw.permissions.put ], // // post: [ mw.permissions.post ], // // delete: [ mw.permissions.delete ] // }, '/project/:project/files/:path(*)': { get: [ mw.auth.access.write, mw.files.get ] }, '/files/?:path(*)': { get: [ mw.auth.access.write, mw.files.get ] }, '/navdata/': { get: [ mw.navdata.get ], 'gis/:featuretype(line|point)': { get: [ mw.gis.navdata.get ] } }, '/info/': { ':path(*)': { get: [ mw.info.get ], put: [ mw.auth.access.write, mw.info.put ], post: [ mw.auth.access.write, mw.info.post ], delete: [ mw.auth.access.write, mw.info.delete ] } }, '/queue/outgoing/': { 'asaqc': { get: [ mw.etag.noSave, mw.queue.asaqc.get ], post: [ mw.auth.access.write, mw.queue.asaqc.post ], '/project/:project': { get: [ mw.etag.noSave, mw.queue.asaqc.get ], '/sequence/:sequence': { get: [ mw.etag.noSave, mw.queue.asaqc.get ], } }, '/:id': { delete: [ mw.auth.access.write, mw.queue.asaqc.delete ] } } }, '/rss/': { get: [ mw.rss.get ] } // // '/user': { // get: [ mw.user.get ], // post: [ mw.user.put ] // }, // '/user/:user': { // get: [ mw.user.get ], // put: [ mw.user.put ], // // delete: [ mw.user.delete ] // }, // }); app.use(mw.etag.save); // Invalidate cache on database events mw.etag.watch(app); // Generic error handler. Stops stack dumps // being sent to clients. app.use(function (err, req, res, next) { const title = `HTTP backend error at ${req.method} ${req.originalUrl}`; const description = err.message; const message = err.message; const alert = {title, message, description, error: err}; console.log("Error:", err); ERROR("%O", err) res.set("Content-Type", "application/json"); if (err instanceof Error && err.name != "UnauthorizedError") { // console.error(err.stack); ERROR(err.stack); res.set("Content-Type", "text/plain"); res.status(500).send('General internal error'); maybeSendAlert(alert); } else if (typeof err === 'string') { res.status(500).send({message: err}); maybeSendAlert(alert); } else { res.status(err.status || 500).send({message: err.message || (err.inner && err.inner.message) || "Internal error"}); if (!res.status) { maybeSendAlert(alert); } } }); app.disable('x-powered-by'); app.enable('trust proxy'); INFO('trust proxy is ' + (app.get('trust proxy')? 'on' : 'off')); if (!module.parent) { const port = process.env.HTTP_PORT || 3000; const host = process.env.HTTP_HOST || "127.0.0.1"; var server = http.createServer(app).listen(port, host); INFO('API started on port ' + port); } else { app.start = function (port = 3000, host = "127.0.0.1", path) { var root = app; if (path) { root = express(); ['x-powered-by', 'trust proxy'].forEach(k => root.set(k, app.get(k))); root.use(path, app); } const server = http.createServer(root).listen(port, host); if (server) { console.log(`API started on port ${port}, prefix: ${path || "/"}`); INFO(`API started on port ${port}, prefix: ${path || "/"}`); } return server; } module.exports = app; }