From 2d2f03b77a4ac2ae336459323c7e846010aa3c2e Mon Sep 17 00:00:00 2001 From: ac-61 Date: Wed, 9 Feb 2022 16:22:57 -0800 Subject: [PATCH] Implement webhooks feature (#146) * First implementation of webhooks * Remove webhooktoken field * Reorganize files and functions * Clean up webhook cards * Add routes for testing webhooks when using development environment * Fix Config subpage scrollbar issue * Fix delete button Co-authored-by: Tariq Soliman --- API/Backend/Config/setup.js | 5 +- API/Backend/Draw/routes/draw.js | 14 + API/Backend/Draw/routes/files.js | 279 ++---------------- API/Backend/Draw/routes/filesutils.js | 263 +++++++++++++++++ API/Backend/Webhooks/models/webhooks.js | 23 ++ .../Webhooks/processes/triggerwebhooks.js | 222 ++++++++++++++ API/Backend/Webhooks/routes/testwebhooks.js | 56 ++++ API/Backend/Webhooks/routes/webhooks.js | 50 ++++ API/Backend/Webhooks/routes/webhookutils.js | 34 +++ API/Backend/Webhooks/setup.js | 24 ++ config/css/config.css | 25 ++ config/css/webhooks.css | 24 ++ config/js/calls.js | 12 + config/js/config.js | 5 + config/js/datasets.js | 106 ++++--- config/js/geodatasets.js | 106 ++++--- config/js/keys.js | 35 ++- config/js/webhooks.js | 275 +++++++++++++++++ views/configure.pug | 4 + 19 files changed, 1202 insertions(+), 360 deletions(-) create mode 100644 API/Backend/Draw/routes/filesutils.js create mode 100644 API/Backend/Webhooks/models/webhooks.js create mode 100644 API/Backend/Webhooks/processes/triggerwebhooks.js create mode 100644 API/Backend/Webhooks/routes/testwebhooks.js create mode 100644 API/Backend/Webhooks/routes/webhooks.js create mode 100644 API/Backend/Webhooks/routes/webhookutils.js create mode 100644 API/Backend/Webhooks/setup.js create mode 100644 config/css/webhooks.css create mode 100644 config/js/webhooks.js diff --git a/API/Backend/Config/setup.js b/API/Backend/Config/setup.js index d1fec458..cc71f7a0 100644 --- a/API/Backend/Config/setup.js +++ b/API/Backend/Config/setup.js @@ -1,4 +1,5 @@ const router = require("./routes/configs"); +const triggerWebhooks = require("../Webhooks/processes/triggerwebhooks.js"); let setup = { //Once the app initializes @@ -33,7 +34,9 @@ let setup = { //Once the server starts onceStarted: (s) => {}, //Once all tables sync - onceSynced: (s) => {}, + onceSynced: (s) => { + triggerWebhooks("getConfiguration", {}); + }, }; module.exports = setup; diff --git a/API/Backend/Draw/routes/draw.js b/API/Backend/Draw/routes/draw.js index 8cde694a..ccdab4c5 100644 --- a/API/Backend/Draw/routes/draw.js +++ b/API/Backend/Draw/routes/draw.js @@ -16,6 +16,7 @@ const { sequelize } = require("../../../connection"); const router = express.Router(); const db = database.db; +const triggerWebhooks = require("../../Webhooks/processes/triggerwebhooks"); router.post("/", function (req, res, next) { res.send("test draw"); @@ -41,6 +42,7 @@ const uniqueAcrossArrays = (arr1, arr2) => { const pushToHistory = ( Table, + res, file_id, feature_id, feature_idRemove, @@ -88,6 +90,10 @@ const pushToHistory = ( Table.create(newHistoryEntry) .then((created) => { successCallback(); + triggerWebhooks("drawFileChange", { + id: file_id, + res, + }); return null; }) .catch((err) => { @@ -239,6 +245,7 @@ const clipOver = function ( if (i >= results.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, @@ -378,6 +385,7 @@ const clipUnder = function ( if (i >= results.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, @@ -566,6 +574,7 @@ const add = function ( } else { pushToHistory( Histories, + res, req.body.file_id, id, null, @@ -733,6 +742,7 @@ const edit = function (req, res, successCallback, failureCallback) { if (req.body.to_history) { pushToHistory( Histories, + res, req.body.file_id, created.id, req.body.feature_id, @@ -844,6 +854,7 @@ router.post("/remove", function (req, res, next) { //Table, file_id, feature_id, feature_idRemove, time, undoToTime, action_index pushToHistory( Histories, + res, req.body.file_id, null, req.body.id, @@ -974,6 +985,7 @@ router.post("/undo", function (req, res, next) { .then((r) => { pushToHistory( Histories, + res, req.body.file_id, null, null, @@ -1112,6 +1124,7 @@ router.post("/merge", function (req, res, next) { if (i >= results.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, @@ -1270,6 +1283,7 @@ router.post("/split", function (req, res, next) { if (i >= r.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, diff --git a/API/Backend/Draw/routes/files.js b/API/Backend/Draw/routes/files.js index c8779b7c..1fff120a 100644 --- a/API/Backend/Draw/routes/files.js +++ b/API/Backend/Draw/routes/files.js @@ -19,6 +19,9 @@ const PublishedTEST = published.PublishedTEST; const PublishedStore = require("../models/publishedstore"); const draw = require("./draw"); +const filesutils = require("./filesutils"); +const getfile = filesutils.getfile; +const triggerWebhooks = require("../../Webhooks/processes/triggerwebhooks"); const router = express.Router(); const db = database.db; @@ -89,257 +92,7 @@ router.post("/getfiles", function (req, res, next) { * published: (optional) get last published version (makes 'time' ignored) * } */ -router.post("/getfile", function (req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - if (req.session.user == "guest" && req.body.quick_published !== "true") { - res.send({ - status: "failure", - message: "Permission denied.", - body: {}, - }); - } - - let published = false; - if (req.body.published === "true") published = true; - if (req.body.quick_published === "true") { - sequelize - .query( - "SELECT " + - "id, intent, parent, children, level, properties, ST_AsGeoJSON(geom)" + - " " + - "FROM " + - (req.body.test === "true" ? "publisheds_test" : "publisheds") + - "" + - (req.body.intent && req.body.intent.length > 0 - ? req.body.intent === "all" - ? " WHERE intent IN ('polygon', 'line', 'point', 'text', 'arrow')" - : " WHERE intent=:intent" - : ""), - { - replacements: { - intent: req.body.intent || "", - }, - } - ) - .spread((results) => { - let geojson = { type: "FeatureCollection", features: [] }; - for (let i = 0; i < results.length; i++) { - let properties = results[i].properties; - let feature = {}; - properties._ = { - id: results[i].id, - intent: results[i].intent, - parent: results[i].parent, - children: results[i].children, - level: results[i].level, - }; - feature.type = "Feature"; - feature.properties = properties; - feature.geometry = JSON.parse(results[i].st_asgeojson); - geojson.features.push(feature); - } - - //Sort features by level - geojson.features.sort((a, b) => - a.properties._.level > b.properties._.level - ? 1 - : b.properties._.level > a.properties._.level - ? -1 - : 0 - ); - - if (req.body.test !== "true") { - //Sort features by geometry type - geojson.features.sort((a, b) => { - if (a.geometry.type == "Point" && b.geometry.type == "Polygon") - return 1; - if (a.geometry.type == "LineString" && b.geometry.type == "Polygon") - return 1; - if (a.geometry.type == "Polygon" && b.geometry.type == "LineString") - return -1; - if (a.geometry.type == "Polygon" && b.geometry.type == "Point") - return -1; - if (a.geometry.type == "LineString" && b.geometry.type == "Point") - return -1; - if (a.geometry.type == b.geometry.type) return 0; - return 0; - }); - } - - res.send({ - status: "success", - message: "Successfully got file.", - body: geojson, - }); - }); - } else { - let idArray = false; - req.body.id = JSON.parse(req.body.id); - if (typeof req.body.id !== "number") idArray = true; - - let atThisTime = published - ? Math.floor(Date.now()) - : req.body.time || Math.floor(Date.now()); - - Table.findAll({ - where: { - id: req.body.id, - //file_owner is req.user or public is '1' - [Sequelize.Op.or]: { - file_owner: req.user, - public: "1", - }, - }, - }) - .then((file) => { - if (!file) { - res.send({ - status: "failure", - message: "Failed to access file.", - body: {}, - }); - } else { - sequelize - .query( - "SELECT history" + - " " + - "FROM file_histories" + - (req.body.test === "true" ? "_tests" : "") + - " " + - "WHERE" + - " " + - (idArray ? "file_id IN (:id)" : "file_id=:id") + - " " + - "AND time<=:time" + - " " + - (published ? "AND action_index=4 " : "") + - "ORDER BY time DESC" + - " " + - "FETCH first " + - (published ? req.body.id.length : "1") + - " rows only", - { - replacements: { - id: req.body.id, - time: atThisTime, - }, - } - ) - .spread((results) => { - let bestHistory = []; - for (let i = 0; i < results.length; i++) { - bestHistory = bestHistory.concat(results[i].history); - } - bestHistory = bestHistory.join(","); - bestHistory = bestHistory || "NULL"; - - //Find best history - sequelize - .query( - "SELECT " + - "id, file_id, level, intent, properties, ST_AsGeoJSON(geom)" + - " " + - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " " + - "WHERE" + - " " + - (idArray ? "file_id IN (:id)" : "file_id=:id") + - " " + - "AND id IN (" + - bestHistory + - ")", - { - replacements: { - id: req.body.id, - }, - } - ) - .spread((results) => { - let geojson = { type: "FeatureCollection", features: [] }; - for (let i = 0; i < results.length; i++) { - let properties = JSON.parse(results[i].properties); - let feature = {}; - properties._ = { - id: results[i].id, - file_id: results[i].file_id, - level: results[i].level, - intent: results[i].intent, - }; - feature.type = "Feature"; - feature.properties = properties; - feature.geometry = JSON.parse(results[i].st_asgeojson); - geojson.features.push(feature); - } - - //Sort features by level - geojson.features.sort((a, b) => - a.properties._.level > b.properties._.level - ? 1 - : b.properties._.level > a.properties._.level - ? -1 - : 0 - ); - - if (req.body.test !== "true") { - //Sort features by geometry type - geojson.features.sort((a, b) => { - if ( - a.geometry.type == "Point" && - b.geometry.type == "Polygon" - ) - return 1; - if ( - a.geometry.type == "LineString" && - b.geometry.type == "Polygon" - ) - return 1; - if ( - a.geometry.type == "Polygon" && - b.geometry.type == "LineString" - ) - return -1; - if ( - a.geometry.type == "Polygon" && - b.geometry.type == "Point" - ) - return -1; - if ( - a.geometry.type == "LineString" && - b.geometry.type == "Point" - ) - return -1; - if (a.geometry.type == b.geometry.type) return 0; - return 0; - }); - } - - res.send({ - status: "success", - message: "Successfully got file.", - body: { - file: file, - geojson: geojson, - }, - }); - }); - }); - } - - return null; - }) - .catch((err) => { - logger("error", "Failed to get file.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to get file.", - body: {}, - }); - }); - } -}); +router.post("/getfile", getfile); /** * Makes a new file @@ -480,6 +233,10 @@ router.post("/make", function (req, res, next) { message: "Successfully made a new file from geojson.", body: {}, }); + triggerWebhooks("drawFileAdd", { + id: created.id, + res, + }); return null; }) .catch((err) => { @@ -522,6 +279,10 @@ router.post("/make", function (req, res, next) { message: "Successfully made a new file.", body: {}, }); + triggerWebhooks("drawFileAdd", { + id: created.id, + res, + }); } return null; @@ -562,6 +323,10 @@ router.post("/remove", function (req, res, next) { message: "File removed.", body: {}, }); + triggerWebhooks("drawFileDelete", { + id: req.body.id, + res, + }); return null; }) @@ -669,6 +434,10 @@ router.post("/change", function (req, res, next) { message: "File edited.", body: {}, }); + triggerWebhooks("drawFileChange", { + id: req.body.id, + res, + }); return null; }) @@ -1482,6 +1251,10 @@ router.post("/publish", function (req, res, next) { message: "Published.", body: {}, }); + triggerWebhooks("drawFileChange", { + id: files[f].dataValues.id, + res, + }); } }, (err) => { @@ -1539,6 +1312,10 @@ router.post("/publish", function (req, res, next) { Table.create(newHistoryEntry) .then((created) => { successCallback(newHistoryEntry); + triggerWebhooks("drawFileAdd", { + id: file_id, + res, + }); return null; }) .catch((err) => { diff --git a/API/Backend/Draw/routes/filesutils.js b/API/Backend/Draw/routes/filesutils.js new file mode 100644 index 00000000..8a966ed8 --- /dev/null +++ b/API/Backend/Draw/routes/filesutils.js @@ -0,0 +1,263 @@ +const logger = require("../../../logger"); +const Sequelize = require("sequelize"); +const { sequelize } = require("../../../connection"); +const fhistories = require("../models/filehistories"); +const Filehistories = fhistories.Filehistories; +const FilehistoriesTEST = fhistories.FilehistoriesTEST; +const ufiles = require("../models/userfiles"); +const Userfiles = ufiles.Userfiles; +const UserfilesTEST = ufiles.UserfilesTEST; + +function getfile(req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + if (req.session.user == "guest" && req.body.quick_published !== "true") { + res.send({ + status: "failure", + message: "Permission denied.", + body: {}, + }); + } + + let published = false; + if (req.body.published === "true") published = true; + if (req.body.quick_published === "true") { + sequelize + .query( + "SELECT " + + "id, intent, parent, children, level, properties, ST_AsGeoJSON(geom)" + + " " + + "FROM " + + (req.body.test === "true" ? "publisheds_test" : "publisheds") + + "" + + (req.body.intent && req.body.intent.length > 0 + ? req.body.intent === "all" + ? " WHERE intent IN ('polygon', 'line', 'point', 'text', 'arrow')" + : " WHERE intent=:intent" + : ""), + { + replacements: { + intent: req.body.intent || "", + }, + } + ) + .spread((results) => { + let geojson = { type: "FeatureCollection", features: [] }; + for (let i = 0; i < results.length; i++) { + let properties = results[i].properties; + let feature = {}; + properties._ = { + id: results[i].id, + intent: results[i].intent, + parent: results[i].parent, + children: results[i].children, + level: results[i].level, + }; + feature.type = "Feature"; + feature.properties = properties; + feature.geometry = JSON.parse(results[i].st_asgeojson); + geojson.features.push(feature); + } + + //Sort features by level + geojson.features.sort((a, b) => + a.properties._.level > b.properties._.level + ? 1 + : b.properties._.level > a.properties._.level + ? -1 + : 0 + ); + + if (req.body.test !== "true") { + //Sort features by geometry type + geojson.features.sort((a, b) => { + if (a.geometry.type == "Point" && b.geometry.type == "Polygon") + return 1; + if (a.geometry.type == "LineString" && b.geometry.type == "Polygon") + return 1; + if (a.geometry.type == "Polygon" && b.geometry.type == "LineString") + return -1; + if (a.geometry.type == "Polygon" && b.geometry.type == "Point") + return -1; + if (a.geometry.type == "LineString" && b.geometry.type == "Point") + return -1; + if (a.geometry.type == b.geometry.type) return 0; + return 0; + }); + } + + res.send({ + status: "success", + message: "Successfully got file.", + body: geojson, + }); + }); + } else { + let idArray = false; + req.body.id = JSON.parse(req.body.id); + if (typeof req.body.id !== "number") idArray = true; + + let atThisTime = published + ? Math.floor(Date.now()) + : req.body.time || Math.floor(Date.now()); + + Table.findAll({ + where: { + id: req.body.id, + //file_owner is req.user or public is '1' + [Sequelize.Op.or]: { + file_owner: req.user, + public: "1", + }, + }, + }) + .then((file) => { + if (!file) { + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + } else { + sequelize + .query( + "SELECT history" + + " " + + "FROM file_histories" + + (req.body.test === "true" ? "_tests" : "") + + " " + + "WHERE" + + " " + + (idArray ? "file_id IN (:id)" : "file_id=:id") + + " " + + "AND time<=:time" + + " " + + (published ? "AND action_index=4 " : "") + + "ORDER BY time DESC" + + " " + + "FETCH first " + + (published ? req.body.id.length : "1") + + " rows only", + { + replacements: { + id: req.body.id, + time: atThisTime, + }, + } + ) + .spread((results) => { + let bestHistory = []; + for (let i = 0; i < results.length; i++) { + bestHistory = bestHistory.concat(results[i].history); + } + bestHistory = bestHistory.join(","); + bestHistory = bestHistory || "NULL"; + + //Find best history + sequelize + .query( + "SELECT " + + "id, file_id, level, intent, properties, ST_AsGeoJSON(geom)" + + " " + + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " " + + "WHERE" + + " " + + (idArray ? "file_id IN (:id)" : "file_id=:id") + + " " + + "AND id IN (" + + bestHistory + + ")", + { + replacements: { + id: req.body.id, + }, + } + ) + .spread((results) => { + let geojson = { type: "FeatureCollection", features: [] }; + for (let i = 0; i < results.length; i++) { + let properties = JSON.parse(results[i].properties); + let feature = {}; + properties._ = { + id: results[i].id, + file_id: results[i].file_id, + level: results[i].level, + intent: results[i].intent, + }; + feature.type = "Feature"; + feature.properties = properties; + feature.geometry = JSON.parse(results[i].st_asgeojson); + geojson.features.push(feature); + } + + //Sort features by level + geojson.features.sort((a, b) => + a.properties._.level > b.properties._.level + ? 1 + : b.properties._.level > a.properties._.level + ? -1 + : 0 + ); + + if (req.body.test !== "true") { + //Sort features by geometry type + geojson.features.sort((a, b) => { + if ( + a.geometry.type == "Point" && + b.geometry.type == "Polygon" + ) + return 1; + if ( + a.geometry.type == "LineString" && + b.geometry.type == "Polygon" + ) + return 1; + if ( + a.geometry.type == "Polygon" && + b.geometry.type == "LineString" + ) + return -1; + if ( + a.geometry.type == "Polygon" && + b.geometry.type == "Point" + ) + return -1; + if ( + a.geometry.type == "LineString" && + b.geometry.type == "Point" + ) + return -1; + if (a.geometry.type == b.geometry.type) return 0; + return 0; + }); + } + + res.send({ + status: "success", + message: "Successfully got file.", + body: { + file: file, + geojson: geojson, + }, + }); + }); + }); + } + + return null; + }) + .catch((err) => { + logger("error", "Failed to get file.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to get file.", + body: {}, + }); + }); + } +} + +module.exports = { getfile }; diff --git a/API/Backend/Webhooks/models/webhooks.js b/API/Backend/Webhooks/models/webhooks.js new file mode 100644 index 00000000..4a4c406f --- /dev/null +++ b/API/Backend/Webhooks/models/webhooks.js @@ -0,0 +1,23 @@ +/*********************************************************** + * Loading all required dependencies, libraries and packages + **********************************************************/ +const Sequelize = require("sequelize"); +const { sequelize } = require("../../../connection"); + +// setup Webhooks model and its fields. +var Webhooks = sequelize.define( + "webhooks", + { + config: { + type: Sequelize.JSON, + allowNull: true, + defaultValue: {} + }, + }, + { + timestamps: true + } +); + +// export Webhooks model for use in other files. +module.exports = Webhooks; diff --git a/API/Backend/Webhooks/processes/triggerwebhooks.js b/API/Backend/Webhooks/processes/triggerwebhooks.js new file mode 100644 index 00000000..8b1ed548 --- /dev/null +++ b/API/Backend/Webhooks/processes/triggerwebhooks.js @@ -0,0 +1,222 @@ +const logger = require("../../../logger"); +const fetch = require("node-fetch"); + +const filesutils = require("../../Draw/routes/filesutils.js"); +const getfile = filesutils.getfile; + +const webhookutils = require("../../Webhooks/routes/webhookutils.js"); +const webhookEntries = webhookutils.entries; + +const INJECT_REGEX = /{(.*?)}/; + +// Save the webhook config to local memory +var webhooksConfig; + +function getWebhooks() { + var res = {}; + res.send = function (payload) { + if (payload.status == "success") { + if (payload?.body?.entries && payload.body.entries.length > 0) { + var config = JSON.parse(payload.body.entries[0].config); + webhooksConfig = config.webhooks; + } + } else { + logger( + "error", + "Unable to get webhook entries", + "TriggerWebhooks", + null, + "Unable to get webhook entries" + ); + } + }; + + webhookEntries({}, res); +} + +function triggerWebhooks(action, payload) { + if (action === "getConfiguration") { + getWebhooks(); + } + + if (!webhooksConfig) { + return; + } + + webhooksConfig.forEach((wh) => { + switch (wh.action) { + case "DrawFileChange": + if (action === "drawFileChange") { + drawFileUpdate(wh, payload); + } + break; + case "DrawFileAdd": + if (action === "drawFileAdd") { + drawFileUpdate(wh, payload); + } + break; + case "DrawFileDelete": + if (action === "drawFileDelete") { + drawFileDelete(wh, payload); + } + break; + } + }); +} + +function drawFileUpdate(webhook, payload) { + var file_id = payload.id; + var data = { + body: { + id: payload.id, + quick_published: false, + published: false, + }, + user: payload.res.req.user, + session: { + user: payload.res.req.user, + }, + }; + + var response = {}; + response.send = function (res) { + var webhookHeader = JSON.parse(webhook.header); + var webhookBody = JSON.parse(webhook.body); + var file_name = res.body?.file[0]?.file_name || null; + var geojson = res.body.geojson; + + const injectableVariables = { + file_id, + file_name, + geojson, + }; + + // Build the body + buildBody(webhookBody, injectableVariables); + + // Build the url + var url = buildUrl(webhook.url, injectableVariables); + + // Push to the remote webhook + pushToRemote(url, webhook.type, webhookHeader, webhookBody); + }; + + getfile(data, response); +} + +function buildBody(webhookBody, injectableVariables) { + // Fill in the body + for (var i in webhookBody) { + var match = INJECT_REGEX.exec(webhookBody[i]); + // Match for curly braces. If the value contains no curly braces, assume the value is hardcoded so leave the value as is + if (match) { + var variable = match[1]; + if (!injectableVariables[variable]) { + logger( + "error", + "The variable '" + variable + "' is not an injectable variable", + "Webhooks", + null, + "The variable '" + variable + "' is not an injectable variable" + ); + } + webhookBody[i] = injectableVariables[variable]; + } + } +} + +function drawFileDelete(webhook, payload) { + var file_id = payload.id; + var data = { + body: { + id: payload.id, + quick_published: false, + published: false, + }, + user: payload.res.req.user, + session: { + user: payload.res.req.user, + }, + }; + + var response = {}; + response.send = function (res) { + var webhookHeader = JSON.parse(webhook.header); + var geojson = res.body.geojson; + var file_name = res.body?.file[0]?.file_name || null; + + const injectableVariables = { + file_id, + file_name, + geojson, + }; + + // Build the url + var url = buildUrl(webhook.url, injectableVariables); + + // Push to the remote webhook + pushToRemote(url, webhook.type, webhookHeader, {}); + }; + + getfile(data, response); +} + +function buildUrl(url, injectableVariables) { + var updatedUrl = url; + var match; + while (null !== (match = INJECT_REGEX.exec(updatedUrl))) { + var variable = match[1]; + if (!injectableVariables[variable]) { + logger( + "error", + "The variable '" + variable + "' is not an injectable variable", + "Webhooks", + null, + "The variable '" + variable + "' is not an injectable variable" + ); + } + + // Stringify if the injectable variable is an object + var newVariable = injectableVariables[variable]; + if (typeof newVariable === "object" && newVariable !== null) { + newVariable = JSON.stringify(newVariable); + } + + updatedUrl = updatedUrl.replace(match[0], newVariable); + } + return updatedUrl; +} + +function pushToRemote(url, type, header, body) { + fetch(url, { + method: type, + headers: header, + body: JSON.stringify(body), + }) + .then((res) => { + if (!res.ok) { + return res.text().then((text) => { + throw new Error(text); + }); + } else { + return res.json(); + } + }) + .then((json) => { + if (json.status == "success") { + logger("info", "Successful webhook call to " + url, "TriggerWebhooks"); + } + }) + .catch(function (err) { + logger( + "error", + "Failed webhook call to " + url, + "TriggerWebhooks", + null, + err + ); + return null; + }); +} + +module.exports = triggerWebhooks; diff --git a/API/Backend/Webhooks/routes/testwebhooks.js b/API/Backend/Webhooks/routes/testwebhooks.js new file mode 100644 index 00000000..e3d82523 --- /dev/null +++ b/API/Backend/Webhooks/routes/testwebhooks.js @@ -0,0 +1,56 @@ +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const express = require("express"); +const router = express.Router(); + +const logger = require("../../../logger"); + +router.get("/test", function (req, res, next) { + logger("success", "Called /testwebhooks/test API", req.originalUrl, req); + res.send("pong"); +}); + +router.post("/test_webhook_post/", function (req, res, next) { + logger("success", "Called /testwebhooks/test_webhook_post API", req.originalUrl, req); + logger("info", "Body of request\n" + JSON.stringify(req.body, null, 4), req.originalUrl, req); + res.send({ + status: "success", + message: "Received data to test_webhook_post API!", + body: { + input: req.body, + } + }); +}); + +router.delete("/test_webhook_del/:id", function (req, res, next) { + logger("success", "Called /testwebhooks/test_webhook_del API", req.originalUrl, req); + logger("info", "Body of request\n" + JSON.stringify(req.body, null, 4), req.originalUrl, req); + + res.send({ + status: "success", + message: "Received data to test_webhook_del API!", + body: { + params: req.params, + input: req.body, + } + }); + +}); + +router.patch("/test_webhook_patch/:id", function (req, res, next) { + logger("success", "Called /testwebhooks/test_webhook_patch API", req.originalUrl, req); + logger("info", "Body of request\n" + JSON.stringify(req.body, null, 4), req.originalUrl, req); + + res.send({ + status: "success", + message: "Received data to test_webhook_patch API!", + body: { + params: req.params, + input: req.body, + } + }); +}); + +module.exports = router; diff --git a/API/Backend/Webhooks/routes/webhooks.js b/API/Backend/Webhooks/routes/webhooks.js new file mode 100644 index 00000000..109cb36e --- /dev/null +++ b/API/Backend/Webhooks/routes/webhooks.js @@ -0,0 +1,50 @@ +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const express = require("express"); +const router = express.Router(); + +const logger = require("../../../logger"); +const Webhooks = require("../models/webhooks"); + +const webhookutils = require("./webhookutils.js"); +const entries = webhookutils.entries; + +const triggerWebhooks = require("../processes/triggerwebhooks.js"); + +router.post("/save", function (req, res, next) { + let webhookConfig = { + config: req.body.config, + }; + + Webhooks.create(webhookConfig) + .then((created) => { + res.send({ + status: "success", + message: "Successfully saved webhooks config.", + }); + }) + .catch((err) => { + res.send({ + status: "failure", + message: "Failed to save webhooks config!", + body: { err }, + }); + }); +}); + +router.get("/entries", entries); + +router.post("/config", function (req, res, next) { + logger("success", "Called /webhooks/config API", req.originalUrl, req); + + triggerWebhooks("getConfiguration", {}); + // FIXME how do we check the above function ran + res.send({ + status: "success", + message: "Successfully updated webhooks config.", + }); +}); + +module.exports = { router }; diff --git a/API/Backend/Webhooks/routes/webhookutils.js b/API/Backend/Webhooks/routes/webhookutils.js new file mode 100644 index 00000000..53d401bb --- /dev/null +++ b/API/Backend/Webhooks/routes/webhookutils.js @@ -0,0 +1,34 @@ +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const logger = require("../../../logger"); +const Webhooks = require("../models/webhooks"); + +function entries(req, res, next) { + logger("success", "Called /webhooks/entries API", req.originalUrl, req); + Webhooks.findAll({ + order: [["updatedAt", "DESC"]], + }) + .then((sets) => { + if (sets && sets.length > 0) { + let entries = []; + for (let i = 0; i < sets.length; i++) { + entries.push({ config: sets[i].config, updated: sets[i].updatedAt }); + } + + res.send({ + status: "success", + body: { entries: entries }, + }); + } + }) + .catch((err) => { + logger("error", "Failure finding webhooks.", req.originalUrl, req, err); + res.send({ + status: "failure", + }); + }); +} + +module.exports = { entries }; diff --git a/API/Backend/Webhooks/setup.js b/API/Backend/Webhooks/setup.js new file mode 100644 index 00000000..54a8a3c0 --- /dev/null +++ b/API/Backend/Webhooks/setup.js @@ -0,0 +1,24 @@ +const routeWebhooks = require("./routes/webhooks"); +const routerWebhooks = routeWebhooks.router; +const fetch = require("node-fetch"); +const routerTestWebhooks = require("./routes/testwebhooks"); + +let setup = { + //Once the app initializes + onceInit: (s) => { + s.app.use("/API/webhooks", s.checkHeadersCodeInjection, routerWebhooks); + if (process.env.NODE_ENV === "development") { + s.app.use( + "/API/testwebhooks", + s.checkHeadersCodeInjection, + routerTestWebhooks + ); + } + }, + //Once the server starts + onceStarted: (s) => {}, + //Once all tables sync + onceSynced: (s) => {}, +}; + +module.exports = setup; diff --git a/config/css/config.css b/config/css/config.css index b6e2db74..37134054 100644 --- a/config/css/config.css +++ b/config/css/config.css @@ -113,6 +113,20 @@ body { opacity: 0; transition: opacity 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); } +.container_webhooks { + margin-left: 220px; + width: calc(100% - 220px) !important; + height: 100vh; + max-width: unset !important; + background: white; + position: absolute; + top: 0; + left: 0; + z-index: 200; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); +} .container { margin-left: 220px; width: 100% !important; @@ -265,6 +279,17 @@ body { #manage_geodatasets:hover { color: #5ea1ed; } +#manage_webhooks { + height: 30px; + width: calc(100% - 16px); + line-height: 30px; + margin: 4px 8px; + font-size: 12px; + color: #ddd; +} +#manage_webhooks:hover { + color: #5ea1ed; +} textarea { resize: vertical; diff --git a/config/css/webhooks.css b/config/css/webhooks.css new file mode 100644 index 00000000..7cda5810 --- /dev/null +++ b/config/css/webhooks.css @@ -0,0 +1,24 @@ +.webhooks { + padding: 20px; + font-family: "Roboto", sans-serif; + display: flex; + flex-flow: column; +} +.webhooks .title { + text-align: center; + font-size: 100px; + color: #ddd; +} +.webhooks .row { + margin-left: 2.5%; + margin-right: 2.5%; +} +.webhooks .CodeMirror { + margin: 0; +} +.webhooks .inject-label{ + margin-top: 0%; +} +.webhooks .card:nth-of-type(even) { + background: rgba(0, 0, 0, 0.03); +} diff --git a/config/js/calls.js b/config/js/calls.js index e52192e7..a4bed70f 100644 --- a/config/js/calls.js +++ b/config/js/calls.js @@ -68,4 +68,16 @@ let calls = { type: "POST", url: "api/longtermtoken/generate", }, + webhooks_save: { + type: "POST", + url: "api/webhooks/save", + }, + webhooks_entries: { + type: "GET", + url: "api/webhooks/entries", + }, + webhooks_config: { + type: "POST", + url: "api/webhooks/config", + }, }; diff --git a/config/js/config.js b/config/js/config.js index 59e7139c..1b81e827 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -96,6 +96,10 @@ function initialize() { $("#manage_geodatasets").on("click", function () { Geodatasets.make(); }); + //Initial manage webhooks + $("#manage_webhooks").on("click", function () { + Webhooks.make(); + }); $.ajax({ type: calls.missions.type, @@ -216,6 +220,7 @@ function initialize() { Keys.destroy(); Datasets.destroy(); Geodatasets.destroy(); + Webhooks.destroy(); $("#missions li").removeClass("active"); $(this).addClass("active"); diff --git a/config/js/datasets.js b/config/js/datasets.js index 31f63ac5..feffdee8 100644 --- a/config/js/datasets.js +++ b/config/js/datasets.js @@ -1,6 +1,6 @@ var Datasets = { csv: null, - init: function() { + init: function () { // prettier-ignore var markup = [ "
", @@ -33,24 +33,24 @@ var Datasets = { Datasets.refreshNames(); //Upload - $(".container_datasets #datasetUploadButton > input").on("change", function( - evt - ) { - var files = evt.target.files; // FileList object + $(".container_datasets #datasetUploadButton > input").on( + "change", + function (evt) { + var files = evt.target.files; // FileList object - // use the 1st file from the list - var f = files[0]; - var ext = Datasets.getExtension(f.name).toLowerCase(); + // use the 1st file from the list + var f = files[0]; + var ext = Datasets.getExtension(f.name).toLowerCase(); - $(".container_datasets .datasetName input").val( - f.name - .split("." + ext)[0] - .replace(/[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "") - ); + $(".container_datasets .datasetName input").val( + f.name + .split("." + ext)[0] + .replace(/[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "") + ); - switch (ext) { - case "csv": - /* + switch (ext) { + case "csv": + /* var reader = new FileReader(); // Closure to capture the file information. reader.onload = (function(file) { @@ -62,16 +62,19 @@ var Datasets = { reader.readAsText(f); */ - Datasets.f = f; - $(".container_datasets .datasetUploadFilename").text(Datasets.f.name); - break; - default: - alert("Only .csv files may be uploaded."); + Datasets.f = f; + $(".container_datasets .datasetUploadFilename").text( + Datasets.f.name + ); + break; + default: + alert("Only .csv files may be uploaded."); + } } - }); + ); //Re/create - $(".container_datasets .datasetRecreate > a").on("click", function(evt) { + $(".container_datasets .datasetRecreate > a").on("click", function (evt) { let name = $(".datasetName input").val(); if (Datasets.f == null) { alert("Please upload a .csv file."); @@ -90,7 +93,7 @@ var Datasets = { let cursorSum = 0; let cursorStep = null; Papa.parse(Datasets.f, { - step: function(row, parser) { + step: function (row, parser) { if (firstStep) { header = row.data; firstStep = false; @@ -117,22 +120,22 @@ var Datasets = { name: name, csv: JSON.stringify(currentRows), header: header, - mode: first ? "full" : "append" + mode: first ? "full" : "append", }, - success: function(data) { + success: function (data) { first = false; currentRows = []; parser.resume(); }, - error: function(err) { + error: function (err) { currentRows = []; console.log(err); - } + }, }); } } }, - complete: function() { + complete: function () { if (currentRows.length > 0) { $.ajax({ type: calls.datasets_recreate.type, @@ -141,18 +144,18 @@ var Datasets = { name: name, csv: JSON.stringify(currentRows), header: header, - mode: "append" + mode: "append", }, - success: function(data) { + success: function (data) { Datasets.refreshNames(); $(".datasetRecreate a") .css("pointer-events", "inherit") .text("Re/create"); }, - error: function(err) { + error: function (err) { currentRows = []; console.log(err); - } + }, }); } else { Datasets.refreshNames(); @@ -160,39 +163,48 @@ var Datasets = { .css("pointer-events", "inherit") .text("Re/create"); } - } + }, }); }); }, - make: function() { + make: function () { $(".container_datasets").css({ opacity: 1, - pointerEvents: "inherit" + pointerEvents: "inherit", + display: "block", }); Keys.destroy(); Geodatasets.destroy(); + Webhooks.destroy(); $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); }, - destroy: function() { + destroy: function () { $(".container_datasets").css({ opacity: 0, - pointerEvents: "none" + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", }); }, - getExtension: function(string) { + getExtension: function (string) { var ex = /(?:\.([^.]+))?$/.exec(string)[1]; return ex || ""; }, - sortArrayOfObjectsByKeyValue: function(arr, key, ascending, stringify) { + sortArrayOfObjectsByKeyValue: function (arr, key, ascending, stringify) { if (arr.constructor !== Array) return arr; const side = ascending ? 1 : -1; - let compareKey = function(a, b) { + let compareKey = function (a, b) { if (a[key] < b[key]) return -1 * side; if (a[key] > b[key]) return side; return 0; }; if (stringify) { - compareKey = function(a, b) { + compareKey = function (a, b) { if (JSON.stringify(a[key]) < JSON.stringify(b[key])) return -1 * side; if (JSON.stringify(a[key]) > JSON.stringify(b[key])) return side; return 0; @@ -201,12 +213,12 @@ var Datasets = { return arr.sort(compareKey); }, - refreshNames: function() { + refreshNames: function () { $.ajax({ type: calls.datasets_entries.type, url: calls.datasets_entries.url, data: {}, - success: function(data) { + success: function (data) { if (data.status == "success") { $(".container_datasets .existing ul").html(""); let entries = Datasets.sortArrayOfObjectsByKeyValue( @@ -223,11 +235,11 @@ var Datasets = { "
" ); } - } + }, }); - } + }, }; -$(document).ready(function() { +$(document).ready(function () { Datasets.init(); }); diff --git a/config/js/geodatasets.js b/config/js/geodatasets.js index 219065be..01a5f1d0 100644 --- a/config/js/geodatasets.js +++ b/config/js/geodatasets.js @@ -1,6 +1,6 @@ var Geodatasets = { geojson: null, - init: function() { + init: function () { // prettier-ignore var markup = [ "
", @@ -35,7 +35,7 @@ var Geodatasets = { //Upload $(".container_geodatasets #geodatasetUploadButton > input").on( "change", - function(evt) { + function (evt) { var files = evt.target.files; // FileList object // use the 1st file from the list @@ -53,8 +53,8 @@ var Geodatasets = { case "geojson": var reader = new FileReader(); // Closure to capture the file information. - reader.onload = (function(file) { - return function(e) { + reader.onload = (function (file) { + return function (e) { $(".container_geodatasets .geodatasetUploadFilename").text( file.name ); @@ -71,68 +71,78 @@ var Geodatasets = { ); //Re/create - $(".container_geodatasets .geodatasetRecreate > a").on("click", function( - evt - ) { - let name = $(".geodatasetName input").val(); - if (Geodatasets.geojson == null) { - alert("Please upload a .geojson file."); - return; - } - if (name == null) { - alert("Please enter a name."); - return; - } - - $.ajax({ - type: calls.geodatasets_recreate.type, - url: calls.geodatasets_recreate.url, - data: { - name: name, - geojson: Geodatasets.geojson - }, - success: function(data) { - Geodatasets.refreshNames(); - $.ajax({ - type: calls.geodatasets_get.type, - url: calls.geodatasets_get.url + "?layer=" + name, - success: function(data) { - console.log(data); - } - }); + $(".container_geodatasets .geodatasetRecreate > a").on( + "click", + function (evt) { + let name = $(".geodatasetName input").val(); + if (Geodatasets.geojson == null) { + alert("Please upload a .geojson file."); + return; } - }); - }); + if (name == null) { + alert("Please enter a name."); + return; + } + + $.ajax({ + type: calls.geodatasets_recreate.type, + url: calls.geodatasets_recreate.url, + data: { + name: name, + geojson: Geodatasets.geojson, + }, + success: function (data) { + Geodatasets.refreshNames(); + $.ajax({ + type: calls.geodatasets_get.type, + url: calls.geodatasets_get.url + "?layer=" + name, + success: function (data) { + console.log(data); + }, + }); + }, + }); + } + ); }, - make: function() { + make: function () { $(".container_geodatasets").css({ opacity: 1, - pointerEvents: "inherit" + pointerEvents: "inherit", + display: "block", }); Keys.destroy(); Datasets.destroy(); + Webhooks.destroy(); $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); }, - destroy: function() { + destroy: function () { $(".container_geodatasets").css({ opacity: 0, - pointerEvents: "none" + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", }); }, - getExtension: function(string) { + getExtension: function (string) { var ex = /(?:\.([^.]+))?$/.exec(string)[1]; return ex || ""; }, - sortArrayOfObjectsByKeyValue: function(arr, key, ascending, stringify) { + sortArrayOfObjectsByKeyValue: function (arr, key, ascending, stringify) { if (arr.constructor !== Array) return arr; const side = ascending ? 1 : -1; - let compareKey = function(a, b) { + let compareKey = function (a, b) { if (a[key] < b[key]) return -1 * side; if (a[key] > b[key]) return side; return 0; }; if (stringify) { - compareKey = function(a, b) { + compareKey = function (a, b) { if (JSON.stringify(a[key]) < JSON.stringify(b[key])) return -1 * side; if (JSON.stringify(a[key]) > JSON.stringify(b[key])) return side; return 0; @@ -141,12 +151,12 @@ var Geodatasets = { return arr.sort(compareKey); }, - refreshNames: function() { + refreshNames: function () { $.ajax({ type: calls.geodatasets_entries.type, url: calls.geodatasets_entries.url, data: {}, - success: function(data) { + success: function (data) { if (data.status == "success") { $(".container_geodatasets .existing ul").html(""); let entries = Geodatasets.sortArrayOfObjectsByKeyValue( @@ -163,11 +173,11 @@ var Geodatasets = { "
" ); } - } + }, }); - } + }, }; -$(document).ready(function() { +$(document).ready(function () { Geodatasets.init(); }); diff --git a/config/js/keys.js b/config/js/keys.js index 61dad6d1..7641abcb 100644 --- a/config/js/keys.js +++ b/config/js/keys.js @@ -1,6 +1,6 @@ var Keys = { token: null, - init: function() { + init: function () { // prettier-ignore var markup = [ "
", @@ -47,44 +47,53 @@ var Keys = { $(".keys select").material_select(); - $(".keys_generate").on("click", function() { + $(".keys_generate").on("click", function () { $.ajax({ type: calls.longtermtoken_generate.type, url: calls.longtermtoken_generate.url, data: { - period: $("select.keys_generation_period").val() + period: $("select.keys_generation_period").val(), }, - success: function(data) { + success: function (data) { if (data.status === "success") { Keys.token = data.body.token; $("#keys_token").text(Keys.token); $("#keys_examples span").text(Keys.token); } }, - error: function(err) { + error: function (err) { Keys.token = null; console.log(err); - } + }, }); }); - $("#keys_token_copy").on("click", function() { + $("#keys_token_copy").on("click", function () { Keys.copyToClipboard(Keys.token); }); }, - make: function() { + make: function () { $(".container_keys").css({ opacity: 1, - pointerEvents: "inherit" + pointerEvents: "inherit", + display: "block", }); Geodatasets.destroy(); Datasets.destroy(); + Webhooks.destroy(); $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); }, - destroy: function() { + destroy: function () { $(".container_keys").css({ opacity: 0, - pointerEvents: "none" + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", }); }, copyToClipboard(text) { @@ -106,9 +115,9 @@ var Keys = { document.getSelection().removeAllRanges(); // Unselect everything on the HTML document document.getSelection().addRange(selected); // Restore the original selection } - } + }, }; -$(document).ready(function() { +$(document).ready(function () { Keys.init(); }); diff --git a/config/js/webhooks.js b/config/js/webhooks.js new file mode 100644 index 00000000..01d12855 --- /dev/null +++ b/config/js/webhooks.js @@ -0,0 +1,275 @@ +var webhooksCounter = 0; +var cardEditors = {}; + +var Webhooks = { + geojson: null, + init: function () { + // prettier-ignore + var markup = [ + "
", + "
Webhooks
", + "
", + "
", + "Add Webhook", + "
", + "", + + "
" + ].join('\n'); + + $(".container_webhooks").html(markup); + + $(".webhooks select").material_select(); + + //Add webhook button + $("#addNewWebhook").on("click", function () { + makeWebhookCard(); + refreshWebhooks(); + }); + + //Save changes button + $("#saveWebhookChanges").on("click", saveWebhookChanges); + + Webhooks.refreshNames(); + }, + make: function () { + $(".container_webhooks").css({ + opacity: 1, + pointerEvents: "inherit", + display: "block", + }); + Keys.destroy(); + Datasets.destroy(); + Geodatasets.destroy(); + $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); + }, + destroy: function () { + $(".container_webhooks").css({ + opacity: 0, + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", + }); + }, + refreshNames: function () { + $.ajax({ + type: calls.webhooks_entries.type, + url: calls.webhooks_entries.url, + data: {}, + success: function (data) { + if (data.status == "success") { + if (data.body && data.body.entries && data.body.entries.length > 0) { + var config = JSON.parse(data.body.entries[0].config); + var webhooks = config.webhooks; + for (let i = 0; i < webhooks.length; i++) { + makeWebhookCard(webhooks[i]); + } + refreshWebhooks(); + } + } + }, + }); + }, +}; + +function makeWebhookCard(data) { + // prettier-ignore + $("#webhooksParent").append( + "
" + + "
    " + + "
  • " + + "
    " + + "" + + "" + + "
    " + + "
    " + + "" + + "" + + "
    " + + "
    " + + "" + + "" + + "
    " + + "
  • " + + "
  • " + + "
    " + + "" + + "" + + "
    " + + "
  • " + + "
  • " + + "
    " + + "" + + "" + + "
    " + + "
  • " + + "
  • " + + "
    " + + "" + + "
    " + + "
    " + + "Delete" + + "
    " + + "
  • " + + "
" + + "
" + ) + + $(".webhooks #webhookUrl_" + webhooksCounter).val( + data && data.url ? data.url : "" + ); + + cardEditors["webhookHeader_" + webhooksCounter] = CodeMirror.fromTextArea( + document.getElementById("webhookHeader_" + webhooksCounter), + { + path: "js/codemirror/codemirror-5.19.0/", + mode: "javascript", + theme: "elegant", + viewportMargin: Infinity, + lineNumbers: true, + autoRefresh: true, + matchBrackets: true, + } + ); + + const headerDefault = { + "Content-Type": "application/json", + }; + + cardEditors["webhookHeader_" + webhooksCounter].setValue( + JSON.stringify( + data && data.header ? JSON.parse(data.header) : headerDefault, + null, + 4 + ) + ); + + cardEditors["webhookBody_" + webhooksCounter] = CodeMirror.fromTextArea( + document.getElementById("webhookBody_" + webhooksCounter), + { + path: "js/codemirror/codemirror-5.19.0/", + mode: "javascript", + theme: "elegant", + viewportMargin: Infinity, + lineNumbers: true, + autoRefresh: true, + matchBrackets: true, + } + ); + + if (data && data.body) { + cardEditors["webhookBody_" + webhooksCounter].setValue( + JSON.stringify(JSON.parse(data.body), null, 4) + ); + } + + //Delete webhook button + $("#deleteWebhook_" + webhooksCounter).on("click", function () { + var deleteThis = $(this).parent().parent().parent(); + deleteThis.remove(); + }); + + webhooksCounter++; +} + +function refreshWebhooks() { + Materialize.updateTextFields(); + $(".webhooks select").material_select(); +} + +function saveWebhookChanges() { + var json = { webhooks: [] }; + + $("#webhooksParent") + .children("div") + .each(function () { + var webhookId = $(this).attr("webhookId"); + var action = $(this).find("#webhookAction").val(); + var type = $(this).find("#webhookType").val(); + var url = $(this) + .find("#webhookUrl_" + webhookId) + .val(); + var header = cardEditors["webhookHeader_" + webhookId] + ? cardEditors["webhookHeader_" + webhookId].getValue() || "{}" + : "{}"; + var body = cardEditors["webhookBody_" + webhookId] + ? cardEditors["webhookBody_" + webhookId].getValue() || "{}" + : "{}"; + + json.webhooks.push({ + action, + type, + url, + header, + body, + }); + }); + + saveWebhookConfig(json); +} + +function saveWebhookConfig(json) { + $.ajax({ + type: calls.webhooks_save.type, + url: calls.webhooks_save.url, + data: { + config: JSON.stringify(json), + }, + success: function (data) { + if (data.status == "success") { + // Update the variable holding the webhooks configuration + updateWebhookConfig(); + + Materialize.toast( + "Save Successful.", + 1600 + ); + $("#toast_success").parent().css("background-color", "#1565C0"); + } else { + Materialize.toast( + "" + data["message"] + "", + 5000 + ); + $("#toast_failure8").parent().css("background-color", "#a11717"); + } + }, + }); +} + +function updateWebhookConfig() { + $.ajax({ + type: calls.webhooks_config.type, + url: calls.webhooks_config.url, + success: function (d) { + if (d.status == "success") { + console.log("Updated webhooks config in backend"); + } + }, + }); +} + +$(document).ready(function () { + Webhooks.init(); +}); diff --git a/views/configure.pug b/views/configure.pug index 158ed807..2b32b88a 100644 --- a/views/configure.pug +++ b/views/configure.pug @@ -11,6 +11,7 @@ head link(type='text/css' rel='stylesheet' href='config/css/keys.css') link(type='text/css' rel='stylesheet' href='config/css/datasets.css') link(type='text/css' rel='stylesheet' href='config/css/geodatasets.css') + link(type='text/css' rel='stylesheet' href='config/css/webhooks.css') link(type='text/css' rel='stylesheet' href='config/css/config.css') // Let browser know website is optimized for mobile meta(name='viewport' content='width=device-width, initial-scale=1.0') @@ -34,6 +35,7 @@ script(type='text/javascript' src='config/js/calls.js') script(type='text/javascript' src='config/js/keys.js') script(type='text/javascript' src='config/js/datasets.js') script(type='text/javascript' src='config/js/geodatasets.js') +script(type='text/javascript' src='config/js/webhooks.js') script(type='text/javascript' src='config/js/config.js') script(type='text/javascript' src='src/pre/RefreshAuth.js') @@ -48,6 +50,7 @@ script(type='text/javascript' src='src/pre/RefreshAuth.js') a#manage_keys.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Keys a#manage_datasets.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Manage Datasets a#manage_geodatasets.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Manage Geodatasets + a#manage_webhooks.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Manage Webhooks #logout i.logout#config_logout.mdi.mdi-logout.mdi-24px @@ -67,6 +70,7 @@ script(type='text/javascript' src='src/pre/RefreshAuth.js') .container_keys .container_datasets .container_geodatasets +.container_webhooks .container #home_cont(style='display: none') MMGIS Configuration #new_mission_cont(style='display: none; margin-top: 25vh;')