mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:57:08 +00:00
- 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.
377 lines
9.5 KiB
JavaScript
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;
|
|
}
|