Files
dougal-software/lib/www/server/api/index.js
D. Berge 707889be42 Refactor layer API endpoint and database functions.
- A single get() function is used both to list all available
  layers, if no layer name is given, or a single layer.
- The database no longer holds the actual layer contents,
  only the path to the layer file(s), so the list() function
  is now redundant as we return the full payload in every case.
- The /gis/layer and /gis/layer/:name endpoints now have the same
  payload structure.
2023-09-12 19:29:02 +02:00

377 lines
9.5 KiB
JavaScript

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.4.0"; // 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 ]
},
'/project/:project/gis/layer': {
get: [ mw.etag.noSave, mw.gis.project.layer.get ]
},
'/project/:project/gis/layer/:name': {
get: [ mw.etag.noSave, mw.gis.project.layer.get ]
},
/*
* 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.etag.noSave, 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;
}