From 6cb0d0449b3134bf35c2c5592fdb575e6be7ba29 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Thu, 11 Jul 2019 17:47:36 -0700 Subject: [PATCH] Database initialization wizard --- README.md | 9 +- build/build.ts | 2 +- lib/init.ts | 425 ++++++++++++++++++++++++++++++++++++++++++++ lib/local-cache.ts | 11 +- lib/ui.ts | 49 ++++- ui/app.ts | 2 + ui/css/app.css | 9 + ui/devices-page.ts | 6 + ui/overview-page.ts | 2 +- ui/wizard-page.ts | 117 ++++++++++++ 10 files changed, 613 insertions(+), 19 deletions(-) create mode 100644 lib/init.ts create mode 100644 ui/wizard-page.ts diff --git a/README.md b/README.md index f34b2b5..c7287d9 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,9 @@ This serves the web based user interface. It listens on port 3000 by default. The argument *--ui-jwt-secret* supplies the key used for signing browser cookies. -The UI has plenty of configuration options. To populate the an initial sample -configuration, run: - - ./dist/tools/configure-ui - -Use admin/admin credentials to log in. +The UI has plenty of configuration options. When you open GenieACS's UI in a +browser you'll be greeted with a database initialization wizard to help you +populate some initial configuration. ## Support diff --git a/build/build.ts b/build/build.ts index b671010..dc40e18 100644 --- a/build/build.ts +++ b/build/build.ts @@ -169,7 +169,7 @@ async function generateCss(): Promise { } async function generateToolsJs(): Promise { - for (const bin of ["configure-ui", "dump-data-model"]) { + for (const bin of ["dump-data-model"]) { const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`); const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`); const bundle = await rollup({ diff --git a/lib/init.ts b/lib/init.ts new file mode 100644 index 0000000..593395f --- /dev/null +++ b/lib/init.ts @@ -0,0 +1,425 @@ +/** + * Copyright 2013-2019 GenieACS Inc. + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import * as localCache from "./local-cache"; +import { generateSalt, hashPassword } from "./auth"; +import * as db from "./ui/db"; +import { del } from "./cache"; + +interface Status { + users: boolean; + presets: boolean; + filters: boolean; + device: boolean; + index: boolean; + overview: boolean; +} + +const BOOTSTRAP_SCRIPT = ` +const now = Date.now(); + +// Clear cached data model to force a refresh +clear("Device", now); +clear("InternetGatewayDevice", now); +`.trim(); + +const DEFAULT_SCRIPT = ` +const hourly = Date.now(3600000); + +// Refresh basic parameters hourly +declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", {path: hourly, value: hourly}); +declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", {path: hourly, value: hourly}); +declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress", {path: hourly, value: hourly}); +declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress", {path: hourly, value: hourly}); +declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", {path: hourly, value: hourly}); +// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons +declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", {path: hourly, value: 1}); +declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", {path: hourly, value: hourly}); +declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", {path: hourly, value: hourly}); +declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", {path: hourly, value: hourly}); +`.trim(); + +const INFORM_SCRIPT = ` +// Device ID as user name +const username = declare("DeviceID.ID", {value: 1}).value[0] + +// Password will be fixed a given device because Math.random() is seeded with devcie ID by default. +const password = "" + Math.random(); + +const informInterval = 300; + +// Refresh values daily +const daily = Date.now(86400000); + +// Unique inform offset per device for better load distribution +const informTime = daily % 86400000; + +declare("InternetGatewayDevice.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username}); +declare("InternetGatewayDevice.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password}); +declare("InternetGatewayDevice.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true}); +declare("InternetGatewayDevice.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval}); +declare("InternetGatewayDevice.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime}); + +declare("Device.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username}); +declare("Device.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password}); +declare("Device.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true}); +declare("Device.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval}); +declare("Device.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime}); +`.trim(); + +export async function getStatus(): Promise { + const configSnapshot = await localCache.getCurrentSnapshot(); + const users = localCache.getUsers(configSnapshot); + const presets = localCache.getPresets(configSnapshot); + const ui = localCache.getUiConfig(configSnapshot); + + return { + users: !Object.keys(users).length, + presets: !presets.length, + filters: !Object.keys(ui["filters"]).length, + device: !Object.keys(ui["device"]).length, + index: !Object.keys(ui["index"]).length, + overview: !Object.keys(ui["overview"]).length + }; +} + +export async function seed(options): Promise { + const resources = {}; + const proms = []; + + if (options.users) { + resources["permissions"] = [ + { role: "admin", resource: "devices", access: 3, validate: "true" }, + { role: "admin", resource: "faults", access: 3, validate: "true" }, + { role: "admin", resource: "files", access: 3, validate: "true" }, + { role: "admin", resource: "presets", access: 3, validate: "true" }, + { role: "admin", resource: "provisions", access: 3, validate: "true" }, + { role: "admin", resource: "config", access: 3, validate: "true" }, + { role: "admin", resource: "permissions", access: 3, validate: "true" }, + { role: "admin", resource: "users", access: 3, validate: "true" }, + { + role: "admin", + resource: "virtualParameters", + access: 3, + validate: "true" + } + ]; + + resources["users"] = [ + { username: "admin", password: "admin", roles: ["admin"] } + ]; + } + + if (options.filters) { + resources["config"] = (resources["config"] || []).concat([ + { _id: "ui.filters.0.label", value: "'Serial number'" }, + { _id: "ui.filters.0.parameter", value: "DeviceID.SerialNumber" }, + { _id: "ui.filters.0.type", value: "'string'" }, + { _id: "ui.filters.1.label", value: "'Product class'" }, + { _id: "ui.filters.1.parameter", value: "DeviceID.ProductClass" }, + { _id: "ui.filters.1.type", value: "'string'" }, + { _id: "ui.filters.2.label", value: "'Tag'" }, + { _id: "ui.filters.2.parameter", value: "tag" }, + { _id: "ui.filters.2.type", value: "'string'" } + ]); + } + + if (options.device) { + resources["config"] = (resources["config"] || []).concat([ + { _id: "ui.device.0.type", value: "'tags'" }, + { _id: "ui.device.1.type", value: "'ping'" }, + { _id: "ui.device.2.type", value: "'parameter-list'" }, + { _id: "ui.device.2.parameters.0.type", value: "'container'" }, + { _id: "ui.device.2.parameters.0.element", value: "'span.inform'" }, + { _id: "ui.device.2.parameters.0.label", value: "'Last inform'" }, + { + _id: "ui.device.2.parameters.0.components.0.type", + value: "'parameter'" + }, + { _id: "ui.device.2.parameters.0.components.1.chart", value: "'online'" }, + { + _id: "ui.device.2.parameters.0.components.1.type", + value: "'overview-dot'" + }, + { + _id: "ui.device.2.parameters.0.components.2.type", + value: "'summon-button'" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.0", + value: "InternetGatewayDevice.DeviceInfo.HardwareVersion" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.1", + value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.2", + value: + "InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.3", + value: + "InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.4", + value: "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.5", + value: + "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.6", + value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.7", + value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress" + }, + { + _id: "ui.device.2.parameters.0.components.2.parameters.8", + value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress" + }, + { + _id: "ui.device.2.parameters.0.parameter", + value: "DATE_STRING(Events.Inform)" + }, + { _id: "ui.device.2.parameters.1.label", value: "'Serial number'" }, + { + _id: "ui.device.2.parameters.1.parameter", + value: "DeviceID.SerialNumber" + }, + { _id: "ui.device.2.parameters.2.label", value: "'Product class'" }, + { + _id: "ui.device.2.parameters.2.parameter", + value: "DeviceID.ProductClass" + }, + { _id: "ui.device.2.parameters.3.label", value: "'OUI'" }, + { + _id: "ui.device.2.parameters.3.parameter", + value: "DeviceID.OUI" + }, + { _id: "ui.device.2.parameters.4.label", value: "'Manufacturer'" }, + { + _id: "ui.device.2.parameters.4.parameter", + value: "DeviceID.Manufacturer" + }, + { _id: "ui.device.2.parameters.5.label", value: "'Hardware version'" }, + { + _id: "ui.device.2.parameters.5.parameter", + value: "InternetGatewayDevice.DeviceInfo.HardwareVersion" + }, + { _id: "ui.device.2.parameters.6.label", value: "'Software version'" }, + { + _id: "ui.device.2.parameters.6.parameter", + value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion" + }, + { _id: "ui.device.2.parameters.7.label", value: "'MAC'" }, + { + _id: "ui.device.2.parameters.7.parameter", + value: + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress" + }, + { _id: "ui.device.2.parameters.8.label", value: "'IP'" }, + { + _id: "ui.device.2.parameters.8.parameter", + value: + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress" + }, + { _id: "ui.device.2.parameters.9.label", value: "'WLAN SSID'" }, + { + _id: "ui.device.2.parameters.9.parameter", + value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID" + }, + { _id: "ui.device.2.parameters.10.label", value: "'WLAN passphrase'" }, + { + _id: "ui.device.2.parameters.10.parameter", + value: + "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase" + }, + { _id: "ui.device.3.type", value: "'parameter-table'" }, + { _id: "ui.device.3.label", value: "'WLAN hosts'" }, + { + _id: "ui.device.3.parameter", + value: "InternetGatewayDevice.LANDevice.1.Hosts.Host" + }, + { _id: "ui.device.3.childParameters.0.label", value: "'Host name'" }, + { _id: "ui.device.3.childParameters.0.parameter", value: "HostName" }, + { _id: "ui.device.3.childParameters.1.label", value: "'IP address'" }, + { _id: "ui.device.3.childParameters.1.parameter", value: "IPAddress" }, + { _id: "ui.device.3.childParameters.2.label", value: "'MAC address'" }, + { _id: "ui.device.3.childParameters.2.parameter", value: "MACAddress" }, + { _id: "ui.device.4.type", value: "'container'" }, + { _id: "ui.device.4.element", value: "'div'" }, + { _id: "ui.device.4.components.0.components.0", value: "'Faults'" }, + { _id: "ui.device.4.components.0.element", value: "'h3'" }, + { _id: "ui.device.4.components.0.type", value: "'container'" }, + { _id: "ui.device.4.components.1.type", value: "'device-faults'" }, + { _id: "ui.device.5.type", value: "'container'" }, + { _id: "ui.device.5.element", value: "'div.container-full-width'" }, + { + _id: "ui.device.5.components.0.components.0", + value: "'All parameters'" + }, + { _id: "ui.device.5.components.0.element", value: "'h3'" }, + { _id: "ui.device.5.components.0.type", value: "'container'" }, + { _id: "ui.device.5.components.1.type", value: "'all-parameters'" }, + { _id: "ui.device.6.type", value: "'device-actions'" } + ]); + } + + if (options.index) { + resources["config"] = (resources["config"] || []).concat([ + { _id: "ui.index.0.type", value: "'device-link'" }, + { _id: "ui.index.0.label", value: "'Serial number'" }, + { _id: "ui.index.0.parameter", value: "DeviceID.SerialNumber" }, + { _id: "ui.index.0.components.0.type", value: "'parameter'" }, + { _id: "ui.index.1.label", value: "'Product class'" }, + { _id: "ui.index.1.parameter", value: "DeviceID.ProductClass" }, + { _id: "ui.index.2.label", value: "'Software version'" }, + { + _id: "ui.index.2.parameter", + value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion" + }, + { _id: "ui.index.3.label", value: "'IP'" }, + { + _id: "ui.index.3.parameter", + value: + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress" + }, + { _id: "ui.index.4.label", value: "'SSID'" }, + { + _id: "ui.index.4.parameter", + value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID" + }, + { _id: "ui.index.5.type", value: "'container'" }, + { _id: "ui.index.5.label", value: "'Last inform'" }, + { _id: "ui.index.5.element", value: "'span.inform'" }, + { _id: "ui.index.5.parameter", value: "DATE_STRING(Events.Inform)" }, + { _id: "ui.index.5.components.0.type", value: "'parameter'" }, + { _id: "ui.index.5.components.1.chart", value: "'online'" }, + { _id: "ui.index.5.components.1.type", value: "'overview-dot'" }, + { _id: "ui.index.6.type", value: "'tags'" }, + { _id: "ui.index.6.label", value: "'Tags'" }, + { _id: "ui.index.6.parameter", value: "Tags" }, + { _id: "ui.index.6.unsortable", value: "true" }, + { _id: "ui.index.6.writable", value: "false" } + ]); + } + + if (options.overview) { + resources["config"] = (resources["config"] || []).concat([ + { _id: "ui.overview.charts.online.label", value: "'Online status'" }, + { + _id: "ui.overview.charts.online.slices.1_onlineNow.color", + value: "'#31a354'" + }, + { + _id: "ui.overview.charts.online.slices.1_onlineNow.filter", + value: "Events.Inform > NOW() - 5 * 60 * 1000" + }, + { + _id: "ui.overview.charts.online.slices.1_onlineNow.label", + value: "'Online now'" + }, + { + _id: "ui.overview.charts.online.slices.2_past24.color", + value: "'#a1d99b'" + }, + { + _id: "ui.overview.charts.online.slices.2_past24.filter", + value: + "Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)" + }, + { + _id: "ui.overview.charts.online.slices.2_past24.label", + value: "'Past 24 hours'" + }, + { + _id: "ui.overview.charts.online.slices.3_others.color", + value: "'#e5f5e0'" + }, + { + _id: "ui.overview.charts.online.slices.3_others.filter", + value: "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)" + }, + { + _id: "ui.overview.charts.online.slices.3_others.label", + value: "'Others'" + }, + { _id: "ui.overview.groups.online.label", value: "''" }, + { _id: "ui.overview.groups.online.charts.0", value: "'online'" } + ]); + } + + if (options.presets) { + resources["presets"] = [ + { + _id: "bootstrap", + weight: 0, + channel: "bootstrap", + provision: "bootstrap" + }, + { _id: "default", weight: 0, channel: "default", provision: "default" }, + { _id: "inform", weight: 0, channel: "inform", provision: "inform" } + ]; + + resources["provisions"] = [ + { _id: "bootstrap", script: BOOTSTRAP_SCRIPT }, + { _id: "default", script: DEFAULT_SCRIPT }, + { _id: "inform", script: INFORM_SCRIPT } + ]; + } + + if (resources["permissions"]) { + for (const p of resources["permissions"]) { + p["_id"] = `${p["role"]}:${p["resource"]}:${p["access"]}`; + proms.push(db.putPermission(p["_id"], p)); + } + } + + if (resources["users"]) { + for (const u of resources["users"]) { + u["salt"] = await generateSalt(64); + u["password"] = await hashPassword(u["password"], u["salt"]); + u["roles"] = (u["roles"] || []).join(","); + u["_id"] = u["username"]; + delete u["username"]; + proms.push(db.putUser(u["_id"], u)); + } + } + + if (resources["provisions"]) { + for (const p of resources["provisions"]) + proms.push(db.putProvision(p["_id"], p)); + } + + if (resources["presets"]) + for (const p of resources["presets"]) proms.push(db.putPreset(p["_id"], p)); + + if (resources["config"]) + for (const c of resources["config"]) proms.push(db.putConfig(c["_id"], c)); + + await proms; + return del("presets_hash"); +} diff --git a/lib/local-cache.ts b/lib/local-cache.ts index 13fbc88..b987556 100644 --- a/lib/local-cache.ts +++ b/lib/local-cache.ts @@ -340,7 +340,7 @@ async function fetchConfig(): Promise<[{}, {}]> { filters: {}, device: {}, index: {}, - overview: { charts: {}, groups: {} } + overview: {} }; const _config = {}; @@ -362,15 +362,6 @@ async function fetchConfig(): Promise<[{}, {}]> { } } - if (!Object.keys(ui["index"]).length) { - ui["index"] = { - "0": { - label: "ID", - parameter: ["PARAM", "DeviceID.ID"] - } - }; - } - return [_config, ui]; } diff --git a/lib/ui.ts b/lib/ui.ts index 8ecff9d..96968bc 100644 --- a/lib/ui.ts +++ b/lib/ui.ts @@ -32,6 +32,7 @@ import * as logger from "./logger"; import * as localCache from "./local-cache"; import { PermissionSet } from "./types"; import { authSimple } from "./ui/api-functions"; +import * as init from "./init"; declare module "koa" { interface Request { @@ -142,11 +143,57 @@ koa.use(async (ctx, next) => { koa.use(koaBodyParser()); router.use("/api", api.routes(), api.allowedMethods()); +router.get("/init", async ctx => { + const status = await init.getStatus(); + if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) { + if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false; + if (!ctx.state.authorizer.hasAccess("permissions", 3)) + status["users"] = false; + if (!ctx.state.authorizer.hasAccess("config", 3)) { + status["filters"] = false; + status["device"] = false; + status["index"] = false; + status["overview"] = false; + } + if (!ctx.state.authorizer.hasAccess("presets", 3)) + status["presets"] = false; + if (!ctx.state.authorizer.hasAccess("provisions", 3)) + status["presets"] = false; + } + + ctx.body = status; +}); + +router.post("/init", async ctx => { + const status = ctx.request.body; + if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) { + if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false; + if (!ctx.state.authorizer.hasAccess("permissions", 3)) + status["users"] = false; + if (!ctx.state.authorizer.hasAccess("config", 3)) { + status["filters"] = false; + status["device"] = false; + status["index"] = false; + status["overview"] = false; + } + if (!ctx.state.authorizer.hasAccess("presets", 3)) + status["presets"] = false; + if (!ctx.state.authorizer.hasAccess("provisions", 3)) + status["presets"] = false; + } + await init.seed(status); + ctx.body = ""; +}); + router.get("/", async ctx => { let permissionSets = []; if (ctx.state.user && ctx.state.user.username) permissionSets = getPermissionSets(ctx); + let wizard = ""; + if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) + wizard = ''; + ctx.body = ` @@ -165,7 +212,7 @@ router.get("/", async ctx => { )}; window.permissionSets = ${JSON.stringify(permissionSets)}; - + ${wizard} `; diff --git a/ui/app.ts b/ui/app.ts index 541af8a..136b24f 100644 --- a/ui/app.ts +++ b/ui/app.ts @@ -20,6 +20,7 @@ import m, { RouteResolver } from "mithril"; import layout from "./layout"; import * as store from "./store"; +import * as wizardPage from "./wizard-page"; import * as loginPage from "./login-page"; import * as overviewPage from "./overview-page"; import * as devicesPage from "./devices-page"; @@ -136,6 +137,7 @@ function redirectAdminPage(): RouteResolver { } m.route(document.body, "/overview", { + "/wizard": pagify("wizard", wizardPage), "/login": pagify("login", loginPage), "/overview": pagify("overview", overviewPage), "/devices": pagify("devices", devicesPage), diff --git a/ui/css/app.css b/ui/css/app.css index 9268ec6..ac68deb 100644 --- a/ui/css/app.css +++ b/ui/css/app.css @@ -716,3 +716,12 @@ span.parameter-value { color: #aaa; } } + +.wizard-dialog { + max-width: 720px; + margin: 10px auto; + padding: 10px; + border-radius: 6px; + border: 1px solid var(--color1); + box-shadow: 0 0 10px #ccc; +} diff --git a/ui/devices-page.ts b/ui/devices-page.ts index 49309cf..45462e7 100644 --- a/ui/devices-page.ts +++ b/ui/devices-page.ts @@ -69,6 +69,12 @@ export function init(args): Promise<{}> { const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; const indexParameters = Object.values(config.ui.index); + if (!indexParameters.length) { + indexParameters.push({ + label: "ID", + parameter: ["PARAM", "DeviceID.ID"] + }); + } resolve({ filter, indexParameters, sort }); }); } diff --git a/ui/overview-page.ts b/ui/overview-page.ts index 5bc5f87..1662735 100644 --- a/ui/overview-page.ts +++ b/ui/overview-page.ts @@ -23,7 +23,7 @@ import config from "./config"; import * as store from "./store"; import pieChartComponent from "./pie-chart-component"; -const GROUPS = config.ui.overview.groups; +const GROUPS = config.ui.overview.groups || {}; const CHARTS = {}; for (const group of Object.values(GROUPS)) { for (const chartName of Object.values(group["charts"]) as string[]) diff --git a/ui/wizard-page.ts b/ui/wizard-page.ts new file mode 100644 index 0000000..33787d7 --- /dev/null +++ b/ui/wizard-page.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2013-2019 GenieACS Inc. + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ + +import m, { ClosureComponent, Component } from "mithril"; +import * as notifications from "./notifications"; + +export async function init(): Promise<{}> { + return m.request({ url: "/init" }); +} + +export const component: ClosureComponent = (vnode): Component => { + let options = vnode.attrs; + const selected = new Set(); + for (const [k, v] of Object.entries(options)) if (v) selected.add(k); + + return { + view: () => { + document.title = "Initialization wizard - GenieACS"; + + const checkboxes = [ + "users", + "presets", + "filters", + "device", + "index", + "overview" + ].map(s => { + if (!options[s]) selected.delete(s); + return m("input", { + type: "checkbox", + checked: selected.has(s), + disabled: !options[s], + style: "display: inline; margin-right: 0.5em;", + onclick: e => { + if (e.target.checked) selected.add(s); + else selected.delete(s); + } + }); + }); + + return m(".wizard-dialog", [ + m("h1", "Initialization wizard"), + m( + "p", + "This wizard will seed the database with a minimal initial configuration to serve as a starting point. Select what you want to initialize and click 'ABRACADABRA!'." + ), + m("div", m("label", checkboxes[0], "Users, roles and permissions")), + m("div", m("label", checkboxes[1], "Presets and provisions")), + m( + "div", + m("label", checkboxes[2], "Devices predefined search filters") + ), + m("div", m("label", checkboxes[3], "Device details page")), + m("div", m("label", checkboxes[4], "Devices listing page")), + m("div", m("label", checkboxes[5], "Overview page")), + m( + "button.primary", + { + style: "margin: 10px;", + disabled: selected.size === 0, + onclick: e => { + e.target.disabled = true; + + const opts = {}; + for (const s of selected) opts[s] = true; + + m.request({ + method: "POST", + url: "/init", + data: opts + }) + .then(() => { + setTimeout(() => { + m.request({ url: "/init" }).then(o => { + e.target.disabled = false; + options = o; + notifications.push("success", "Initialization complete", { + "Open Sesame!": () => { + m.route.set("/login"); + window.location.reload(); + } + }); + }); + }, 3000); + if (opts["users"]) { + alert( + "An administrator user has been created for you. Use admin/admin to log in. Don't forget to change the default password." + ); + } + }) + .catch(err => { + notifications.push("error", err.message); + }); + } + }, + "ABRACADABRA!" + ) + ]); + } + }; +};