mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 06:27:07 +00:00
482 lines
13 KiB
JavaScript
482 lines
13 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.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((req, res, next) => {
|
|
DEBUG("⟴ %s %s %s (%j)", req.method, req.originalUrl, req.body?.length ? `${req.body.length} bytes` : "", req.user);
|
|
next();
|
|
});
|
|
|
|
// 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) ] };
|
|
};
|
|
|
|
|
|
//
|
|
// NOTICE 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 ]
|
|
},
|
|
'/version': {
|
|
get: [ mw.auth.operations, mw.version.get ],
|
|
'/history': {
|
|
get: [ /*mw.auth.user,*/ mw.version.history.get ],
|
|
}
|
|
},
|
|
'/': {
|
|
get: [ mw.openapi.get ]
|
|
}
|
|
});
|
|
|
|
//
|
|
// WARNING Every route from here onwards requires authentication!
|
|
//
|
|
app.use(mw.auth.authentify);
|
|
// Users must be authenticated to access anything below here
|
|
app.use(mw.auth.access.user);
|
|
|
|
// Don't process the request if the data hasn't changed
|
|
app.use(mw.etag.ifNoneMatch);
|
|
|
|
// Use compression across the board
|
|
app.use(mw.compress);
|
|
|
|
// We must be authenticated before we can access these
|
|
app.map({
|
|
'/project': {
|
|
get: [ mw.project.get ], // Get list of projects, filtered by `read` access
|
|
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.edit, mw.project.delete ], // Delete a project (only if empty)
|
|
},
|
|
'/project/:project/summary': {
|
|
get: [ mw.auth.access.read, mw.project.summary.get ],
|
|
},
|
|
'/project/:project/configuration': {
|
|
patch: [ mw.auth.access.edit, mw.project.configuration.patch ], // Modify project configuration
|
|
put: [ mw.auth.access.edit, mw.project.configuration.put ], // Overwrite configuration
|
|
},
|
|
'/project/:project/configuration/:path(*)?': {
|
|
get: [ mw.auth.access.read, mw.configuration.get ],
|
|
},
|
|
|
|
/*
|
|
* GIS endpoints
|
|
*/
|
|
|
|
'/project/:project/gis': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.bbox ]
|
|
},
|
|
'/project/:project/gis/preplot': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.preplot ]
|
|
},
|
|
'/project/:project/gis/preplot/:featuretype(line|point)': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.preplot ]
|
|
},
|
|
'/project/:project/gis/raw/:featuretype(line|point)': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.raw ]
|
|
},
|
|
'/project/:project/gis/final/:featuretype(line|point)': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.final ]
|
|
},
|
|
'/project/:project/gis/layer': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.layer.get ]
|
|
},
|
|
'/project/:project/gis/layer/:name': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.layer.get ]
|
|
},
|
|
|
|
/*
|
|
* Line endpoints
|
|
*/
|
|
|
|
'/project/:project/line/': {
|
|
get: [ mw.auth.access.read, mw.line.list ],
|
|
},
|
|
'/project/:project/line/:line(\\d+)': {
|
|
get: [ mw.auth.access.read, mw.line.get ],
|
|
patch: [ mw.auth.access.write, mw.line.patch ],
|
|
},
|
|
'/project/:project/line/:class(sail|source)': {
|
|
get: [ mw.auth.access.read, mw.line.get ],
|
|
},
|
|
|
|
/*
|
|
* Sequence endpoints
|
|
*/
|
|
|
|
'/project/:project/sequence/': {
|
|
get: [ mw.auth.access.read, mw.sequence.list ],
|
|
},
|
|
'/project/:project/sequence/:sequence': {
|
|
get: [ mw.auth.access.read, mw.sequence.get ],
|
|
patch: [ mw.auth.access.write, mw.sequence.patch ],
|
|
'/:point': {
|
|
get: [ mw.auth.access.read, mw.sequence.point.get ]
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Planner endpoints
|
|
*/
|
|
|
|
'/project/:project/plan/': {
|
|
get: [ mw.auth.access.read, 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.auth.access.read, mw.plan.get ],
|
|
patch: [ mw.auth.access.write, mw.plan.patch ],
|
|
delete: [ mw.auth.access.write, mw.plan.delete ]
|
|
},
|
|
|
|
/*
|
|
* Line name endpoints
|
|
*
|
|
*/
|
|
|
|
// Read access is sufficient for the next two endpoints
|
|
|
|
'/project/:project/linename': {
|
|
post: [ mw.auth.access.read, mw.linename.post ], // Get a linename
|
|
},
|
|
'/project/:project/linename/properties': {
|
|
get: [ mw.auth.access.read, mw.linename.properties.get ], // Get linename properties
|
|
},
|
|
|
|
/*
|
|
* Event log endpoints
|
|
*/
|
|
|
|
'/project/:project/event/': {
|
|
get: [ mw.auth.access.read, 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 ],
|
|
'changes/:since': {
|
|
get: [ mw.auth.access.read, mw.event.changes ]
|
|
},
|
|
// NOTE: old alias for /sequence/:sequence
|
|
'-/:sequence/': { // NOTE: We need to avoid conflict with the next endpoint ☹
|
|
get: [ mw.auth.access.read, mw.event.sequence.get ],
|
|
},
|
|
'sequence/:sequence/': {
|
|
get: [ mw.auth.access.read, mw.event.sequence.get ],
|
|
},
|
|
':id(\\d+)/': {
|
|
get: [ mw.auth.access.read, 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 ]
|
|
},
|
|
'import': {
|
|
put: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
|
|
post: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
|
|
'/:filename': {
|
|
put: [ mw.auth.access.read, mw.event.import.csv, mw.event.import.put ],
|
|
post: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
|
|
delete: [ mw.auth.access.write, mw.event.import.delete ]
|
|
},
|
|
},
|
|
},
|
|
|
|
/*
|
|
* QC endpoints
|
|
*/
|
|
|
|
'/project/:project/qc': {
|
|
'/results': {
|
|
// Get all QC results for :project
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.qc.results.get ],
|
|
|
|
// Delete all QC results for :project
|
|
delete: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.delete ],
|
|
|
|
'/accept': {
|
|
post: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.accept ]
|
|
},
|
|
|
|
'/unaccept': {
|
|
post: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.unaccept ]
|
|
},
|
|
|
|
'/sequence/:sequence': {
|
|
// Get QC results for :project, :sequence
|
|
get: [ mw.etag.noSave, mw.qc.results.get ],
|
|
|
|
// Delete QC results for :project, :sequence
|
|
delete: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.delete ]
|
|
}
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Other miscellaneous endpoints
|
|
*/
|
|
|
|
'/project/:project/label/': {
|
|
get: [ mw.auth.access.read, mw.label.list ],
|
|
// post: [ mw.label.post ],
|
|
},
|
|
'/project/:project/info/:path(*)': {
|
|
get: [ mw.auth.operations, mw.auth.access.read, mw.info.get ],
|
|
post: [ mw.auth.operations, mw.auth.access.write, mw.info.post ],
|
|
put: [ mw.auth.operations, mw.auth.access.write, mw.info.put ],
|
|
delete: [ mw.auth.operations, 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.auth.access.read, 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.files.get ]
|
|
},
|
|
'/files/?:path(*)': {
|
|
get: [ mw.etag.noSave, mw.files.get ]
|
|
},
|
|
'/navdata/': { // TODO These endpoints should probably need read access auth
|
|
get: [ mw.etag.noSave, mw.navdata.get ],
|
|
'gis/:featuretype(line|point)': {
|
|
get: [ mw.etag.noSave, mw.gis.navdata.get ]
|
|
}
|
|
},
|
|
'/vessel/track': {
|
|
get: [ /*mw.etag.noSave,*/ mw.vessel.track.get ], // JSON array
|
|
'/line': {
|
|
get: [ // GeoJSON Feature: type = LineString
|
|
//mw.etag.noSave,
|
|
(req, res, next) => { req.query.geojson = 'LineString'; next(); },
|
|
mw.vessel.track.get
|
|
]
|
|
},
|
|
'/point': {
|
|
get: [ // GeoJSON FeatureCollection: feature types = Point
|
|
//mw.etag.noSave,
|
|
(req, res, next) => { req.query.geojson = 'Point'; next(); },
|
|
mw.vessel.track.get
|
|
]
|
|
},
|
|
'/points': {
|
|
get: [ // JSON array of (Feature: type = Point)
|
|
mw.etag.noSave,
|
|
(req, res, next) => { req.query.geojson = true; next(); },
|
|
mw.vessel.track.get
|
|
],
|
|
},
|
|
},
|
|
'/info/': {
|
|
':path(*)': {
|
|
get: [ mw.auth.operations, mw.info.get ],
|
|
put: [ mw.auth.operations, mw.auth.access.write, mw.info.put ],
|
|
post: [ mw.auth.operations, mw.auth.access.write, mw.info.post ],
|
|
delete: [ mw.auth.operations, mw.auth.access.write, mw.info.delete ]
|
|
}
|
|
},
|
|
|
|
/*
|
|
* 4D comparisons
|
|
*/
|
|
|
|
// FIXME no authentication yet!
|
|
|
|
'/comparison/group': {
|
|
get: [ mw.etag.noSave, mw.comparisons.groups.list ],
|
|
'/:group': {
|
|
get: [ mw.etag.noSave, mw.comparisons.groups.get ],
|
|
},
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
* Other endpoints
|
|
*/
|
|
|
|
'/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 ]
|
|
}
|
|
}
|
|
},
|
|
'/diagnostics/': {
|
|
get: [ mw.auth.access.write, mw.etag.noSave, mw.admin.diagnostics.get ]
|
|
},
|
|
'/rss/': {
|
|
get: [ mw.rss.get ]
|
|
},
|
|
//
|
|
'/user': {
|
|
get: [ mw.auth.access.read, mw.etag.noSave, mw.user.list ],
|
|
post: [ mw.auth.access.edit, mw.etag.noSave, mw.user.post ],
|
|
},
|
|
'/user/:user_id': {
|
|
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.get("*", (req, res, next) => {
|
|
if (!res.headersSent) {
|
|
res.status(404).send({status: 404, message: "This endpoint does not exist"});
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|