Implement authentication and access control
This commit is contained in:
1 parent
e1bdcda191
commit
327cb78f1a
27 files changed
+600
-167
No files matched your search
+10
-4
@@ -4,11 +4,15 @@ import m from "mithril";
|
|||||||
|
|
||||||
import layout from "./layout";
|
import layout from "./layout";
|
||||||
import * as store from "./store";
|
import * as store from "./store";
|
||||||
|
import * as loginPage from "./login-page";
|
||||||
import * as overviewPage from "./overview-page";
|
import * as overviewPage from "./overview-page";
|
||||||
import * as devicesPage from "./devices-page";
|
import * as devicesPage from "./devices-page";
|
||||||
import * as devicePage from "./device-page";
|
import * as devicePage from "./device-page";
|
||||||
import * as errorPage from "./error-page";
|
import * as errorPage from "./error-page";
|
||||||
import * as faultsPage from "./faults-page";
|
import * as faultsPage from "./faults-page";
|
||||||
|
import Authorizer from "../common/authorizer";
|
||||||
|
|
||||||
|
window.authorizer = new Authorizer(window.permissionSets);
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
@@ -29,17 +33,18 @@ function pagify(pageName, page) {
|
|||||||
const component = {
|
const component = {
|
||||||
render: () => {
|
render: () => {
|
||||||
lastRenderTimestamp = Date.now();
|
lastRenderTimestamp = Date.now();
|
||||||
let c;
|
let p;
|
||||||
if (state && state.error) c = m(errorPage.component, state);
|
if (state && state.error) p = m(errorPage.component, state);
|
||||||
else c = m(layout, { page: pageName }, m(page.component, state));
|
else p = m(page.component, state);
|
||||||
fulfill();
|
fulfill();
|
||||||
return c;
|
return m(layout, { page: pageName }, p);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
component.onmatch = args => {
|
component.onmatch = args => {
|
||||||
pageVisitTimestamp = Date.now();
|
pageVisitTimestamp = Date.now();
|
||||||
if (!page.init) {
|
if (!page.init) {
|
||||||
|
state = null;
|
||||||
fulfill();
|
fulfill();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,6 +69,7 @@ function pagify(pageName, page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.route(document.body, "/overview", {
|
m.route(document.body, "/overview", {
|
||||||
|
"/login": pagify("login", loginPage),
|
||||||
"/overview": pagify("overview", overviewPage),
|
"/overview": pagify("overview", overviewPage),
|
||||||
"/devices": pagify("devices", devicesPage),
|
"/devices": pagify("devices", devicesPage),
|
||||||
"/devices/:id": pagify("devices", devicePage),
|
"/devices/:id": pagify("devices", devicePage),
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ const component = {
|
|||||||
title: "Delete device",
|
title: "Delete device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
if (!confirm("Deleting this device. Are you sure?")) return;
|
if (!confirm("Deleting this device. Are you sure?")) return;
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
|
||||||
store
|
store
|
||||||
.deleteResource("devices", device["DeviceID.ID"].value[0])
|
.deleteResource("devices", deviceId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Device deleted");
|
notifications.push("success", `${deviceId}: Device deleted`);
|
||||||
m.route.set("/devices");
|
m.route.set("/devices");
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|||||||
@@ -31,23 +31,33 @@ const component = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
taskQueue
|
taskQueue
|
||||||
.commit([task], (deviceId, connectionRequestStatus, tasks2) => {
|
.commit(
|
||||||
for (let t of tasks2)
|
[task],
|
||||||
if (t.status === "stale") taskQueue.deleteTask(t);
|
(deviceId, err, connectionRequestStatus, tasks2) => {
|
||||||
if (connectionRequestStatus !== "OK")
|
if (err)
|
||||||
notifications.push(
|
return notifications.push(
|
||||||
"error",
|
"error",
|
||||||
`${deviceId}: ${connectionRequestStatus}`
|
`${deviceId}: ${err.message}`
|
||||||
);
|
);
|
||||||
else if (tasks2[0].status === "stale")
|
|
||||||
notifications.push(
|
for (let t of tasks2)
|
||||||
"error",
|
if (t.status === "stale") taskQueue.deleteTask(t);
|
||||||
`${deviceId}: No contact from device`
|
|
||||||
);
|
if (connectionRequestStatus !== "OK")
|
||||||
else if (tasks2[0].status === "fault")
|
notifications.push(
|
||||||
notifications.push("error", `${deviceId}: Refresh faulted`);
|
"error",
|
||||||
else notifications.push("success", `${deviceId}: Summoned`);
|
`${deviceId}: ${connectionRequestStatus}`
|
||||||
})
|
);
|
||||||
|
else if (tasks2[0].status === "stale")
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: No contact from device`
|
||||||
|
);
|
||||||
|
else if (tasks2[0].status === "fault")
|
||||||
|
notifications.push("error", `${deviceId}: Refresh faulted`);
|
||||||
|
else notifications.push("success", `${deviceId}: Summoned`);
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
store.fulfill(0, Date.now());
|
store.fulfill(0, Date.now());
|
||||||
|
|||||||
@@ -29,14 +29,18 @@ const component = {
|
|||||||
{
|
{
|
||||||
onclick: e => {
|
onclick: e => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
store
|
store
|
||||||
.updateTags(device["DeviceID.ID"].value[0], { [tag]: false })
|
.updateTags(deviceId, { [tag]: false })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
notifications.push("success", `Tag '${tag}' unassigned`);
|
notifications.push("success", `${deviceId}: Tags updated`);
|
||||||
store.fulfill(0, Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch(err => notifications.push("error", err.message));
|
.catch(err => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("error", `${deviceId}: ${err.message}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"✕"
|
"✕"
|
||||||
@@ -50,6 +54,7 @@ const component = {
|
|||||||
{
|
{
|
||||||
onclick: e => {
|
onclick: e => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
const tag = prompt(`Enter tag to assign to device:`);
|
const tag = prompt(`Enter tag to assign to device:`);
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
@@ -57,13 +62,16 @@ const component = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store
|
store
|
||||||
.updateTags(device["DeviceID.ID"].value[0], { [tag]: true })
|
.updateTags(deviceId, { [tag]: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
notifications.push("success", `Tag '${tag}' assigned`);
|
notifications.push("success", `${deviceId}: Tags updated`);
|
||||||
store.fulfill(0, Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch(err => notifications.push("error", err.message));
|
.catch(err => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("error", `${deviceId}: ${err.message}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"🞢"
|
"🞢"
|
||||||
|
|||||||
@@ -229,11 +229,17 @@ i {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .logo {
|
& > .logo {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
display: inline;
|
display: inline;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .user-menu {
|
||||||
|
float: right;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
@@ -308,6 +314,10 @@ h3 {
|
|||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.error {
|
||||||
|
color: #a00;
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove button padding in FF */
|
/* Remove button padding in FF */
|
||||||
input::-moz-focus-inner {
|
input::-moz-focus-inner {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import * as store from "./store";
|
|||||||
import * as components from "./components";
|
import * as components from "./components";
|
||||||
|
|
||||||
function init(args) {
|
function init(args) {
|
||||||
return new Promise(resolve => {
|
if (!window.authorizer.hasAccess("devices", 2))
|
||||||
resolve({
|
return Promise.reject(
|
||||||
deviceId: args.id,
|
new Error("You are not authorized to view this page")
|
||||||
deviceFilter: new Filter(["=", "DeviceID.ID", args.id])
|
);
|
||||||
});
|
|
||||||
|
return Promise.resolve({
|
||||||
|
deviceId: args.id,
|
||||||
|
deviceFilter: new Filter(["=", "DeviceID.ID", args.id])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-35
@@ -12,7 +12,10 @@ import * as taskQueue from "./task-queue";
|
|||||||
import * as notifications from "./notifications";
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
function init(args) {
|
function init(args) {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!window.authorizer.hasAccess("devices", 2))
|
||||||
|
return reject(new Error("You are not authorized to view this page"));
|
||||||
|
|
||||||
let filter = new Filter(args.filter);
|
let filter = new Filter(args.filter);
|
||||||
let indexParameters = Object.values(config.get("ui.index")).map(p =>
|
let indexParameters = Object.values(config.get("ui.index")).map(p =>
|
||||||
Object.assign({}, p, {
|
Object.assign({}, p, {
|
||||||
@@ -177,14 +180,21 @@ function renderActions(selected) {
|
|||||||
const ids = Array.from(selected);
|
const ids = Array.from(selected);
|
||||||
if (!confirm(`Deleting ${ids.length} devices. Are you sure?`)) return;
|
if (!confirm(`Deleting ${ids.length} devices. Are you sure?`)) return;
|
||||||
|
|
||||||
Promise.all(ids.map(id => store.deleteResource("devices", id)))
|
let counter = 1;
|
||||||
.then(() => {
|
for (let id of ids) {
|
||||||
notifications.push("success", `${ids.size} devices deleted`);
|
++counter;
|
||||||
store.fulfill(0, Date.now());
|
store
|
||||||
})
|
.deleteResource("devices", id)
|
||||||
.catch(err => {
|
.then(() => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("success", `${id}: Deleted`);
|
||||||
});
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Delete"
|
"Delete"
|
||||||
@@ -202,19 +212,21 @@ function renderActions(selected) {
|
|||||||
const tag = prompt(`Enter tag to assign to ${ids.length} devices:`);
|
const tag = prompt(`Enter tag to assign to ${ids.length} devices:`);
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
Promise.all(
|
let counter = 1;
|
||||||
ids.map(id => {
|
for (let id of ids) {
|
||||||
store.updateTags(id, { [tag]: true });
|
++counter;
|
||||||
})
|
store
|
||||||
)
|
.updateTags(id, { [tag]: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push(
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
"success",
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
`Tag '${tag}' assigned to ${ids.length} devices`
|
})
|
||||||
);
|
.catch(err => {
|
||||||
store.fulfill(0, Date.now());
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
})
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
.catch(err => notifications.push("error", err.message));
|
});
|
||||||
|
}
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Tag"
|
"Tag"
|
||||||
@@ -234,19 +246,21 @@ function renderActions(selected) {
|
|||||||
);
|
);
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
Promise.all(
|
let counter = 1;
|
||||||
ids.map(id => {
|
for (let id of ids) {
|
||||||
store.updateTags(id, { [tag]: false });
|
++counter;
|
||||||
})
|
store
|
||||||
)
|
.updateTags(id, { [tag]: false })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push(
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
"success",
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
`Tag '${tag}' unassigned from ${ids.length} devices`
|
})
|
||||||
);
|
.catch(err => {
|
||||||
store.fulfill(0, Date.now());
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
})
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
.catch(err => notifications.push("error", err.message));
|
});
|
||||||
|
}
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Untag"
|
"Untag"
|
||||||
|
|||||||
+30
-25
@@ -243,41 +243,46 @@ const component = {
|
|||||||
t => t.status === "queued"
|
t => t.status === "queued"
|
||||||
);
|
);
|
||||||
taskQueue
|
taskQueue
|
||||||
.commit(tasks, (deviceId, connectionRequestStatus, tasks2) => {
|
.commit(
|
||||||
if (connectionRequestStatus !== "OK") {
|
tasks,
|
||||||
notifications.push(
|
(deviceId, err, connectionRequestStatus, tasks2) => {
|
||||||
"error",
|
if (err)
|
||||||
`${deviceId}: ${connectionRequestStatus}`
|
return notifications.push(
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let t of tasks2)
|
|
||||||
if (t.status === "stale") {
|
|
||||||
notifications.push(
|
|
||||||
"error",
|
"error",
|
||||||
`${deviceId}: No contact from device`
|
`${deviceId}: ${err.message}`
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
} else if (t.status === "fault") {
|
if (connectionRequestStatus !== "OK") {
|
||||||
notifications.push(
|
notifications.push(
|
||||||
"error",
|
"error",
|
||||||
`${deviceId}: Task(s) faulted`
|
`${deviceId}: ${connectionRequestStatus}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.push(
|
for (let t of tasks2)
|
||||||
"success",
|
if (t.status === "stale") {
|
||||||
`${deviceId}: Task(s) committed`
|
notifications.push(
|
||||||
);
|
"error",
|
||||||
})
|
`${deviceId}: No contact from device`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (t.status === "fault") {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: Task(s) faulted`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
`${deviceId}: Task(s) committed`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
store.fulfill(0, Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
notifications.push("error", err.message);
|
|
||||||
throw err;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
import m from "mithril";
|
||||||
|
|
||||||
const component = {
|
const component = {
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
document.title = "Error! - GenieACS";
|
document.title = "Error! - GenieACS";
|
||||||
return vnode.attrs.error;
|
return m("p.error", vnode.attrs.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import * as store from "./store";
|
|||||||
import * as notifications from "./notifications";
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
function init(args) {
|
function init(args) {
|
||||||
return new Promise(resolve => {
|
if (!window.authorizer.hasAccess("devices", 2))
|
||||||
let filter = new Filter(args.filter);
|
return Promise.reject(
|
||||||
resolve({ filter: filter });
|
new Error("You are not authorized to view this page")
|
||||||
});
|
);
|
||||||
|
|
||||||
|
let filter = new Filter(args.filter);
|
||||||
|
return Promise.resolve({ filter: filter });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(faultsResponse, total, selected, showMoreCallback) {
|
function renderTable(faultsResponse, total, selected, showMoreCallback) {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import m from "mithril";
|
|||||||
|
|
||||||
import menu from "./menu";
|
import menu from "./menu";
|
||||||
import drawerComponent from "./drawer-component";
|
import drawerComponent from "./drawer-component";
|
||||||
|
import userMenu from "./user-menu";
|
||||||
|
|
||||||
const layout = {
|
const layout = {
|
||||||
view: vnode => {
|
view: vnode => {
|
||||||
return [
|
return [
|
||||||
m("#header", [
|
m("#header", [
|
||||||
m("img.logo", { src: "logo.svg" }),
|
m("img.logo", { src: "logo.svg" }),
|
||||||
|
m(userMenu),
|
||||||
m(menu, { page: vnode.attrs.page }),
|
m(menu, { page: vnode.attrs.page }),
|
||||||
m(drawerComponent)
|
m(drawerComponent)
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import m from "mithril";
|
||||||
|
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
|
function init(args) {
|
||||||
|
return Promise.resolve(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = {
|
||||||
|
view: vnode => {
|
||||||
|
if (window.username) m.route.set(vnode.attrs.continue || "/");
|
||||||
|
|
||||||
|
document.title = "Login - GenieACS";
|
||||||
|
return [
|
||||||
|
m("h1", "Log in"),
|
||||||
|
m(
|
||||||
|
"form",
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m("label", { for: "username" }, "Username"),
|
||||||
|
m("input", {
|
||||||
|
name: "username",
|
||||||
|
type: "text",
|
||||||
|
value: vnode.state.username,
|
||||||
|
oncreate: vnode2 => {
|
||||||
|
vnode2.dom.focus();
|
||||||
|
},
|
||||||
|
oninput: m.withAttr("value", v => (vnode.state.username = v))
|
||||||
|
})
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m("label", { for: "password" }, "Password"),
|
||||||
|
m("input", {
|
||||||
|
name: "password",
|
||||||
|
type: "password",
|
||||||
|
value: vnode.state.password,
|
||||||
|
oninput: m.withAttr("value", v => (vnode.state.password = v))
|
||||||
|
})
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
type: "submit",
|
||||||
|
onclick: e => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
store
|
||||||
|
.logIn(vnode.state.username, vnode.state.password)
|
||||||
|
.then(() => {
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
e.target.disabled = false;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Login"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { init, component };
|
||||||
+16
-7
@@ -6,26 +6,35 @@ const menu = {
|
|||||||
view: vnode => {
|
view: vnode => {
|
||||||
const active = { [vnode.attrs.page]: "active" };
|
const active = { [vnode.attrs.page]: "active" };
|
||||||
|
|
||||||
return m(
|
let tabs = [];
|
||||||
"nav",
|
if (window.authorizer.hasAccess("devices", 1))
|
||||||
m("ul", [
|
tabs.push(
|
||||||
m(
|
m(
|
||||||
"li",
|
"li",
|
||||||
{ class: active["overview"] },
|
{ class: active["overview"] },
|
||||||
m("a", { href: "#!/overview" }, "Overview")
|
m("a", { href: "#!/overview" }, "Overview")
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("devices", 2))
|
||||||
|
tabs.push(
|
||||||
m(
|
m(
|
||||||
"li",
|
"li",
|
||||||
{ class: active["devices"] },
|
{ class: active["devices"] },
|
||||||
m("a", { href: "#!/devices" }, "Devices")
|
m("a", { href: "#!/devices" }, "Devices")
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("faults", 2))
|
||||||
|
tabs.push(
|
||||||
m(
|
m(
|
||||||
"li",
|
"li",
|
||||||
{ class: active["faults"] },
|
{ class: active["faults"] },
|
||||||
m("a", { href: "#!/faults" }, "Faults")
|
m("a", { href: "#!/faults" }, "Faults")
|
||||||
)
|
)
|
||||||
])
|
);
|
||||||
);
|
|
||||||
|
return m("nav", m("ul", tabs));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ function queryCharts(charts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const init = function() {
|
const init = function() {
|
||||||
return new Promise(resolve => {
|
if (!window.authorizer.hasAccess("devices", 1))
|
||||||
resolve({ charts: queryCharts(CHARTS) });
|
return Promise.reject(
|
||||||
});
|
new Error("You are not authorized to view this page")
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.resolve({ charts: queryCharts(CHARTS) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const component = {
|
const component = {
|
||||||
|
|||||||
+44
-36
@@ -323,43 +323,33 @@ function getTimestamp() {
|
|||||||
return fulfillTimestamp;
|
return fulfillTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function postTasks(tasks, callback) {
|
function postTasks(deviceId, tasks) {
|
||||||
|
for (let t of tasks) {
|
||||||
|
t.status = "pending";
|
||||||
|
t.device = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let devices = {};
|
m
|
||||||
for (let t of tasks) {
|
.request({
|
||||||
t.status = "pending";
|
method: "POST",
|
||||||
devices[t.device] = devices[t.device] || [];
|
url: `/api/devices/${encodeURIComponent(deviceId)}/tasks`,
|
||||||
devices[t.device].push(t);
|
data: tasks,
|
||||||
}
|
extract: xhr => {
|
||||||
|
if (xhr.status !== 200) throw new Error(xhr.response);
|
||||||
let promises = [];
|
const connectionRequestStatus = xhr.getResponseHeader(
|
||||||
for (let [deviceId, tasks2] of Object.entries(devices))
|
"Connection-Request"
|
||||||
promises.push(
|
);
|
||||||
m.request({
|
let st = JSON.parse(xhr.response);
|
||||||
method: "POST",
|
for (let [i, t] of st.entries()) {
|
||||||
url: `/api/devices/${encodeURIComponent(deviceId)}/tasks`,
|
tasks[i]._id = t._id;
|
||||||
data: tasks2,
|
tasks[i].status = t.status;
|
||||||
extract: xhr => {
|
tasks[i].fault = t.fault;
|
||||||
const connectionRequestStatus = xhr.getResponseHeader(
|
|
||||||
"Connection-Request"
|
|
||||||
);
|
|
||||||
let st = JSON.parse(xhr.response);
|
|
||||||
for (let [i, t] of st.entries()) {
|
|
||||||
tasks2[i]._id = t._id;
|
|
||||||
tasks2[i].status = t.status;
|
|
||||||
tasks2[i].fault = t.fault;
|
|
||||||
}
|
|
||||||
if (callback) callback(deviceId, connectionRequestStatus, tasks2);
|
|
||||||
}
|
}
|
||||||
})
|
resolve(connectionRequestStatus);
|
||||||
);
|
}
|
||||||
|
})
|
||||||
Promise.all(promises)
|
.catch(reject);
|
||||||
.then(() => resolve(tasks))
|
|
||||||
.catch(err => {
|
|
||||||
callback = null;
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +384,22 @@ function evaluateExpression(exp, device) {
|
|||||||
return exps.get(device);
|
return exps.get(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logIn(username, password) {
|
||||||
|
return m.request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/login",
|
||||||
|
background: true,
|
||||||
|
data: { username, password }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOut() {
|
||||||
|
return m.request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/logout"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
count,
|
count,
|
||||||
fetch,
|
fetch,
|
||||||
@@ -403,5 +409,7 @@ export {
|
|||||||
postTasks,
|
postTasks,
|
||||||
updateTags,
|
updateTags,
|
||||||
deleteResource,
|
deleteResource,
|
||||||
evaluateExpression
|
evaluateExpression,
|
||||||
|
logIn,
|
||||||
|
logOut
|
||||||
};
|
};
|
||||||
+23
-7
@@ -37,16 +37,32 @@ function stageSpv(task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function commit(tasks, callback) {
|
function commit(tasks, callback) {
|
||||||
|
let devices = {};
|
||||||
for (let t of tasks) {
|
for (let t of tasks) {
|
||||||
t.status = "pending";
|
devices[t.device] = devices[t.device] || [];
|
||||||
queue.add(t);
|
devices[t.device].push(t);
|
||||||
|
queueTask(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.postTasks(tasks, (deviceId, connectionRequestStatus, tasks2) => {
|
return new Promise(resolve => {
|
||||||
for (let t of tasks2)
|
let counter = 1;
|
||||||
if (t.status === "pending") t.status = "stale";
|
for (let [deviceId, tasks2] of Object.entries(devices))
|
||||||
else if (t.status === "done") queue.delete(t);
|
store
|
||||||
if (callback) callback(deviceId, connectionRequestStatus, tasks2);
|
.postTasks(deviceId, tasks)
|
||||||
|
.then(connectionRequestStatus => {
|
||||||
|
for (let t of tasks2)
|
||||||
|
if (t.status === "pending") t.status = "stale";
|
||||||
|
else if (t.status === "done") queue.delete(t);
|
||||||
|
callback(deviceId, null, connectionRequestStatus, tasks2);
|
||||||
|
if (--counter === 0) resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
for (let t of tasks2) t.status = "stale";
|
||||||
|
callback(deviceId, err, null, tasks2);
|
||||||
|
if (--counter === 0) resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (--counter === 0) resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import m from "mithril";
|
||||||
|
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
|
const userMenu = {
|
||||||
|
view: () => {
|
||||||
|
if (window.username)
|
||||||
|
return m(
|
||||||
|
"div.user-menu",
|
||||||
|
window.username,
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: e => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
store
|
||||||
|
.logOut()
|
||||||
|
.then(() => {
|
||||||
|
location.hash = "";
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Log out"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return m(
|
||||||
|
"div.user-menu",
|
||||||
|
m(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
href: "#!/login?" + m.buildQueryString({ continue: m.route.get() })
|
||||||
|
},
|
||||||
|
"Log in"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userMenu;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const validators = {
|
||||||
|
pass: require("./validators/pass"),
|
||||||
|
test: require("./validators/test")
|
||||||
|
};
|
||||||
|
|
||||||
|
class Authorizer {
|
||||||
|
constructor(permissionSets) {
|
||||||
|
this.permissionSets = permissionSets;
|
||||||
|
this.validtorCache = new WeakMap();
|
||||||
|
this.hasAccessCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAccess(resourceType, access) {
|
||||||
|
const cacheKey = `${resourceType}-${access}`;
|
||||||
|
if (cacheKey in this.hasAccessCache)
|
||||||
|
return this.hasAccessCache[cacheKey];
|
||||||
|
|
||||||
|
let has = false;
|
||||||
|
for (const permissionSet of this.permissionSets) {
|
||||||
|
for (const perm of permissionSet) {
|
||||||
|
if (perm[resourceType]) {
|
||||||
|
if (perm[resourceType].access >= access) {
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasAccessCache[cacheKey] = has;
|
||||||
|
return has;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidator(resourceType, resource) {
|
||||||
|
if (this.validtorCache.has(resource))
|
||||||
|
return this.validtorCache.get(resource);
|
||||||
|
|
||||||
|
let funcs = {};
|
||||||
|
|
||||||
|
for (let permissionSet of this.permissionSets)
|
||||||
|
for (let perm of permissionSet)
|
||||||
|
if (perm[resourceType]) {
|
||||||
|
if (perm[resourceType].access >= 3) {
|
||||||
|
if (perm[resourceType].validate)
|
||||||
|
for (let [k, v] of Object.entries(perm[resourceType].validate))
|
||||||
|
funcs[k] = v;
|
||||||
|
} else {
|
||||||
|
funcs = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = (mutationType, mutation, any) => {
|
||||||
|
let valid = false;
|
||||||
|
for (let [k, v] of Object.entries(funcs))
|
||||||
|
if (v) {
|
||||||
|
let res = validators[k](
|
||||||
|
resourceType,
|
||||||
|
resource,
|
||||||
|
mutationType,
|
||||||
|
mutation,
|
||||||
|
any
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res > 0) valid = true;
|
||||||
|
else if (res < 0) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.validtorCache.set(resource, validator);
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Authorizer;
|
||||||
+5
-3
@@ -17,9 +17,10 @@ function* permute(arr) {
|
|||||||
|
|
||||||
class Filter {
|
class Filter {
|
||||||
constructor(str) {
|
constructor(str) {
|
||||||
if (Array.isArray(str)) this.ast = str;
|
if (!str) this.ast = null;
|
||||||
|
else if (Array.isArray(str)) this.ast = str;
|
||||||
else if (str instanceof Filter) this.ast = str.ast;
|
else if (str instanceof Filter) this.ast = str.ast;
|
||||||
else if (!str) this.ast = null;
|
else if (Array.isArray(str.ast) || str.ast === null) this.ast = str.ast;
|
||||||
else this.ast = filterParser.parse(str);
|
else this.ast = filterParser.parse(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ class Filter {
|
|||||||
|
|
||||||
or(fltr) {
|
or(fltr) {
|
||||||
if (!fltr || !this.ast) return this;
|
if (!fltr || !this.ast) return this;
|
||||||
|
else if (!fltr.ast) return fltr;
|
||||||
|
|
||||||
let f = ["OR"];
|
let f = ["OR"];
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ class Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
and(fltr) {
|
and(fltr) {
|
||||||
if (!fltr) return this;
|
if (!fltr || !fltr.ast) return this;
|
||||||
else if (!this.ast) return fltr;
|
else if (!this.ast) return fltr;
|
||||||
|
|
||||||
let f = ["AND"];
|
let f = ["AND"];
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
function pass() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = pass;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
function test(resourceType, resource) {
|
||||||
|
if (resourceType === "devices") {
|
||||||
|
if (resource["Tags.test"] && resource["Tags.test"].value[0]) return 1;
|
||||||
|
} else if (resourceType === "presets") {
|
||||||
|
if (resource["_id"].startsWith("test-"))
|
||||||
|
// TODO check "test" tag too
|
||||||
|
return 1;
|
||||||
|
} else if (resourceType === "provisions") {
|
||||||
|
if (resource["_id"].startsWith("test-")) return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = test;
|
||||||
+11
@@ -1,6 +1,17 @@
|
|||||||
{
|
{
|
||||||
"server.port": 3000,
|
"server.port": 3000,
|
||||||
"server.nbi": "http://127.0.0.1:7557/",
|
"server.nbi": "http://127.0.0.1:7557/",
|
||||||
|
"server.jwtSecret": "jwtSecret",
|
||||||
|
"auth.simple.users.admin.password": "admin",
|
||||||
|
"auth.simple.users.admin.roles": "admin",
|
||||||
|
"auth.simple.users.view.password": "view",
|
||||||
|
"auth.simple.users.view.roles": "view",
|
||||||
|
"permissions.admin.1.devices.access": 3,
|
||||||
|
"permissions.admin.1.devices.validate.pass": true,
|
||||||
|
"permissions.admin.1.faults.access": 3,
|
||||||
|
"permissions.admin.1.faults.validate.pass": true,
|
||||||
|
"permissions.view.1.devices.access": 2,
|
||||||
|
"permissions.view.1.faults.access": 2,
|
||||||
"ui.pageSize": 10,
|
"ui.pageSize": 10,
|
||||||
"ui.filters.1.label": "Serial number",
|
"ui.filters.1.label": "Serial number",
|
||||||
"ui.filters.1.parameter": "DeviceID.SerialNumber",
|
"ui.filters.1.parameter": "DeviceID.SerialNumber",
|
||||||
|
|||||||
+3
-1
@@ -4,16 +4,18 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"lint": "eslint --fix common/*.js server/*.js client/*.js client/components/*.js;prettier --write common/*.js server/*.js client/*.js client/components/*.js client/css/*.css",
|
"lint": "eslint --fix common/*.js common/validators/*.js server/*.js client/*.js client/components/*.js;prettier --write common/*.js common/validators/*.js server/*.js client/*.js client/components/*.js client/css/*.css",
|
||||||
"build": "npm run lint;webpack;postcss -c postcss.config.js client/css/app.css -o public/app.css",
|
"build": "npm run lint;webpack;postcss -c postcss.config.js client/css/app.css -o public/app.css",
|
||||||
"start": "node server/server"
|
"start": "node server/server"
|
||||||
},
|
},
|
||||||
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jsonwebtoken": "^8.2.1",
|
||||||
"koa": "^2.5.0",
|
"koa": "^2.5.0",
|
||||||
"koa-body": "^2.5.0",
|
"koa-body": "^2.5.0",
|
||||||
"koa-bodyparser": "^4.2.0",
|
"koa-bodyparser": "^4.2.0",
|
||||||
"koa-compress": "^2.0.0",
|
"koa-compress": "^2.0.0",
|
||||||
|
"koa-jwt": "^3.3.1",
|
||||||
"koa-router": "^7.4.0",
|
"koa-router": "^7.4.0",
|
||||||
"koa-static": "^4.0.2",
|
"koa-static": "^4.0.2",
|
||||||
"mithril": "^1.1.6",
|
"mithril": "^1.1.6",
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function filterToQuery(filter, negate = false, res = {}) {
|
|||||||
function count(resource, filter, limit) {
|
function count(resource, filter, limit) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let qs = {};
|
let qs = {};
|
||||||
if (filter) {
|
if (filter && filter.ast) {
|
||||||
filter = filter.evaluateExpressions();
|
filter = filter.evaluateExpressions();
|
||||||
let ast = unpackTimestamps(filter.ast);
|
let ast = unpackTimestamps(filter.ast);
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ function query(resource, filter, limit, skip, projection, callback) {
|
|||||||
let ret;
|
let ret;
|
||||||
if (!callback) ret = [];
|
if (!callback) ret = [];
|
||||||
let qs = {};
|
let qs = {};
|
||||||
if (filter) {
|
if (filter && filter.ast) {
|
||||||
filter = filter.evaluateExpressions();
|
filter = filter.evaluateExpressions();
|
||||||
let ast = unpackTimestamps(filter.ast);
|
let ast = unpackTimestamps(filter.ast);
|
||||||
|
|
||||||
|
|||||||
+42
-9
@@ -18,11 +18,13 @@ const resources = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (let [resource, update] of Object.entries(resources)) {
|
for (let [resource, update] of Object.entries(resources)) {
|
||||||
router.head(`/${resource}`, async ctx => {
|
router.head(`/${resource}`, async (ctx, next) => {
|
||||||
let filter, limit;
|
let filter, limit;
|
||||||
if (ctx.request.query.filter) filter = new Filter(ctx.request.query.filter);
|
if (ctx.request.query.filter) filter = new Filter(ctx.request.query.filter);
|
||||||
if (ctx.request.query.limit) limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) limit = +ctx.request.query.limit;
|
||||||
|
|
||||||
|
if (!ctx.state.authorizer.hasAccess(resource, 1)) return next();
|
||||||
|
|
||||||
// Exclude temporary tasks and faults
|
// Exclude temporary tasks and faults
|
||||||
if (resource === "tasks" || resource === "faults") {
|
if (resource === "tasks" || resource === "faults") {
|
||||||
let f = new Filter(["NOT", ["<", "expiry", Date.now() + 60000]]);
|
let f = new Filter(["NOT", ["<", "expiry", Date.now() + 60000]]);
|
||||||
@@ -34,12 +36,14 @@ for (let [resource, update] of Object.entries(resources)) {
|
|||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get(`/${resource}`, async ctx => {
|
router.get(`/${resource}`, async (ctx, next) => {
|
||||||
let filter, limit, skip;
|
let filter, limit, skip;
|
||||||
if (ctx.request.query.filter) filter = new Filter(ctx.request.query.filter);
|
if (ctx.request.query.filter) filter = new Filter(ctx.request.query.filter);
|
||||||
if (ctx.request.query.limit) limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) skip = +ctx.request.query.skip;
|
||||||
|
|
||||||
|
if (!ctx.state.authorizer.hasAccess(resource, 2)) return next();
|
||||||
|
|
||||||
// Exclude temporary tasks and faults
|
// Exclude temporary tasks and faults
|
||||||
if (resource === "tasks" || resource === "faults") {
|
if (resource === "tasks" || resource === "faults") {
|
||||||
let f = new Filter(["NOT", ["<", "expiry", Date.now() + 60000]]);
|
let f = new Filter(["NOT", ["<", "expiry", Date.now() + 60000]]);
|
||||||
@@ -57,6 +61,8 @@ for (let [resource, update] of Object.entries(resources)) {
|
|||||||
|
|
||||||
router.head(`/${resource}/:id`, async (ctx, next) => {
|
router.head(`/${resource}/:id`, async (ctx, next) => {
|
||||||
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
||||||
|
if (!ctx.state.authorizer.hasAccess(resource, 2)) return next();
|
||||||
|
|
||||||
let res = await apiFunctions.query(resource, filter);
|
let res = await apiFunctions.query(resource, filter);
|
||||||
if (!res.length) return next();
|
if (!res.length) return next();
|
||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
@@ -64,6 +70,8 @@ for (let [resource, update] of Object.entries(resources)) {
|
|||||||
|
|
||||||
router.get(`/${resource}/:id`, async (ctx, next) => {
|
router.get(`/${resource}/:id`, async (ctx, next) => {
|
||||||
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
||||||
|
if (!ctx.state.authorizer.hasAccess(resource, 2)) return next();
|
||||||
|
|
||||||
let res = await apiFunctions.query(resource, filter);
|
let res = await apiFunctions.query(resource, filter);
|
||||||
if (!res.length) return next();
|
if (!res.length) return next();
|
||||||
ctx.body = res[0];
|
ctx.body = res[0];
|
||||||
@@ -71,22 +79,47 @@ for (let [resource, update] of Object.entries(resources)) {
|
|||||||
|
|
||||||
// TODO add PUT, PATCH routes
|
// TODO add PUT, PATCH routes
|
||||||
if (update)
|
if (update)
|
||||||
router.delete(`/${resource}/:id`, async ctx => {
|
router.delete(`/${resource}/:id`, async (ctx, next) => {
|
||||||
let found = await apiFunctions.deleteResource(resource, ctx.params.id);
|
const authorizer = ctx.state.authorizer;
|
||||||
if (found) ctx.status = 200;
|
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
||||||
else ctx.status = 404;
|
if (!authorizer.hasAccess(resource, 2)) return next();
|
||||||
|
let res = await apiFunctions.query(resource, filter);
|
||||||
|
if (!res.length) return next();
|
||||||
|
|
||||||
|
const validate = authorizer.getValidator(resource, res[0]);
|
||||||
|
if (!validate("delete")) return (ctx.status = 403);
|
||||||
|
|
||||||
|
await apiFunctions.deleteResource(resource, ctx.params.id);
|
||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post("/devices/:id/tasks", async ctx => {
|
router.post("/devices/:id/tasks", async (ctx, next) => {
|
||||||
|
const authorizer = ctx.state.authorizer;
|
||||||
|
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
||||||
|
if (!authorizer.hasAccess("devices", 2)) return next();
|
||||||
|
let devices = await apiFunctions.query("devices", filter);
|
||||||
|
if (!devices.length) return next();
|
||||||
|
|
||||||
|
const validate = authorizer.getValidator("devices", devices[0]);
|
||||||
|
for (let t of ctx.request.body)
|
||||||
|
if (!validate("task", t)) return (ctx.status = 403);
|
||||||
|
|
||||||
let res = await apiFunctions.postTasks(ctx.params.id, ctx.request.body);
|
let res = await apiFunctions.postTasks(ctx.params.id, ctx.request.body);
|
||||||
// TODO 404 if no such device
|
|
||||||
ctx.set("Connection-Request", res.connectionRequest);
|
ctx.set("Connection-Request", res.connectionRequest);
|
||||||
ctx.body = res.tasks;
|
ctx.body = res.tasks;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/devices/:id/tags", async ctx => {
|
router.post("/devices/:id/tags", async (ctx, next) => {
|
||||||
|
const authorizer = ctx.state.authorizer;
|
||||||
|
let filter = new Filter(`DeviceID.ID = "${ctx.params.id}"`);
|
||||||
|
if (!authorizer.hasAccess("devices", 2)) return next();
|
||||||
|
let res = await apiFunctions.query("devices", filter);
|
||||||
|
if (!res.length) return next();
|
||||||
|
|
||||||
|
const validate = authorizer.getValidator("devices", res[0]);
|
||||||
|
if (!validate("tags", ctx.request.body)) return (ctx.status = 403);
|
||||||
|
|
||||||
await apiFunctions.updateTags(ctx.params.id, ctx.request.body);
|
await apiFunctions.updateTags(ctx.params.id, ctx.request.body);
|
||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ const cache = {};
|
|||||||
function parse(name, value) {
|
function parse(name, value) {
|
||||||
if (/^ui\.overview\.charts\.[^.]+\.slices\.[^.]+\.filter$/.test(name))
|
if (/^ui\.overview\.charts\.[^.]+\.slices\.[^.]+\.filter$/.test(name))
|
||||||
return new Filter(value);
|
return new Filter(value);
|
||||||
|
else if (/^permissions\.[^.]+\.[^.]+\.devices\.filter$/.test(name))
|
||||||
|
return new Filter(value);
|
||||||
|
else if (/^auth\.[^.]+\.users\.[^.]+\.roles$/.test(name))
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.map(x => x.trim())
|
||||||
|
.sort();
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,73 @@ const Router = require("koa-router");
|
|||||||
const koaStatic = require("koa-static");
|
const koaStatic = require("koa-static");
|
||||||
const koaCompress = require("koa-compress");
|
const koaCompress = require("koa-compress");
|
||||||
const koaBody = require("koa-body");
|
const koaBody = require("koa-body");
|
||||||
|
const koaJwt = require("koa-jwt");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const api = require("./api");
|
const api = require("./api");
|
||||||
|
const Authorizer = require("../common/authorizer");
|
||||||
|
|
||||||
const koa = new Koa();
|
const koa = new Koa();
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
|
const JWT_SECRET = config.get("server.jwtSecret");
|
||||||
|
const JWT_COOKIE = "genieacs-ui-jwt";
|
||||||
|
|
||||||
|
function getPermissionSets(roles) {
|
||||||
|
const allPermissions = config.get("permissions");
|
||||||
|
const permissionSets = roles.map(r => Object.values(allPermissions[r] || {}));
|
||||||
|
return permissionSets;
|
||||||
|
}
|
||||||
|
|
||||||
|
koa.use(
|
||||||
|
koaJwt({
|
||||||
|
secret: JWT_SECRET,
|
||||||
|
passthrough: true,
|
||||||
|
cookie: JWT_COOKIE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
koa.use(async (ctx, next) => {
|
||||||
|
if (ctx.state.user && ctx.state.user.roles)
|
||||||
|
ctx.state.authorizer = new Authorizer(
|
||||||
|
getPermissionSets(ctx.state.user.roles)
|
||||||
|
);
|
||||||
|
else ctx.state.authorizer = new Authorizer([]);
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/login", async ctx => {
|
||||||
|
const username = ctx.request.body.username;
|
||||||
|
const password = ctx.request.body.password;
|
||||||
|
|
||||||
|
const user = config.get(`auth.simple.users.${username}`);
|
||||||
|
|
||||||
|
if (!user || user.password !== password) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = "Incorrect username or password";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = jwt.sign({ username: username, roles: user.roles }, JWT_SECRET);
|
||||||
|
ctx.cookies.set(JWT_COOKIE, token);
|
||||||
|
ctx.body = JSON.stringify(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/logout", async ctx => {
|
||||||
|
ctx.cookies.set(JWT_COOKIE); // Delete cookie
|
||||||
|
ctx.body = "";
|
||||||
|
});
|
||||||
|
|
||||||
koa.use(koaBody());
|
koa.use(koaBody());
|
||||||
router.use("/api", api.routes(), api.allowedMethods());
|
router.use("/api", api.routes(), api.allowedMethods());
|
||||||
|
|
||||||
router.get("/", async ctx => {
|
router.get("/", async ctx => {
|
||||||
|
let permissionSets = [];
|
||||||
|
if (ctx.state.user && ctx.state.user.roles)
|
||||||
|
permissionSets = getPermissionSets(ctx.state.user.roles);
|
||||||
|
|
||||||
ctx.body = `
|
ctx.body = `
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -26,6 +82,10 @@ router.get("/", async ctx => {
|
|||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
window.clientConfig = ${JSON.stringify(config.getClientConfig())};
|
window.clientConfig = ${JSON.stringify(config.getClientConfig())};
|
||||||
|
window.username = ${JSON.stringify(
|
||||||
|
ctx.state.user ? ctx.state.user.username : ""
|
||||||
|
)};
|
||||||
|
window.permissionSets = ${JSON.stringify(permissionSets)};
|
||||||
</script>
|
</script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in new issue
Block a user