Browse Source

fix(monitor-checks): save checks to the database

bertyhell/feature/monitor-checks
Bert Verhelst 3 years ago
parent
commit
5a29df40ec
  1. 2
      server/database.js
  2. 12
      server/model/monitor.js
  3. 92
      server/server.js
  4. 21
      server/util-server.js
  5. 4
      server/validate-monitor-checks.js
  6. 8
      src/components/MonitorCheckEditor.vue
  7. 4
      src/pages/EditMonitor.vue
  8. 4
      src/util.js
  9. 2
      src/util.ts

2
server/database.js

@ -4,6 +4,7 @@ const { setSetting, setting } = require("./util-server");
const { debug, sleep } = require("../src/util"); const { debug, sleep } = require("../src/util");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const knex = require("knex"); const knex = require("knex");
const MonitorCheck = require("./model/monitor-check");
/** /**
* Database & App Data Folder * Database & App Data Folder
@ -107,6 +108,7 @@ class Database {
// Auto map the model to a bean object // Auto map the model to a bean object
R.freeze(true); R.freeze(true);
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
R.modelList["monitor_checks"] = MonitorCheck;
// Change to WAL // Change to WAL
await R.exec("PRAGMA journal_mode = WAL"); await R.exec("PRAGMA journal_mode = WAL");

12
server/model/monitor.js

@ -6,12 +6,12 @@ dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger, MONITOR_CHECK_SELECTOR_TYPES } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, getTotalClientInRoom } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, getTotalClientInRoom } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
const validateMonitorChecks = require("./validate-monitor-checks"); const validateMonitorChecks = require("../validate-monitor-checks");
const version = require("../../package.json").version; const version = require("../../package.json").version;
/** /**
@ -49,6 +49,13 @@ class Monitor extends BeanModel {
} }
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]); const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
const checks = await R.getAll("SELECT mc.* FROM monitor_checks mc WHERE mc.monitor_id = ?", [this.id]);
checks.forEach(check => {
if (MONITOR_CHECK_SELECTOR_TYPES.includes(check.type) && typeof check.value === "string" && check.value.startsWith("{")) {
check.value = JSON.parse(check.value);
}
});
return { return {
id: this.id, id: this.id,
@ -69,6 +76,7 @@ class Monitor extends BeanModel {
dns_resolve_server: this.dns_resolve_server, dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result, dns_last_result: this.dns_last_result,
notificationIDList, notificationIDList,
checks: checks,
tags: tags, tags: tags,
}; };
} }

92
server/server.js

@ -1,12 +1,16 @@
console.log("Welcome to Uptime Kuma"); console.log("Welcome to Uptime Kuma");
if (! process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
} }
console.log("Node Env: " + process.env.NODE_ENV); console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug, getRandomInt } = require("../src/util"); const {
sleep,
debug,
getRandomInt,
} = require("../src/util");
console.log("Importing Node libraries"); console.log("Importing Node libraries");
const fs = require("fs"); const fs = require("fs");
@ -37,7 +41,16 @@ console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, genSecret, allowDevAllOrigin, checkLogin } = require("./util-server"); const {
getSettings,
setSettings,
setting,
initJWTSecret,
genSecret,
allowDevAllOrigin,
checkLogin,
updateMonitorChecks,
} = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -80,7 +93,7 @@ if (sslKey && sslCert) {
console.log("Server Type: HTTPS"); console.log("Server Type: HTTPS");
server = https.createServer({ server = https.createServer({
key: fs.readFileSync(sslKey), key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert) cert: fs.readFileSync(sslCert),
}, app); }, app);
} else { } else {
console.log("Server Type: HTTP"); console.log("Server Type: HTTP");
@ -91,7 +104,11 @@ const io = new Server(server);
module.exports.io = io; module.exports.io = io;
// Must be after io instantiation // Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client"); const {
sendNotificationList,
sendHeartbeatList,
sendImportantHeartbeatList,
} = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
app.use(express.json()); app.use(express.json());
@ -143,7 +160,7 @@ exports.entryPage = "dashboard";
// Robots.txt // Robots.txt
app.get("/robots.txt", async (_request, response) => { app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:"; let txt = "User-agent: *\nDisallow:";
if (! await setting("searchEngineIndex")) { if (!await setting("searchEngineIndex")) {
txt += " /"; txt += " /";
} }
response.setHeader("Content-Type", "text/plain"); response.setHeader("Content-Type", "text/plain");
@ -467,14 +484,7 @@ exports.entryPage = "dashboard";
bean.user_id = socket.userID; bean.user_id = socket.userID;
await R.store(bean); await R.store(bean);
// Store checks info await updateMonitorChecks(bean.id, checks);
for (let i = 0; i < (checks || []).length; i++) {
let checkBean = R.dispense("monitor_checks");
checks[i].monitor_id = bean.id;
checks[i].value = typeof checks[i].value === "object" ? JSON.stringify(checks[i].value) : checks[i].value;
checkBean.import(checks[i]);
await R.store(checkBean);
}
await updateMonitorNotification(bean.id, notificationIDList); await updateMonitorNotification(bean.id, notificationIDList);
@ -500,7 +510,7 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]); let bean = await R.findOne("monitor", " id = ? ", [monitor.id]);
if (bean.user_id !== socket.userID) { if (bean.user_id !== socket.userID) {
throw new Error("Permission denied."); throw new Error("Permission denied.");
@ -525,13 +535,24 @@ exports.entryPage = "dashboard";
await R.store(bean); await R.store(bean);
// Store checks info // Store checks
let trx = await R.begin();
try {
// delete existing checks for monitor
const existingMonitorChecks = await R.find("monitor_checks", " monitor_id = ?", [bean.id]);
await trx.trashAll(existingMonitorChecks);
// Replace them with new checks
for (let i = 0; i < (checks || []).length; i++) { for (let i = 0; i < (checks || []).length; i++) {
let checkBean = R.dispense("monitor_checks"); let checkBean = trx.dispense("monitor_checks");
checks[i].monitor_id = bean.id; checks[i].monitor_id = bean.id;
checks[i].value = typeof checks[i].value === "object" ? JSON.stringify(checks[i].value) : checks[i].value; checks[i].value = typeof checks[i].value === "object" ? JSON.stringify(checks[i].value) : checks[i].value;
checkBean.import(checks[i]); checkBean.import(checks[i]);
await R.store(checkBean); await trx.store(checkBean);
}
} catch (err) {
await trx.rollback();
throw err;
} }
await updateMonitorNotification(bean.id, monitor.notificationIDList); await updateMonitorNotification(bean.id, monitor.notificationIDList);
@ -712,7 +733,7 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]); let bean = await R.findOne("monitor", " id = ? ", [tag.id]);
bean.name = tag.name; bean.name = tag.name;
bean.color = tag.color; bean.color = tag.color;
await R.store(bean); await R.store(bean);
@ -734,7 +755,7 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]); await R.exec("DELETE FROM tag WHERE id = ? ", [tagID]);
callback({ callback({
ok: true, ok: true,
@ -825,7 +846,7 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
if (! password.currentPassword) { if (!password.currentPassword) {
throw new Error("Invalid new password"); throw new Error("Invalid new password");
} }
@ -879,7 +900,7 @@ exports.entryPage = "dashboard";
callback({ callback({
ok: true, ok: true,
msg: "Saved" msg: "Saved",
}); });
} catch (e) { } catch (e) {
@ -1068,14 +1089,7 @@ exports.entryPage = "dashboard";
bean.user_id = socket.userID; bean.user_id = socket.userID;
await R.store(bean); await R.store(bean);
// Store monitor checks await updateMonitorChecks(bean.id, checks);
for (let i = 0; i < (checks || []).length; i++) {
let checkBean = R.dispense("monitor_checks");
checks[i].monitor_id = bean.id;
checks[i].value = typeof checks[i].value === "object" ? JSON.stringify(checks[i].value) : checks[i].value;
checkBean.import(checks[i]);
await R.store(checkBean);
}
// Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented // Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented
if (version17x) { if (version17x) {
@ -1088,7 +1102,7 @@ exports.entryPage = "dashboard";
]); ]);
let tagId; let tagId;
if (! tag) { if (!tag) {
// -> If it doesn't exist, create new tag from backup file // -> If it doesn't exist, create new tag from backup file
let beanTag = R.dispense("tag"); let beanTag = R.dispense("tag");
beanTag.name = oldTag.name; beanTag.name = oldTag.name;
@ -1173,7 +1187,7 @@ exports.entryPage = "dashboard";
console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`); console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`);
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [ await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
monitorID monitorID,
]); ]);
await sendHeartbeatList(socket, monitorID, true, true); await sendHeartbeatList(socket, monitorID, true, true);
@ -1270,7 +1284,7 @@ async function checkOwner(userID, monitorID) {
userID, userID,
]); ]);
if (! row) { if (!row) {
throw new Error("You do not own this monitor."); throw new Error("You do not own this monitor.");
} }
} }
@ -1311,14 +1325,6 @@ async function getMonitorJSONList(userID) {
]); ]);
for (let monitor of monitorList) { for (let monitor of monitorList) {
const monitorChecks = await R.find("monitor_checks", " monitor_id = ? ORDER BY id", [
monitor.id,
]);
const monitorCheckObj = [];
for (let monitorCheck of monitorChecks) {
monitorCheckObj.push(await monitorCheck.toJSON());
}
monitor.checks = monitorCheckObj;
result[monitor.id] = await monitor.toJSON(); result[monitor.id] = await monitor.toJSON();
} }
@ -1326,7 +1332,7 @@ async function getMonitorJSONList(userID) {
} }
async function initDatabase() { async function initDatabase() {
if (! fs.existsSync(Database.path)) { if (!fs.existsSync(Database.path)) {
console.log("Copying Database"); console.log("Copying Database");
fs.copyFileSync(Database.templatePath, Database.path); fs.copyFileSync(Database.templatePath, Database.path);
} }
@ -1342,7 +1348,7 @@ async function initDatabase() {
"jwtSecret", "jwtSecret",
]); ]);
if (! jwtSecretBean) { if (!jwtSecretBean) {
console.log("JWT secret is not found, generate one."); console.log("JWT secret is not found, generate one.");
jwtSecretBean = await initJWTSecret(); jwtSecretBean = await initJWTSecret();
console.log("Stored JWT secret into database"); console.log("Stored JWT secret into database");

21
server/util-server.js

@ -298,3 +298,24 @@ exports.checkLogin = (socket) => {
throw new Error("You are not logged in."); throw new Error("You are not logged in.");
} }
}; };
exports.updateMonitorChecks = async (monitorId, checks) => {
let trx = await R.begin();
try {
// delete existing checks for monitor
const existingMonitorChecks = await R.find("monitor_checks", " monitor_id = ?", [bean.id]);
await trx.trashAll(existingMonitorChecks);
// Replace them with new checks
for (let i = 0; i < (checks || []).length; i++) {
let checkBean = trx.dispense("monitor_checks");
checks[i].monitor_id = monitorId;
checks[i].value = typeof checks[i].value === "object" ? JSON.stringify(checks[i].value) : checks[i].value;
checkBean.import(checks[i]);
await trx.store(checkBean);
}
} catch (err) {
await trx.rollback();
throw err;
}
};

4
server/model/validate-monitor-checks.js → server/validate-monitor-checks.js

@ -1,5 +1,5 @@
const { checkStatusCode } = require("../util-server"); const { checkStatusCode } = require("./util-server");
const { UP } = require("../../src/util"); const { UP } = require("../src/util");
const get = require("lodash.get"); const get = require("lodash.get");
function validateMonitorChecks(res, checks, bean) { function validateMonitorChecks(res, checks, bean) {

8
src/components/MonitorCheckEditor.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="monitor-check mb-4"> <div class="monitor-check mb-4">
<div> <div>
<select id="type" :value="monitorCheck.type" :class="{'form-select': true, 'mb-1': !!monitorCheck.type}" @input="changeType($event.target.value)"> <select id="type" :value="monitorCheck.type" :class="{'form-select': true, 'mb-1': !!monitorCheck.type}" @input="changeType($event.target.value)" required>
<option value="HTTP_STATUS_CODE_SHOULD_EQUAL"> <option value="HTTP_STATUS_CODE_SHOULD_EQUAL">
{{ $t("HTTP status code should equal") }} {{ $t("HTTP status code should equal") }}
</option> </option>
@ -81,6 +81,7 @@
<script> <script>
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import { MONITOR_CHECK_SELECTOR_TYPES, MONITOR_CHECK_STRING_TYPES } from "../util.ts";
export default { export default {
components: { components: {
@ -121,9 +122,8 @@ export default {
this.$emit("delete"); this.$emit("delete");
}, },
changeType(type) { changeType(type) {
const stringValueTypes = ["HTTP_STATUS_CODE_SHOULD_EQUAL", "RESPONSE_SHOULD_CONTAIN_TEXT", "RESPONSE_SHOULD_NOT_CONTAIN_TEXT", "RESPONSE_SHOULD_MATCH_REGEX", "RESPONSE_SHOULD_NOT_MATCH_REGEX"]; if (MONITOR_CHECK_STRING_TYPES.includes(type) && MONITOR_CHECK_STRING_TYPES.includes(this.monitorCheck.type) ||
if (stringValueTypes.includes(type) && stringValueTypes.includes(this.monitorCheck.type) || MONITOR_CHECK_SELECTOR_TYPES.includes(type) && MONITOR_CHECK_SELECTOR_TYPES.includes(this.monitorCheck.type)) {
!stringValueTypes.includes(type) && !stringValueTypes.includes(this.monitorCheck.type)) {
// Check value stays same type (string => string or object => object) // Check value stays same type (string => string or object => object)
this.$emit("change", { this.$emit("change", {
...this.monitorCheck, ...this.monitorCheck,

4
src/pages/EditMonitor.vue

@ -333,7 +333,9 @@ export default {
}, },
deleteMonitorCheck(index) { deleteMonitorCheck(index) {
this.monitor.checks = this.monitor.checks.splice(index, 1); const newList = [...this.monitor.checks];
newList.splice(index, 1);
this.monitor.checks = newList;
}, },
async submit() { async submit() {

4
src/util.js

@ -7,7 +7,7 @@
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MONITOR_CHECK_SELECTOR_TYPES = exports.MONITOR_CHECK_STRING_TYPES = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const _dayjs = require("dayjs"); const _dayjs = require("dayjs");
const dayjs = _dayjs; const dayjs = _dayjs;
exports.isDev = process.env.NODE_ENV === "development"; exports.isDev = process.env.NODE_ENV === "development";
@ -18,6 +18,8 @@ exports.PENDING = 2;
exports.STATUS_PAGE_ALL_DOWN = 0; exports.STATUS_PAGE_ALL_DOWN = 0;
exports.STATUS_PAGE_ALL_UP = 1; exports.STATUS_PAGE_ALL_UP = 1;
exports.STATUS_PAGE_PARTIAL_DOWN = 2; exports.STATUS_PAGE_PARTIAL_DOWN = 2;
exports.MONITOR_CHECK_STRING_TYPES = ["HTTP_STATUS_CODE_SHOULD_EQUAL", "RESPONSE_SHOULD_CONTAIN_TEXT", "RESPONSE_SHOULD_NOT_CONTAIN_TEXT", "RESPONSE_SHOULD_MATCH_REGEX", "RESPONSE_SHOULD_NOT_MATCH_REGEX"];
exports.MONITOR_CHECK_SELECTOR_TYPES = ["RESPONSE_SELECTOR_SHOULD_EQUAL", "RESPONSE_SELECTOR_SHOULD_NOT_EQUAL", "RESPONSE_SELECTOR_SHOULD_MATCH_REGEX", "RESPONSE_SELECTOR_SHOULD_NOT_MATCH_REGEX"];
function flipStatus(s) { function flipStatus(s) {
if (s === exports.UP) { if (s === exports.UP) {
return exports.DOWN; return exports.DOWN;

2
src/util.ts

@ -19,6 +19,8 @@ export const STATUS_PAGE_ALL_DOWN = 0;
export const STATUS_PAGE_ALL_UP = 1; export const STATUS_PAGE_ALL_UP = 1;
export const STATUS_PAGE_PARTIAL_DOWN = 2; export const STATUS_PAGE_PARTIAL_DOWN = 2;
export const MONITOR_CHECK_STRING_TYPES = ["HTTP_STATUS_CODE_SHOULD_EQUAL", "RESPONSE_SHOULD_CONTAIN_TEXT", "RESPONSE_SHOULD_NOT_CONTAIN_TEXT", "RESPONSE_SHOULD_MATCH_REGEX", "RESPONSE_SHOULD_NOT_MATCH_REGEX"];
export const MONITOR_CHECK_SELECTOR_TYPES = ["RESPONSE_SELECTOR_SHOULD_EQUAL", "RESPONSE_SELECTOR_SHOULD_NOT_EQUAL", "RESPONSE_SELECTOR_SHOULD_MATCH_REGEX", "RESPONSE_SELECTOR_SHOULD_NOT_MATCH_REGEX"];
export function flipStatus(s: number) { export function flipStatus(s: number) {
if (s === UP) { if (s === UP) {

Loading…
Cancel
Save