Merge remote-tracking branch 'ui/master'
This commit is contained in:
commit
4530967b6b
70 files changed
+10951
-8
No files matched your search
+485
@@ -0,0 +1,485 @@
|
||||
"use strict";
|
||||
|
||||
const stream = require("stream");
|
||||
const Router = require("koa-router");
|
||||
const db = require("./db");
|
||||
const apiFunctions = require("./api-functions");
|
||||
const expression = require("../common/expression");
|
||||
const logger = require("./logger");
|
||||
|
||||
const router = new Router();
|
||||
|
||||
function logUnauthorizedWarning(log) {
|
||||
log.message += " not authorized";
|
||||
logger.accessWarn(log);
|
||||
}
|
||||
|
||||
const RESOURCE_DELETE = 1 << 0;
|
||||
const RESOURCE_PUT = 1 << 1;
|
||||
|
||||
const RESOURCE_IDS = {
|
||||
devices: "DeviceID.ID",
|
||||
presets: "_id",
|
||||
provisions: "_id",
|
||||
files: "_id",
|
||||
virtualParameters: "_id",
|
||||
faults: "_id",
|
||||
tasks: "_id"
|
||||
};
|
||||
|
||||
const resources = {
|
||||
devices: 0 | RESOURCE_DELETE,
|
||||
presets: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||
provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||
files: 0 | RESOURCE_DELETE,
|
||||
virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||
faults: 0 | RESOURCE_DELETE,
|
||||
tasks: 0
|
||||
};
|
||||
|
||||
router.get(`/devices/:id.csv`, async ctx => {
|
||||
const log = {
|
||||
message: "Query device (CSV)",
|
||||
context: ctx,
|
||||
id: ctx.params.id
|
||||
};
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS.devices], ctx.params.id];
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess("devices", 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 404);
|
||||
}
|
||||
|
||||
const res = await db.query("devices", filter);
|
||||
if (!res[0]) return void (ctx.status = 404);
|
||||
|
||||
ctx.type = "text/csv";
|
||||
ctx.attachment(
|
||||
`device-${ctx.params.id}-${new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, "")}.csv`
|
||||
);
|
||||
|
||||
ctx.body = new stream.PassThrough();
|
||||
ctx.body.write(
|
||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp\n"
|
||||
);
|
||||
|
||||
for (const k of Object.keys(res[0]).sort()) {
|
||||
const p = res[0][k];
|
||||
const row = [
|
||||
k,
|
||||
p.object,
|
||||
p.objectTimestamp,
|
||||
p.writable,
|
||||
p.writableTimestamp,
|
||||
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
|
||||
p.value != null ? p.value[1] : "",
|
||||
p.valueTimestamp
|
||||
];
|
||||
ctx.body.write(row.map(r => (r != null ? r : "")).join(",") + "\n");
|
||||
}
|
||||
ctx.body.end();
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
for (const [resource, flags] of Object.entries(resources)) {
|
||||
router.head(`/${resource}`, async (ctx, next) => {
|
||||
let filter = true;
|
||||
if (ctx.request.query.filter)
|
||||
filter = expression.parse(ctx.request.query.filter);
|
||||
|
||||
const log = {
|
||||
message: `Count ${resource}`,
|
||||
context: ctx,
|
||||
filter: ctx.request.query.filter
|
||||
};
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 1)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = expression.and(filter, [
|
||||
"NOT",
|
||||
["<", ["PARAM", "expiry"], Date.now() + 60000]
|
||||
]);
|
||||
}
|
||||
|
||||
const count = await db.count(resource, filter);
|
||||
|
||||
ctx.set("X-Total-Count", count);
|
||||
ctx.body = "";
|
||||
|
||||
log.count = count;
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
router.get(`/${resource}`, async (ctx, next) => {
|
||||
const options = {};
|
||||
let filter = true;
|
||||
if (ctx.request.query.filter)
|
||||
filter = expression.parse(ctx.request.query.filter);
|
||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||
if (ctx.request.query.sort)
|
||||
options.sort = JSON.parse(ctx.request.query.sort);
|
||||
if (ctx.request.query.projection) {
|
||||
options.projection = ctx.request.query.projection
|
||||
.split(",")
|
||||
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
|
||||
}
|
||||
|
||||
const log = {
|
||||
message: `Query ${resource}`,
|
||||
context: ctx,
|
||||
filter: ctx.request.query.filter,
|
||||
limit: options.limit,
|
||||
skip: options.skip,
|
||||
sort: options.sort,
|
||||
projection: options.projection
|
||||
};
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = expression.and(filter, [
|
||||
"NOT",
|
||||
["<", ["PARAM", "expiry"], Date.now() + 60000]
|
||||
]);
|
||||
}
|
||||
|
||||
ctx.body = new stream.PassThrough();
|
||||
ctx.type = "application/json";
|
||||
|
||||
let c = 0;
|
||||
ctx.body.write("[\n");
|
||||
await db.query(resource, filter, options, obj => {
|
||||
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
|
||||
});
|
||||
ctx.body.end("]");
|
||||
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
// CSV download
|
||||
router.get(`/${resource}.csv`, async (ctx, next) => {
|
||||
const options = { projection: {} };
|
||||
let filter = true;
|
||||
if (ctx.request.query.filter)
|
||||
filter = expression.parse(ctx.request.query.filter);
|
||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||
if (ctx.request.query.sort)
|
||||
options.sort = JSON.parse(ctx.request.query.sort);
|
||||
|
||||
const log = {
|
||||
message: `Query ${resource} (CSV)`,
|
||||
context: ctx,
|
||||
filter: ctx.request.query.filter,
|
||||
limit: options.limit,
|
||||
skip: options.skip,
|
||||
sort: options.sort
|
||||
};
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
const columns = JSON.parse(ctx.request.query.columns);
|
||||
const now = Date.now();
|
||||
|
||||
for (const [k, v] of Object.entries(columns)) {
|
||||
const e = expression.evaluate(expression.parse(v), null, now);
|
||||
columns[k] = e;
|
||||
for (const p of expression.extractParams(e)) options.projection[p] = 1;
|
||||
}
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = expression.and(filter, [
|
||||
"NOT",
|
||||
["<", ["PARAM", "expiry"], Date.now() + 60000]
|
||||
]);
|
||||
}
|
||||
|
||||
ctx.body = new stream.PassThrough();
|
||||
ctx.type = "text/csv";
|
||||
ctx.attachment(
|
||||
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
|
||||
);
|
||||
|
||||
ctx.body.write(
|
||||
Object.keys(columns).map(k => `"${k.replace(/"/, '""')}"`) + "\n"
|
||||
);
|
||||
await db.query(resource, filter, options, obj => {
|
||||
const arr = Object.values(columns).map(exp => {
|
||||
const v = expression.evaluate(exp, obj, null, e => {
|
||||
if (Array.isArray(e)) {
|
||||
if (e[0] === "PARAM") {
|
||||
if (resource === "devices") {
|
||||
if (e[1] === "Tags") {
|
||||
const tags = [];
|
||||
for (const p in obj)
|
||||
if (p.startsWith("Tags.")) tags.push(p.slice(5));
|
||||
|
||||
return tags.join(", ");
|
||||
}
|
||||
}
|
||||
} else if (e[0] === "FUNC") {
|
||||
if (e[1] === "DATE_STRING") {
|
||||
if (e[2] && !Array.isArray(e[2]))
|
||||
return new Date(e[2]).toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
|
||||
if (Array.isArray(v) || v == null) return "";
|
||||
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
|
||||
return v;
|
||||
});
|
||||
ctx.body.write(arr.join(",") + "\n");
|
||||
});
|
||||
ctx.body.end();
|
||||
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
router.head(`/${resource}/:id`, async (ctx, next) => {
|
||||
const log = {
|
||||
message: `Count ${resource}`,
|
||||
context: ctx,
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
||||
};
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
const res = await db.query(resource, filter);
|
||||
|
||||
if (!res.length) return void next();
|
||||
|
||||
logger.accessInfo(log);
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.get(`/${resource}/:id`, async (ctx, next) => {
|
||||
const log = {
|
||||
message: `Query ${resource}`,
|
||||
context: ctx,
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
||||
};
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
const res = await db.query(resource, filter);
|
||||
|
||||
if (!res.length) return void next();
|
||||
|
||||
logger.accessInfo(log);
|
||||
ctx.body = res[0];
|
||||
});
|
||||
|
||||
// TODO add PUT, PATCH routes
|
||||
if (flags & RESOURCE_DELETE) {
|
||||
router.delete(`/${resource}/:id`, async (ctx, next) => {
|
||||
const log = {
|
||||
message: `Delete ${resource}`,
|
||||
context: ctx,
|
||||
id: ctx.params.id
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
const res = await db.query(resource, filter);
|
||||
if (!res.length) return void next();
|
||||
|
||||
const validate = authorizer.getValidator(resource, res[0]);
|
||||
if (!validate("delete")) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
await apiFunctions.deleteResource(resource, ctx.params.id);
|
||||
|
||||
logger.accessInfo(log);
|
||||
|
||||
ctx.body = "";
|
||||
});
|
||||
}
|
||||
|
||||
if (flags & RESOURCE_PUT) {
|
||||
router.put(`/${resource}/:id`, async (ctx, next) => {
|
||||
const id = ctx.params.id;
|
||||
|
||||
const log = {
|
||||
message: `Put ${resource}`,
|
||||
context: ctx,
|
||||
id: id
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
const obj = ctx.request.body;
|
||||
|
||||
const validate = authorizer.getValidator(resource, obj);
|
||||
if (!validate("put")) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const err = await apiFunctions.putResource(resource, id, obj);
|
||||
|
||||
if (err) {
|
||||
log.message += " failed";
|
||||
logger.accessWarn(log);
|
||||
ctx.body = err;
|
||||
return void (ctx.status = 400);
|
||||
}
|
||||
|
||||
logger.accessInfo(log);
|
||||
|
||||
ctx.body = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
router.put("/files/:id", async (ctx, next) => {
|
||||
const resource = "files";
|
||||
const id = ctx.params.id;
|
||||
|
||||
const log = {
|
||||
message: `Upload ${resource}`,
|
||||
context: ctx,
|
||||
id: id
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
fileType: ctx.request.headers["metadata.filetype"] || "",
|
||||
oui: ctx.request.headers["metadata.oui"] || "",
|
||||
productClass: ctx.request.headers["metadata.productclass"] || "",
|
||||
version: ctx.request.headers["metadata.version"] || ""
|
||||
};
|
||||
|
||||
const validate = authorizer.getValidator(resource, metadata);
|
||||
if (!validate("put")) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const err = await apiFunctions.putFile(id, metadata, ctx.req);
|
||||
|
||||
if (err) {
|
||||
log.message += " failed";
|
||||
logger.accessWarn(log);
|
||||
ctx.body = err;
|
||||
return void (ctx.status = 400);
|
||||
}
|
||||
|
||||
log.metadata = metadata;
|
||||
logger.accessInfo(log);
|
||||
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.post("/devices/:id/tasks", async (ctx, next) => {
|
||||
const log = {
|
||||
message: "Commit tasks",
|
||||
context: ctx,
|
||||
deviceId: ctx.params.id
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
|
||||
if (!authorizer.hasAccess("devices", 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
const devices = await db.query("devices", filter);
|
||||
if (!devices.length) return void next();
|
||||
|
||||
const validate = authorizer.getValidator("devices", devices[0]);
|
||||
for (const t of ctx.request.body) {
|
||||
if (!validate("task", t)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await apiFunctions.postTasks(ctx.params.id, ctx.request.body);
|
||||
|
||||
log.tasks = res.tasks.map(t => t._id).join(",");
|
||||
|
||||
logger.accessInfo(log);
|
||||
|
||||
ctx.set("Connection-Request", res.connectionRequest);
|
||||
ctx.body = res.tasks;
|
||||
});
|
||||
|
||||
router.post("/devices/:id/tags", async (ctx, next) => {
|
||||
const log = {
|
||||
message: "Update tags",
|
||||
context: ctx,
|
||||
deviceId: ctx.params.id,
|
||||
tags: ctx.request.body
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
|
||||
if (!authorizer.hasAccess("devices", 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
}
|
||||
const res = await db.query("devices", filter);
|
||||
if (!res.length) return void next();
|
||||
|
||||
const validate = authorizer.getValidator("devices", res[0]);
|
||||
if (!validate("tags", ctx.request.body)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
await apiFunctions.updateTags(ctx.params.id, ctx.request.body);
|
||||
|
||||
logger.accessInfo(log);
|
||||
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.get("/ping/:host", async ctx => {
|
||||
ctx.body = await apiFunctions.ping(ctx.params.host);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in new issue
Block a user