diff --git a/.dockerignore b/.dockerignore index fedba889..3d92084d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,10 +2,12 @@ /dist /node_modules /data +/out /test /kubernetes /.do **/.dockerignore +/private **/.git **/.gitignore **/docker-compose* diff --git a/.eslintrc.js b/.eslintrc.js index 21fb5608..8b45337f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { requireConfigFile: false, }, rules: { + "linebreak-style": ["error", "unix"], "camelcase": ["warn", { "properties": "never", "ignoreImports": true @@ -33,11 +34,12 @@ module.exports = { }, ], quotes: ["warn", "double"], - //semi: ['off', 'never'], + semi: "warn", "vue/html-indent": ["warn", 4], // default: 2 "vue/max-attributes-per-line": "off", "vue/singleline-html-element-content-newline": "off", "vue/html-self-closing": "off", + "vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly "no-multi-spaces": ["error", { ignoreEOLComments: true, }], @@ -85,10 +87,10 @@ module.exports = { }, "overrides": [ { - "files": [ "src/languages/*.js" ], + "files": [ "src/languages/*.js", "src/icon.js" ], "rules": { "comma-dangle": ["error", "always-multiline"], } } ] -} +}; diff --git a/.gitignore b/.gitignore index 56007fb0..2bf60f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ dist-ssr /data !/data/.gitkeep .vscode + +/private +/out diff --git a/db/demo_kuma.db b/db/demo_kuma.db deleted file mode 100644 index 2042fcf2..00000000 Binary files a/db/demo_kuma.db and /dev/null differ diff --git a/db/patch-group-table.sql b/db/patch-group-table.sql new file mode 100644 index 00000000..1c6f366b --- /dev/null +++ b/db/patch-group-table.sql @@ -0,0 +1,30 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +create table `group` +( + id INTEGER not null + constraint group_pk + primary key autoincrement, + name VARCHAR(255) not null, + created_date DATETIME default (DATETIME('now')) not null, + public BOOLEAN default 0 not null, + active BOOLEAN default 1 not null, + weight BOOLEAN NOT NULL DEFAULT 1000 +); + +CREATE TABLE [monitor_group] +( + [id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + [monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, + [group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, + weight BOOLEAN NOT NULL DEFAULT 1000 +); + +CREATE INDEX [fk] + ON [monitor_group] ( + [monitor_id], + [group_id]); + + +COMMIT; diff --git a/db/patch-incident-table.sql b/db/patch-incident-table.sql new file mode 100644 index 00000000..531cfb38 --- /dev/null +++ b/db/patch-incident-table.sql @@ -0,0 +1,18 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +create table incident +( + id INTEGER not null + constraint incident_pk + primary key autoincrement, + title VARCHAR(255) not null, + content TEXT not null, + style VARCHAR(30) default 'warning' not null, + created_date DATETIME default (DATETIME('now')) not null, + last_updated_date DATETIME, + pin BOOLEAN default 1 not null, + active BOOLEAN default 1 not null +); + +COMMIT; diff --git a/package-lock.json b/package-lock.json index 523098fe..c6af1835 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,14 +42,17 @@ "thirty-two": "^1.0.2", "timezones-list": "^3.0.1", "v-pagination-3": "^0.1.6", - "vue": "^3.2.8", + "vue": "next", "vue-chart-3": "^0.5.8", "vue-confirm-dialog": "^1.0.2", + "vue-contenteditable": "^3.0.4", "vue-i18n": "^9.1.7", + "vue-image-crop-upload": "^3.0.3", "vue-multiselect": "^3.0.0-alpha.2", "vue-qrcode": "^1.0.0", "vue-router": "^4.0.11", - "vue-toastification": "^2.0.0-rc.1" + "vue-toastification": "^2.0.0-rc.1", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@babel/eslint-parser": "^7.15.4", @@ -61,7 +64,7 @@ "dns2": "^2.0.1", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.17.0", - "sass": "^1.39.2", + "sass": "^1.41.0", "stylelint": "^13.13.1", "stylelint-config-standard": "^22.0.0", "typescript": "^4.4.3", @@ -156,9 +159,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.7.tgz", - "integrity": "sha512-yJkHyomClm6A2Xzb8pdAo4HzYMSXFn1O5zrCYvbFP0yQFvHueLedV8WiEno8yJOKStjUXzBZzJFeWQ7b3YMsqQ==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.4.tgz", + "integrity": "sha512-hPMIAmGNbmQzXJIo2P43Zj9UhRmGev5f9nqdBFOWNGDGh6XKmjby79woBvg6y0Jur6yRfQBneDbUQ8ZVc1krFw==", "dev": true, "dependencies": { "eslint-scope": "^5.1.1", @@ -277,19 +280,19 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.7.tgz", - "integrity": "sha512-ZNqjjQG/AuFfekFTY+7nY4RgBSklgTu970c7Rj3m/JOhIu5KPBUuTA9AY6zaKcUvk4g6EbDXdBnhi35FAssdSw==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz", + "integrity": "sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.15.4", "@babel/helper-replace-supers": "^7.15.4", "@babel/helper-simple-access": "^7.15.4", "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.14.9", "@babel/template": "^7.15.4", "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.6" + "@babel/types": "^7.15.4" }, "engines": { "node": ">=6.9.0" @@ -347,9 +350,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", "engines": { "node": ">=6.9.0" } @@ -463,9 +466,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", - "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==", + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.6.tgz", + "integrity": "sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==", "bin": { "parser": "bin/babel-parser.js" }, @@ -474,9 +477,9 @@ } }, "node_modules/@babel/standalone": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.15.7.tgz", - "integrity": "sha512-1dPLi+eQEJE0g1GnUM0Ik2GcS5SMXivoxt6meQxQxGWEd/DCdSBRJClUVlQ25Vbqe49g1HG5Ej0ULhmsqtSMmg==", + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.15.6.tgz", + "integrity": "sha512-1N9+KHL9ZYKiDDXFgBvg8Sl135evIJgP/YZdOhqdfMMTL/zuAm6bUi/FYEwzTXYhQS8MBtRMVmmcIurif7hYiQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -896,9 +899,9 @@ } }, "node_modules/@types/bootstrap": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz", - "integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.4.tgz", + "integrity": "sha512-VAY+o6sCKrJ7Xix/lugdvQz0PpOn7Go+fQzCXOZvIdp7E/TDaiJddInVhNB/84bk9NX6uuKFSfl2pqslNYH9aA==", "dev": true, "dependencies": { "@popperjs/core": "^2.9.2", @@ -1039,9 +1042,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.4.tgz", - "integrity": "sha512-KDazLNYAGIuJugdbULwFZULF9qQ13yNWEBFnfVpqlpgAAo6H/qnM9RjBgh0A0kmHf3XxAKLdN5mTIng9iUvVLA==" + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -1106,53 +1109,52 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.8.1.tgz", - "integrity": "sha512-gktQGZ7qfaDdVJhT86fWSkyhP+bdoA81f5S2TQOL5Sbe5q7B36XfLGq8Q0BpHoqhPSflAMe6WwM1IecP1sChRw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.6.2.tgz", + "integrity": "sha512-Pf+dqkT4pWPfziPm51VtDXsPwE74CEGRiK6Vgm5EDBewHw1EgcxG7V2ZI/Yqj5gcDy5nVtjgx0AbsTL+F3gddg==", "dev": true, "engines": { "node": ">=12.0.0" }, "peerDependencies": { - "@vue/compiler-sfc": "^3.2.6", - "vite": "^2.5.10" + "@vue/compiler-sfc": "^3.2.6" } }, "node_modules/@vue/compiler-core": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.12.tgz", - "integrity": "sha512-IGJ0JmrAaAl5KBBegPAKkoXvsfDFgN/h7K1t/+0MxqpZF1fTDVUOp3tG7q9gWa7fwzGEaIsPhjtT5C3qztdLKg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.11.tgz", + "integrity": "sha512-bcbsLx5XyQg8WDDEGwmpX0BfEfv82wIs9fWFelpyVhNRGMaABvUTalYINyfhVT+jOqNaD4JBhJiVKd/8TmsHWg==", "dependencies": { "@babel/parser": "^7.15.0", "@babel/types": "^7.15.0", - "@vue/shared": "3.2.12", + "@vue/shared": "3.2.11", "estree-walker": "^2.0.2", "source-map": "^0.6.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.12.tgz", - "integrity": "sha512-MulvKilA2USm8ubPfvXvNY55HVTn+zHERsXeNg437TXrmM4FRCis6zjWW47QZ3ZyxEkCdqOmuiFCtXbpnuthyw==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.11.tgz", + "integrity": "sha512-DNvhUHI/1Hn0/+ZYDYGAuDGasUm+XHKC3FE4GqkNCTO/fcLaJMRg/7eT1m1lkc7jPffUwwfh1rZru5mwzOjrNw==", "dependencies": { - "@vue/compiler-core": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/compiler-core": "3.2.11", + "@vue/shared": "3.2.11" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.12.tgz", - "integrity": "sha512-EjzeMQ7H2ICj+JRw2buSFXTocdCg8e5yWQTlNM/6h/u68sTwMbIfiOJBFEwBhG/wCG7Nb6Nnz888AfHTU3hdrA==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.11.tgz", + "integrity": "sha512-cUIaS8mgJrQ6yucj2AupWAwBRITK3W/a8wCOn9g5fJGtOl8h4APY8vN3lzP8HIJDyEeRF3I8SfRhL+oX97kSnw==", "dev": true, "dependencies": { "@babel/parser": "^7.15.0", "@babel/types": "^7.15.0", "@types/estree": "^0.0.48", - "@vue/compiler-core": "3.2.12", - "@vue/compiler-dom": "3.2.12", - "@vue/compiler-ssr": "3.2.12", - "@vue/ref-transform": "3.2.12", - "@vue/shared": "3.2.12", + "@vue/compiler-core": "3.2.11", + "@vue/compiler-dom": "3.2.11", + "@vue/compiler-ssr": "3.2.11", + "@vue/ref-transform": "3.2.11", + "@vue/shared": "3.2.11", "consolidate": "^0.16.0", "estree-walker": "^2.0.2", "hash-sum": "^2.0.0", @@ -1166,64 +1168,64 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.12.tgz", - "integrity": "sha512-sY+VbLQ17FPr1CgirnqEgY+jbC7wI5c2Ma6u8le0+b4UKMYF9urI2pybAZc1nKz6O78FWA3OSnQFxTTLppe+9Q==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.11.tgz", + "integrity": "sha512-+ptAdUlFDij+Z0VGCbRRkxQlNev5LkbZAntvkxrFjc08CTMhZmiV4Js48n2hAmuSXaKNEpmGkDGU26c/vf1+xw==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/compiler-dom": "3.2.11", + "@vue/shared": "3.2.11" } }, "node_modules/@vue/devtools-api": { - "version": "6.0.0-beta.17", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.17.tgz", - "integrity": "sha512-hwGY4Xxc2nl34OyNH7l2VO8/ja3R78B8bcbaBQnZljSju5Z0Bm9HTt+/fQao+TUrs3gfNrrQrY3euWqiaG8chw==" + "version": "6.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.15.tgz", + "integrity": "sha512-quBx4Jjpexo6KDiNUGFr/zF/2A4srKM9S9v2uHgMXSU//hjgq1eGzqkIFql8T9gfX5ZaVOUzYBP3jIdIR3PKIA==" }, "node_modules/@vue/reactivity": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.12.tgz", - "integrity": "sha512-Lr5CTQjFm5mT/6DGnVNhptmba/Qg1DbD6eNWWmiHLMlpPt4q2ww9A2orEjVw0qNcdTJ04JLPEVAz5jhTZTCfIg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.11.tgz", + "integrity": "sha512-hEQstxPQbgGZq5qApzrvbDmRdK1KP96O/j4XrwT8fVkT1ytkFs4fH2xNEh9QKwXfybbQkLs77W7OfXCv5o6qbA==", "dependencies": { - "@vue/shared": "3.2.12" + "@vue/shared": "3.2.11" } }, "node_modules/@vue/ref-transform": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.12.tgz", - "integrity": "sha512-lS7TDda61iSf3ljokXVfN0VbOsQdmpST6MZLjxzBydFCECCJaEAr6o+K8VZ7NhUCSrl+gKXHpdXxmcvwdk66aQ==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.11.tgz", + "integrity": "sha512-7rX0YsfYb7+1PeKPME1tQyUQcQgt0sIXRRnPD1Vw8Zs2KIo90YLy9CrvwalcRCxGw0ScsjBEhVjJtWIT79TElg==", "dev": true, "dependencies": { "@babel/parser": "^7.15.0", - "@vue/compiler-core": "3.2.12", - "@vue/shared": "3.2.12", + "@vue/compiler-core": "3.2.11", + "@vue/shared": "3.2.11", "estree-walker": "^2.0.2", "magic-string": "^0.25.7" } }, "node_modules/@vue/runtime-core": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.12.tgz", - "integrity": "sha512-LO+ztgcmsomavYUaSq7BTteh8pmnUmvUnXUFVYdlcg3VCdYRS0ImlclpYsNHqjAk2gU+H09dr2PP0kL961xUfQ==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.11.tgz", + "integrity": "sha512-horlxjWwSvModC87WdsWswzzHE5IexmKkQA65S5vFgP5hLUBW+HRyScDeuB/RRcFmqnf+ozacNCfap0kqcpODw==", "dependencies": { - "@vue/reactivity": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/reactivity": "3.2.11", + "@vue/shared": "3.2.11" } }, "node_modules/@vue/runtime-dom": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.12.tgz", - "integrity": "sha512-+NSDqivgihvoPYbKFDmzFu1tW7SOzwc7r0b7T8vsJtooVPGxwtfAFZ6wyLtteOXXrCpyTR3kpyTCIp31uY7aJg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.11.tgz", + "integrity": "sha512-cOK1g0INdiCbds2xrrJKrrN+pDHuLz6esUs/crdEiupDuX7IeiMbdqrAQCkYHp5P1KLWcbGlkmwfVD7HQGii0Q==", "dependencies": { - "@vue/runtime-core": "3.2.12", - "@vue/shared": "3.2.12", + "@vue/runtime-core": "3.2.11", + "@vue/shared": "3.2.11", "csstype": "^2.6.8" } }, "node_modules/@vue/shared": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.12.tgz", - "integrity": "sha512-5CkaifUCJwcTuru7FDwKFacPJuEoGUTw0LKSa5bw40B23s0TS+MGlYR1285nbV/ju3QUGlA6d6PD+GJkWy7uFg==" + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.11.tgz", + "integrity": "sha512-ovfXAsSsCvV9JVceWjkqC/7OF5HbgLOtCWjCIosmPGG8lxbPuavhIxRH1dTx4Dg9xLgRTNLvI3pVxG4ItQZekg==" }, "node_modules/abbrev": { "version": "1.1.1", @@ -1575,6 +1577,27 @@ "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=" }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, "node_modules/backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -1868,9 +1891,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001259", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001259.tgz", - "integrity": "sha512-V7mQTFhjITxuk9zBpI6nYsiTXhcPe05l+364nZjK7MFK/E7ibvYBSAXr4YcA6oPR8j3ZLM/LN+lUqUVAQEUZFg==", + "version": "1.0.30001257", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz", + "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==", "dev": true, "funding": { "type": "opencollective", @@ -2164,9 +2187,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "node_modules/core-js": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.0.tgz", - "integrity": "sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", + "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==", "dev": true, "hasInstallScript": true, "funding": { @@ -2459,9 +2482,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "node_modules/electron-to-chromium": { - "version": "1.3.845", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.845.tgz", - "integrity": "sha512-y0RorqmExFDI4RjLEC6j365bIT5UAXf9WIRcknvSFHVhbC/dRnCgJnPA3DUUW6SCC85QGKEafgqcHJ6uPdEP1Q==", + "version": "1.3.840", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.840.tgz", + "integrity": "sha512-yRoUmTLDJnkIJx23xLY7GbSvnmDCq++NSuxHDQ0jiyDJ9YZBUGJcrdUqm+ZwZFzMbCciVzfem2N2AWiHJcWlbw==", "dev": true }, "node_modules/emoji-regex": { @@ -2668,9 +2691,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.18.0.tgz", - "integrity": "sha512-ceDXlXYMMPMSXw7tdKUR42w9jlzthJGJ3Kvm3YrZ0zuQfvAySNxe8sm6VHuksBW0+060GzYXhHJG6IHVOfF83Q==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.17.0.tgz", + "integrity": "sha512-Rq5R2QetDCgC+kBFQw1+aJ5B93tQ4xqZvoCUxuIzwTonngNArsdP8ChM8PowIzsJvRtWl4ltGh/bZcN3xhFWSw==", "dev": true, "dependencies": { "eslint-utils": "^2.1.0", @@ -4328,9 +4351,9 @@ } }, "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", + "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", "dev": true, "engines": { "node": ">=8" @@ -4659,12 +4682,9 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, "node_modules/node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-BXSmNTLLDHT0UjQDg5E23x+0n/hPDjySqc0ELE4NpCa2wE5qmmaEWFRP/+v8pfuocchR9l5vFLbSB7CPE2ahvQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==", "engines": { "node": "4.x || >=6.0.0" } @@ -4727,9 +4747,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "1.1.76", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.76.tgz", - "integrity": "sha512-9/IECtNr8dXNmPWmFXepT0/7o5eolGesHUa3mtr0KlgnCvnZxwh2qensKL42JJY2vQKC3nIBXetFAqR+PW1CmA==", + "version": "1.1.75", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", + "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", "dev": true }, "node_modules/nodemailer": { @@ -6003,9 +6023,9 @@ } }, "node_modules/redbean-node/node_modules/@types/node": { - "version": "14.17.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.17.tgz", - "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==" + "version": "14.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.16.tgz", + "integrity": "sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==" }, "node_modules/redent": { "version": "3.0.0", @@ -6259,9 +6279,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.0.tgz", - "integrity": "sha512-kcjxsemgaOnfl43oZgO/IePLvXQI0ZKzo0/xbCt6uyrg3FY/FF8hVK9YoO8GiZBcEG2Ebl79EKnUc+aiE4f2Vw==", + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.41.0.tgz", + "integrity": "sha512-wb8nT60cjo9ZZMcHzG7TzdbFtCAmHEKWrH+zAdScPb4ZxL64WQBnGdbp5nwlenW5wJPcHva1JWmVa0h6iqA5eg==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0" @@ -6461,6 +6481,11 @@ "node": ">=10.0.0" } }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7205,11 +7230,6 @@ "node": ">=0.8" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -7498,9 +7518,9 @@ } }, "node_modules/vite": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", - "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.7.tgz", + "integrity": "sha512-hyUoWmRPhjN1aI+ZSBqDINKdIq7aokHE2ZXiztOg4YlmtpeQtMwMeyxv6X9YxHZmvGzg/js/eATM9Z1nwyakxg==", "dev": true, "dependencies": { "esbuild": "^0.12.17", @@ -7519,13 +7539,13 @@ } }, "node_modules/vue": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.12.tgz", - "integrity": "sha512-VV14HtubmB56uuQaSvLkJZgoocPiN8CJI3zZA9y8h7q/Z5hcknDIFkbq5d8ku0ukZ6AJPQqMsZWcq0qryF0jgg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.11.tgz", + "integrity": "sha512-JkI3/eIgfk4E0f/p319TD3EZgOwBQfftgnkRsXlT7OrRyyiyoyUXn6embPGZXSBxD3LoZ9SWhJoxLhFh5AleeA==", "dependencies": { - "@vue/compiler-dom": "3.2.12", - "@vue/runtime-dom": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/compiler-dom": "3.2.11", + "@vue/runtime-dom": "3.2.11", + "@vue/shared": "3.2.11" } }, "node_modules/vue-chart-3": { @@ -7559,6 +7579,14 @@ "vue": "^2.6.10" } }, + "node_modules/vue-contenteditable": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/vue-contenteditable/-/vue-contenteditable-3.0.4.tgz", + "integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-demi": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz", @@ -7645,6 +7673,14 @@ "vue": "^3.0.0" } }, + "node_modules/vue-image-crop-upload": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vue-image-crop-upload/-/vue-image-crop-upload-3.0.3.tgz", + "integrity": "sha512-VeBsU0oI1hXeCvdpnu19DM/r3KTlI8SUXTxsHsU4MhDXR0ahRziiL9tf4FbILGx+gRVNZhGbl32yuM6TiaGNhA==", + "dependencies": { + "babel-runtime": "^6.11.6" + } + }, "node_modules/vue-multiselect": { "version": "3.0.0-alpha.2", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.0.0-alpha.2.tgz", @@ -7717,18 +7753,15 @@ "vue": "^3.0.2" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" } }, "node_modules/which": { @@ -8109,9 +8142,9 @@ } }, "@babel/eslint-parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.7.tgz", - "integrity": "sha512-yJkHyomClm6A2Xzb8pdAo4HzYMSXFn1O5zrCYvbFP0yQFvHueLedV8WiEno8yJOKStjUXzBZzJFeWQ7b3YMsqQ==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.4.tgz", + "integrity": "sha512-hPMIAmGNbmQzXJIo2P43Zj9UhRmGev5f9nqdBFOWNGDGh6XKmjby79woBvg6y0Jur6yRfQBneDbUQ8ZVc1krFw==", "dev": true, "requires": { "eslint-scope": "^5.1.1", @@ -8198,19 +8231,19 @@ } }, "@babel/helper-module-transforms": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.7.tgz", - "integrity": "sha512-ZNqjjQG/AuFfekFTY+7nY4RgBSklgTu970c7Rj3m/JOhIu5KPBUuTA9AY6zaKcUvk4g6EbDXdBnhi35FAssdSw==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz", + "integrity": "sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.15.4", "@babel/helper-replace-supers": "^7.15.4", "@babel/helper-simple-access": "^7.15.4", "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.14.9", "@babel/template": "^7.15.4", "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.6" + "@babel/types": "^7.15.4" } }, "@babel/helper-optimise-call-expression": { @@ -8253,9 +8286,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==" + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==" }, "@babel/helper-validator-option": { "version": "7.14.5", @@ -8344,14 +8377,14 @@ } }, "@babel/parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", - "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==" + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.6.tgz", + "integrity": "sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==" }, "@babel/standalone": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.15.7.tgz", - "integrity": "sha512-1dPLi+eQEJE0g1GnUM0Ik2GcS5SMXivoxt6meQxQxGWEd/DCdSBRJClUVlQ25Vbqe49g1HG5Ej0ULhmsqtSMmg==", + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.15.6.tgz", + "integrity": "sha512-1N9+KHL9ZYKiDDXFgBvg8Sl135evIJgP/YZdOhqdfMMTL/zuAm6bUi/FYEwzTXYhQS8MBtRMVmmcIurif7hYiQ==", "dev": true }, "@babel/template": { @@ -8468,7 +8501,8 @@ "@fortawesome/vue-fontawesome": { "version": "3.0.0-4", "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-4.tgz", - "integrity": "sha512-dQVhhMRcUPCb0aqk5ohm0KGk5OJ7wFZ9aYapLzJB3Z+xs7LhkRWLTb87reelUAG5PFDjutDAXuloT9hi6cz72A==" + "integrity": "sha512-dQVhhMRcUPCb0aqk5ohm0KGk5OJ7wFZ9aYapLzJB3Z+xs7LhkRWLTb87reelUAG5PFDjutDAXuloT9hi6cz72A==", + "requires": {} }, "@humanwhocodes/config-array": { "version": "0.5.0", @@ -8665,9 +8699,9 @@ } }, "@types/bootstrap": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz", - "integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.4.tgz", + "integrity": "sha512-VAY+o6sCKrJ7Xix/lugdvQz0PpOn7Go+fQzCXOZvIdp7E/TDaiJddInVhNB/84bk9NX6uuKFSfl2pqslNYH9aA==", "dev": true, "requires": { "@popperjs/core": "^2.9.2", @@ -8808,9 +8842,9 @@ "dev": true }, "@types/node": { - "version": "16.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.4.tgz", - "integrity": "sha512-KDazLNYAGIuJugdbULwFZULF9qQ13yNWEBFnfVpqlpgAAo6H/qnM9RjBgh0A0kmHf3XxAKLdN5mTIng9iUvVLA==" + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" }, "@types/normalize-package-data": { "version": "2.4.1", @@ -8869,46 +8903,47 @@ } }, "@vitejs/plugin-vue": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.8.1.tgz", - "integrity": "sha512-gktQGZ7qfaDdVJhT86fWSkyhP+bdoA81f5S2TQOL5Sbe5q7B36XfLGq8Q0BpHoqhPSflAMe6WwM1IecP1sChRw==", - "dev": true + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.6.2.tgz", + "integrity": "sha512-Pf+dqkT4pWPfziPm51VtDXsPwE74CEGRiK6Vgm5EDBewHw1EgcxG7V2ZI/Yqj5gcDy5nVtjgx0AbsTL+F3gddg==", + "dev": true, + "requires": {} }, "@vue/compiler-core": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.12.tgz", - "integrity": "sha512-IGJ0JmrAaAl5KBBegPAKkoXvsfDFgN/h7K1t/+0MxqpZF1fTDVUOp3tG7q9gWa7fwzGEaIsPhjtT5C3qztdLKg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.11.tgz", + "integrity": "sha512-bcbsLx5XyQg8WDDEGwmpX0BfEfv82wIs9fWFelpyVhNRGMaABvUTalYINyfhVT+jOqNaD4JBhJiVKd/8TmsHWg==", "requires": { "@babel/parser": "^7.15.0", "@babel/types": "^7.15.0", - "@vue/shared": "3.2.12", + "@vue/shared": "3.2.11", "estree-walker": "^2.0.2", "source-map": "^0.6.1" } }, "@vue/compiler-dom": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.12.tgz", - "integrity": "sha512-MulvKilA2USm8ubPfvXvNY55HVTn+zHERsXeNg437TXrmM4FRCis6zjWW47QZ3ZyxEkCdqOmuiFCtXbpnuthyw==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.11.tgz", + "integrity": "sha512-DNvhUHI/1Hn0/+ZYDYGAuDGasUm+XHKC3FE4GqkNCTO/fcLaJMRg/7eT1m1lkc7jPffUwwfh1rZru5mwzOjrNw==", "requires": { - "@vue/compiler-core": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/compiler-core": "3.2.11", + "@vue/shared": "3.2.11" } }, "@vue/compiler-sfc": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.12.tgz", - "integrity": "sha512-EjzeMQ7H2ICj+JRw2buSFXTocdCg8e5yWQTlNM/6h/u68sTwMbIfiOJBFEwBhG/wCG7Nb6Nnz888AfHTU3hdrA==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.11.tgz", + "integrity": "sha512-cUIaS8mgJrQ6yucj2AupWAwBRITK3W/a8wCOn9g5fJGtOl8h4APY8vN3lzP8HIJDyEeRF3I8SfRhL+oX97kSnw==", "dev": true, "requires": { "@babel/parser": "^7.15.0", "@babel/types": "^7.15.0", "@types/estree": "^0.0.48", - "@vue/compiler-core": "3.2.12", - "@vue/compiler-dom": "3.2.12", - "@vue/compiler-ssr": "3.2.12", - "@vue/ref-transform": "3.2.12", - "@vue/shared": "3.2.12", + "@vue/compiler-core": "3.2.11", + "@vue/compiler-dom": "3.2.11", + "@vue/compiler-ssr": "3.2.11", + "@vue/ref-transform": "3.2.11", + "@vue/shared": "3.2.11", "consolidate": "^0.16.0", "estree-walker": "^2.0.2", "hash-sum": "^2.0.0", @@ -8922,64 +8957,64 @@ } }, "@vue/compiler-ssr": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.12.tgz", - "integrity": "sha512-sY+VbLQ17FPr1CgirnqEgY+jbC7wI5c2Ma6u8le0+b4UKMYF9urI2pybAZc1nKz6O78FWA3OSnQFxTTLppe+9Q==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.11.tgz", + "integrity": "sha512-+ptAdUlFDij+Z0VGCbRRkxQlNev5LkbZAntvkxrFjc08CTMhZmiV4Js48n2hAmuSXaKNEpmGkDGU26c/vf1+xw==", "dev": true, "requires": { - "@vue/compiler-dom": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/compiler-dom": "3.2.11", + "@vue/shared": "3.2.11" } }, "@vue/devtools-api": { - "version": "6.0.0-beta.17", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.17.tgz", - "integrity": "sha512-hwGY4Xxc2nl34OyNH7l2VO8/ja3R78B8bcbaBQnZljSju5Z0Bm9HTt+/fQao+TUrs3gfNrrQrY3euWqiaG8chw==" + "version": "6.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.15.tgz", + "integrity": "sha512-quBx4Jjpexo6KDiNUGFr/zF/2A4srKM9S9v2uHgMXSU//hjgq1eGzqkIFql8T9gfX5ZaVOUzYBP3jIdIR3PKIA==" }, "@vue/reactivity": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.12.tgz", - "integrity": "sha512-Lr5CTQjFm5mT/6DGnVNhptmba/Qg1DbD6eNWWmiHLMlpPt4q2ww9A2orEjVw0qNcdTJ04JLPEVAz5jhTZTCfIg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.11.tgz", + "integrity": "sha512-hEQstxPQbgGZq5qApzrvbDmRdK1KP96O/j4XrwT8fVkT1ytkFs4fH2xNEh9QKwXfybbQkLs77W7OfXCv5o6qbA==", "requires": { - "@vue/shared": "3.2.12" + "@vue/shared": "3.2.11" } }, "@vue/ref-transform": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.12.tgz", - "integrity": "sha512-lS7TDda61iSf3ljokXVfN0VbOsQdmpST6MZLjxzBydFCECCJaEAr6o+K8VZ7NhUCSrl+gKXHpdXxmcvwdk66aQ==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.11.tgz", + "integrity": "sha512-7rX0YsfYb7+1PeKPME1tQyUQcQgt0sIXRRnPD1Vw8Zs2KIo90YLy9CrvwalcRCxGw0ScsjBEhVjJtWIT79TElg==", "dev": true, "requires": { "@babel/parser": "^7.15.0", - "@vue/compiler-core": "3.2.12", - "@vue/shared": "3.2.12", + "@vue/compiler-core": "3.2.11", + "@vue/shared": "3.2.11", "estree-walker": "^2.0.2", "magic-string": "^0.25.7" } }, "@vue/runtime-core": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.12.tgz", - "integrity": "sha512-LO+ztgcmsomavYUaSq7BTteh8pmnUmvUnXUFVYdlcg3VCdYRS0ImlclpYsNHqjAk2gU+H09dr2PP0kL961xUfQ==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.11.tgz", + "integrity": "sha512-horlxjWwSvModC87WdsWswzzHE5IexmKkQA65S5vFgP5hLUBW+HRyScDeuB/RRcFmqnf+ozacNCfap0kqcpODw==", "requires": { - "@vue/reactivity": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/reactivity": "3.2.11", + "@vue/shared": "3.2.11" } }, "@vue/runtime-dom": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.12.tgz", - "integrity": "sha512-+NSDqivgihvoPYbKFDmzFu1tW7SOzwc7r0b7T8vsJtooVPGxwtfAFZ6wyLtteOXXrCpyTR3kpyTCIp31uY7aJg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.11.tgz", + "integrity": "sha512-cOK1g0INdiCbds2xrrJKrrN+pDHuLz6esUs/crdEiupDuX7IeiMbdqrAQCkYHp5P1KLWcbGlkmwfVD7HQGii0Q==", "requires": { - "@vue/runtime-core": "3.2.12", - "@vue/shared": "3.2.12", + "@vue/runtime-core": "3.2.11", + "@vue/shared": "3.2.11", "csstype": "^2.6.8" } }, "@vue/shared": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.12.tgz", - "integrity": "sha512-5CkaifUCJwcTuru7FDwKFacPJuEoGUTw0LKSa5bw40B23s0TS+MGlYR1285nbV/ju3QUGlA6d6PD+GJkWy7uFg==" + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.11.tgz", + "integrity": "sha512-ovfXAsSsCvV9JVceWjkqC/7OF5HbgLOtCWjCIosmPGG8lxbPuavhIxRH1dTx4Dg9xLgRTNLvI3pVxG4ItQZekg==" }, "abbrev": { "version": "1.1.1", @@ -9005,7 +9040,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "agent-base": { "version": "6.0.2", @@ -9254,6 +9290,27 @@ "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=" }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -9365,7 +9422,8 @@ "bootstrap": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.1.tgz", - "integrity": "sha512-/jUa4sSuDZWlDLQ1gwQQR8uoYSvLJzDd8m5o6bPKh3asLAMYVZKdRCjb1joUd5WXf0WwCNzd2EjwQQhupou0dA==" + "integrity": "sha512-/jUa4sSuDZWlDLQ1gwQQR8uoYSvLJzDd8m5o6bPKh3asLAMYVZKdRCjb1joUd5WXf0WwCNzd2EjwQQhupou0dA==", + "requires": {} }, "brace-expansion": { "version": "1.1.11", @@ -9464,9 +9522,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001259", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001259.tgz", - "integrity": "sha512-V7mQTFhjITxuk9zBpI6nYsiTXhcPe05l+364nZjK7MFK/E7ibvYBSAXr4YcA6oPR8j3ZLM/LN+lUqUVAQEUZFg==", + "version": "1.0.30001257", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz", + "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==", "dev": true }, "caseless": { @@ -9511,7 +9569,8 @@ "chartjs-adapter-dayjs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs/-/chartjs-adapter-dayjs-1.0.0.tgz", - "integrity": "sha512-EnbVqTJGFKLpg1TROLdCEufrzbmIa2oeLGx8O2Wdjw2EoMudoOo9+YFu+6CM0Z0hQ/v3yq/e/Y6efQMu22n8Jg==" + "integrity": "sha512-EnbVqTJGFKLpg1TROLdCEufrzbmIa2oeLGx8O2Wdjw2EoMudoOo9+YFu+6CM0Z0hQ/v3yq/e/Y6efQMu22n8Jg==", + "requires": {} }, "chokidar": { "version": "3.5.2", @@ -9689,9 +9748,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-js": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.0.tgz", - "integrity": "sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", + "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==", "dev": true }, "core-util-is": { @@ -9921,9 +9980,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.845", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.845.tgz", - "integrity": "sha512-y0RorqmExFDI4RjLEC6j365bIT5UAXf9WIRcknvSFHVhbC/dRnCgJnPA3DUUW6SCC85QGKEafgqcHJ6uPdEP1Q==", + "version": "1.3.840", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.840.tgz", + "integrity": "sha512-yRoUmTLDJnkIJx23xLY7GbSvnmDCq++NSuxHDQ0jiyDJ9YZBUGJcrdUqm+ZwZFzMbCciVzfem2N2AWiHJcWlbw==", "dev": true }, "emoji-regex": { @@ -10115,9 +10174,9 @@ } }, "eslint-plugin-vue": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.18.0.tgz", - "integrity": "sha512-ceDXlXYMMPMSXw7tdKUR42w9jlzthJGJ3Kvm3YrZ0zuQfvAySNxe8sm6VHuksBW0+060GzYXhHJG6IHVOfF83Q==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.17.0.tgz", + "integrity": "sha512-Rq5R2QetDCgC+kBFQw1+aJ5B93tQ4xqZvoCUxuIzwTonngNArsdP8ChM8PowIzsJvRtWl4ltGh/bZcN3xhFWSw==", "dev": true, "requires": { "eslint-utils": "^2.1.0", @@ -10851,7 +10910,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -11363,9 +11423,9 @@ } }, "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", + "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", "dev": true }, "mathml-tag-names": { @@ -11602,12 +11662,9 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, "node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-BXSmNTLLDHT0UjQDg5E23x+0n/hPDjySqc0ELE4NpCa2wE5qmmaEWFRP/+v8pfuocchR9l5vFLbSB7CPE2ahvQ==", - "requires": { - "whatwg-url": "^5.0.0" - } + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==" }, "node-gyp": { "version": "7.1.2", @@ -11654,9 +11711,9 @@ } }, "node-releases": { - "version": "1.1.76", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.76.tgz", - "integrity": "sha512-9/IECtNr8dXNmPWmFXepT0/7o5eolGesHUa3mtr0KlgnCvnZxwh2qensKL42JJY2vQKC3nIBXetFAqR+PW1CmA==", + "version": "1.1.75", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", + "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", "dev": true }, "nodemailer": { @@ -12068,7 +12125,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -12387,7 +12445,8 @@ "version": "0.36.2", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true + "dev": true, + "requires": {} }, "postcss-value-parser": { "version": "4.1.0", @@ -12626,9 +12685,9 @@ }, "dependencies": { "@types/node": { - "version": "14.17.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.17.tgz", - "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==" + "version": "14.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.16.tgz", + "integrity": "sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==" } } }, @@ -12810,9 +12869,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.0.tgz", - "integrity": "sha512-kcjxsemgaOnfl43oZgO/IePLvXQI0ZKzo0/xbCt6uyrg3FY/FF8hVK9YoO8GiZBcEG2Ebl79EKnUc+aiE4f2Vw==", + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.41.0.tgz", + "integrity": "sha512-wb8nT60cjo9ZZMcHzG7TzdbFtCAmHEKWrH+zAdScPb4ZxL64WQBnGdbp5nwlenW5wJPcHva1JWmVa0h6iqA5eg==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0" @@ -12976,6 +13035,11 @@ "debug": "~4.3.1" } }, + "sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13311,7 +13375,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-5.0.0.tgz", "integrity": "sha512-c8aubuARSu5A3vEHLBeOSJt1udOdS+1iue7BmJDTSXoCBmfEQmmWX+59vYIj3NQdJBY6a/QRv1ozVFpaB9jaqA==", - "dev": true + "dev": true, + "requires": {} }, "stylelint-config-standard": { "version": "22.0.0", @@ -13577,11 +13642,6 @@ "punycode": "^2.1.1" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, "trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -13802,9 +13862,9 @@ } }, "vite": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", - "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.7.tgz", + "integrity": "sha512-hyUoWmRPhjN1aI+ZSBqDINKdIq7aokHE2ZXiztOg4YlmtpeQtMwMeyxv6X9YxHZmvGzg/js/eATM9Z1nwyakxg==", "dev": true, "requires": { "esbuild": "^0.12.17", @@ -13815,13 +13875,13 @@ } }, "vue": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.12.tgz", - "integrity": "sha512-VV14HtubmB56uuQaSvLkJZgoocPiN8CJI3zZA9y8h7q/Z5hcknDIFkbq5d8ku0ukZ6AJPQqMsZWcq0qryF0jgg==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.11.tgz", + "integrity": "sha512-JkI3/eIgfk4E0f/p319TD3EZgOwBQfftgnkRsXlT7OrRyyiyoyUXn6embPGZXSBxD3LoZ9SWhJoxLhFh5AleeA==", "requires": { - "@vue/compiler-dom": "3.2.12", - "@vue/runtime-dom": "3.2.12", - "@vue/shared": "3.2.12" + "@vue/compiler-dom": "3.2.11", + "@vue/runtime-dom": "3.2.11", + "@vue/shared": "3.2.11" } }, "vue-chart-3": { @@ -13840,12 +13900,20 @@ "vue-confirm-dialog": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/vue-confirm-dialog/-/vue-confirm-dialog-1.0.2.tgz", - "integrity": "sha512-gTo1bMDWOLd/6ihmWv8VlPxhc9QaKoE5YqlsKydUOfrrQ3Q3taljF6yI+1TMtAtJLrvZ8DYrePhgBhY1VCJzbQ==" + "integrity": "sha512-gTo1bMDWOLd/6ihmWv8VlPxhc9QaKoE5YqlsKydUOfrrQ3Q3taljF6yI+1TMtAtJLrvZ8DYrePhgBhY1VCJzbQ==", + "requires": {} + }, + "vue-contenteditable": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/vue-contenteditable/-/vue-contenteditable-3.0.4.tgz", + "integrity": "sha512-CmtqT4zHQwLoJEyNVaXUjjUFPUVYlXXBHfSbRCHCUjODMqrn6L293LM1nc1ELx8epitZZvecTfIqOLlSzB3d+w==", + "requires": {} }, "vue-demi": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz", - "integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA==" + "integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA==", + "requires": {} }, "vue-eslint-parser": { "version": "7.11.0", @@ -13892,6 +13960,14 @@ "@vue/devtools-api": "^6.0.0-beta.7" } }, + "vue-image-crop-upload": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vue-image-crop-upload/-/vue-image-crop-upload-3.0.3.tgz", + "integrity": "sha512-VeBsU0oI1hXeCvdpnu19DM/r3KTlI8SUXTxsHsU4MhDXR0ahRziiL9tf4FbILGx+gRVNZhGbl32yuM6TiaGNhA==", + "requires": { + "babel-runtime": "^6.11.6" + } + }, "vue-multiselect": { "version": "3.0.0-alpha.2", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.0.0-alpha.2.tgz", @@ -13909,7 +13985,8 @@ "vue-demi": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.11.4.tgz", - "integrity": "sha512-/3xFwzSykLW2HiiLie43a+FFgNOcokbBJ+fzvFXd0r2T8MYohqvphUyDQ8lbAwzQ3Dlcrb1c9ykifGkhSIAk6A==" + "integrity": "sha512-/3xFwzSykLW2HiiLie43a+FFgNOcokbBJ+fzvFXd0r2T8MYohqvphUyDQ8lbAwzQ3Dlcrb1c9ykifGkhSIAk6A==", + "requires": {} } } }, @@ -13924,20 +14001,15 @@ "vue-toastification": { "version": "2.0.0-rc.1", "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.1.tgz", - "integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==", + "requires": {} }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "sortablejs": "1.14.0" } }, "which": { @@ -14049,7 +14121,8 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} }, "xmlhttprequest-ssl": { "version": "2.0.0", diff --git a/package.json b/package.json index 4452a88c..da332d3f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "start": "npm run start-server", "start-server": "node server/server.js", "build": "vite build", + "tsc": "tsc", "vite-preview-dist": "vite preview --host", "build-docker": "npm run build-docker-debian && npm run build-docker-alpine", "build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.6.0-alpine --target release . --push", @@ -37,7 +38,7 @@ "test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", "test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .", "simple-dns-server": "node extra/simple-dns-server.js", - "update-language-files_old": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", + "update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix" }, "dependencies": { @@ -71,17 +72,20 @@ "socket.io": "^4.2.0", "socket.io-client": "^4.2.0", "tcp-ping": "^0.1.1", - "timezones-list": "^3.0.1", "thirty-two": "^1.0.2", + "timezones-list": "^3.0.1", "v-pagination-3": "^0.1.6", - "vue": "^3.2.8", + "vue": "next", "vue-chart-3": "^0.5.8", "vue-confirm-dialog": "^1.0.2", + "vue-contenteditable": "^3.0.4", "vue-i18n": "^9.1.7", + "vue-image-crop-upload": "^3.0.3", "vue-multiselect": "^3.0.0-alpha.2", "vue-qrcode": "^1.0.0", "vue-router": "^4.0.11", - "vue-toastification": "^2.0.0-rc.1" + "vue-toastification": "^2.0.0-rc.1", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@babel/eslint-parser": "^7.15.4", @@ -93,7 +97,7 @@ "dns2": "^2.0.1", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.17.0", - "sass": "^1.39.2", + "sass": "^1.41.0", "stylelint": "^13.13.1", "stylelint-config-standard": "^22.0.0", "typescript": "^4.4.3", diff --git a/server/check-version.js b/server/check-version.js index 96e8aecf..3ac2eee4 100644 --- a/server/check-version.js +++ b/server/check-version.js @@ -18,7 +18,7 @@ exports.startInterval = () => { // For debug if (process.env.TEST_CHECK_VERSION === "1") { - res.data.version = "1000.0.0" + res.data.version = "1000.0.0"; } exports.latestVersion = res.data.version; diff --git a/server/database.js b/server/database.js index 2058f028..2f6c1c5f 100644 --- a/server/database.js +++ b/server/database.js @@ -5,10 +5,23 @@ const { debug, sleep } = require("../src/util"); const dayjs = require("dayjs"); const knex = require("knex"); +/** + * Database & App Data Folder + */ class Database { static templatePath = "./db/kuma.db"; + + /** + * Data Dir (Default: ./data) + */ static dataDir; + + /** + * User Upload Dir (Default: ./data/upload) + */ + static uploadDir; + static path; /** @@ -33,6 +46,8 @@ class Database { "patch-improve-performance.sql": true, "patch-2fa.sql": true, "patch-add-retry-interval-monitor.sql": true, + "patch-incident-table.sql": true, + "patch-group-table.sql": true, } /** @@ -50,6 +65,13 @@ class Database { if (! fs.existsSync(Database.dataDir)) { fs.mkdirSync(Database.dataDir, { recursive: true }); } + + Database.uploadDir = Database.dataDir + "upload/"; + + if (! fs.existsSync(Database.uploadDir)) { + fs.mkdirSync(Database.uploadDir, { recursive: true }); + } + console.log(`Data Dir: ${Database.dataDir}`); } @@ -82,7 +104,7 @@ class Database { } // Auto map the model to a bean object - R.freeze(true) + R.freeze(true); await R.autoloadModels("./server/model"); // Change to WAL @@ -110,7 +132,7 @@ class Database { } else if (version > this.latestVersion) { console.info("Warning: Database version is newer than expected"); } else { - console.info("Database patch is needed") + console.info("Database patch is needed"); this.backup(version); @@ -125,11 +147,12 @@ class Database { } } catch (ex) { await Database.close(); - this.restore(); - console.error(ex) - console.error("Start Uptime-Kuma failed due to patch db failed") - console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues") + console.error(ex); + console.error("Start Uptime-Kuma failed due to patch db failed"); + console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues"); + + this.restore(); process.exit(1); } } @@ -154,7 +177,7 @@ class Database { try { for (let sqlFilename in this.patchList) { - await this.patch2Recursion(sqlFilename, databasePatchedFiles) + await this.patch2Recursion(sqlFilename, databasePatchedFiles); } if (this.patched) { @@ -163,11 +186,13 @@ class Database { } catch (ex) { await Database.close(); - this.restore(); - console.error(ex) + console.error(ex); console.error("Start Uptime-Kuma failed due to patch db failed"); console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues"); + + this.restore(); + process.exit(1); } @@ -207,7 +232,7 @@ class Database { console.log(sqlFilename + " is patched successfully"); } else { - console.log(sqlFilename + " is already patched, skip"); + debug(sqlFilename + " is already patched, skip"); } } @@ -225,12 +250,12 @@ class Database { // Remove all comments (--) let lines = text.split("\n"); lines = lines.filter((line) => { - return ! line.startsWith("--") + return ! line.startsWith("--"); }); // Split statements by semicolon // Filter out empty line - text = lines.join("\n") + text = lines.join("\n"); let statements = text.split(";") .map((statement) => { @@ -238,7 +263,7 @@ class Database { }) .filter((statement) => { return statement !== ""; - }) + }); for (let statement of statements) { await R.exec(statement); @@ -284,7 +309,7 @@ class Database { */ static backup(version) { if (! this.backupPath) { - console.info("Backup the db") + console.info("Backup the db"); this.backupPath = this.dataDir + "kuma.db.bak" + version; fs.copyFileSync(Database.path, this.backupPath); diff --git a/server/image-data-uri.js b/server/image-data-uri.js new file mode 100644 index 00000000..3ccaab7d --- /dev/null +++ b/server/image-data-uri.js @@ -0,0 +1,57 @@ +/* + From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js + Modified with 0 dependencies + */ +let fs = require("fs"); + +let ImageDataURI = (() => { + + function decode(dataURI) { + if (!/data:image\//.test(dataURI)) { + console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); + return null; + } + + let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); + return { + imageType: regExMatches[1], + dataBase64: regExMatches[2], + dataBuffer: new Buffer(regExMatches[2], "base64") + }; + } + + function encode(data, mediaType) { + if (!data || !mediaType) { + console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); + return null; + } + + mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; + let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); + let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; + + return dataImgBase64; + } + + function outputFile(dataURI, filePath) { + filePath = filePath || "./"; + return new Promise((resolve, reject) => { + let imageDecoded = decode(dataURI); + + fs.writeFile(filePath, imageDecoded.dataBuffer, err => { + if (err) { + return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); + } + resolve(filePath); + }); + }); + } + + return { + decode: decode, + encode: encode, + outputFile: outputFile, + }; +})(); + +module.exports = ImageDataURI; diff --git a/server/model/group.js b/server/model/group.js new file mode 100644 index 00000000..567f3865 --- /dev/null +++ b/server/model/group.js @@ -0,0 +1,34 @@ +const { BeanModel } = require("redbean-node/dist/bean-model"); +const { R } = require("redbean-node"); + +class Group extends BeanModel { + + async toPublicJSON() { + let monitorBeanList = await this.getMonitorList(); + let monitorList = []; + + for (let bean of monitorBeanList) { + monitorList.push(await bean.toPublicJSON()); + } + + return { + id: this.id, + name: this.name, + weight: this.weight, + monitorList, + }; + } + + async getMonitorList() { + return R.convertToBeans("monitor", await R.getAll(` + SELECT monitor.* FROM monitor, monitor_group + WHERE monitor.id = monitor_group.monitor_id + AND group_id = ? + ORDER BY monitor_group.weight + `, [ + this.id, + ])); + } +} + +module.exports = Group; diff --git a/server/model/heartbeat.js b/server/model/heartbeat.js index 54679414..e0a77c06 100644 --- a/server/model/heartbeat.js +++ b/server/model/heartbeat.js @@ -1,8 +1,8 @@ const dayjs = require("dayjs"); -const utc = require("dayjs/plugin/utc") -let timezone = require("dayjs/plugin/timezone") -dayjs.extend(utc) -dayjs.extend(timezone) +const utc = require("dayjs/plugin/utc"); +let timezone = require("dayjs/plugin/timezone"); +dayjs.extend(utc); +dayjs.extend(timezone); const { BeanModel } = require("redbean-node/dist/bean-model"); /** @@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model"); */ class Heartbeat extends BeanModel { + toPublicJSON() { + return { + status: this.status, + time: this.time, + msg: "", // Hide for public + ping: this.ping, + }; + } + toJSON() { return { monitorID: this.monitor_id, diff --git a/server/model/incident.js b/server/model/incident.js new file mode 100644 index 00000000..89c117e9 --- /dev/null +++ b/server/model/incident.js @@ -0,0 +1,18 @@ +const { BeanModel } = require("redbean-node/dist/bean-model"); + +class Incident extends BeanModel { + + toPublicJSON() { + return { + id: this.id, + style: this.style, + title: this.title, + content: this.content, + pin: this.pin, + createdDate: this.createdDate, + lastUpdatedDate: this.lastUpdatedDate, + }; + } +} + +module.exports = Incident; diff --git a/server/model/monitor.js b/server/model/monitor.js index c574df77..9a80225e 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,16 +1,16 @@ const https = require("https"); const dayjs = require("dayjs"); -const utc = require("dayjs/plugin/utc") -let timezone = require("dayjs/plugin/timezone") -dayjs.extend(utc) -dayjs.extend(timezone) +const utc = require("dayjs/plugin/utc"); +let timezone = require("dayjs/plugin/timezone"); +dayjs.extend(utc); +dayjs.extend(timezone); const axios = require("axios"); const { Prometheus } = require("../prometheus"); const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); -const { Notification } = require("../notification") +const { Notification } = require("../notification"); const version = require("../../package.json").version; /** @@ -20,13 +20,28 @@ const version = require("../../package.json").version; * 2 = PENDING */ class Monitor extends BeanModel { + + /** + * Return a object that ready to parse to JSON for public + * Only show necessary data to public + */ + async toPublicJSON() { + return { + id: this.id, + name: this.name, + }; + } + + /** + * Return a object that ready to parse to JSON + */ async toJSON() { let notificationIDList = {}; let list = await R.find("monitor_notification", " monitor_id = ? ", [ this.id, - ]) + ]); for (let bean of list) { notificationIDList[bean.notification_id] = true; @@ -64,7 +79,7 @@ class Monitor extends BeanModel { * @returns {boolean} */ getIgnoreTls() { - return Boolean(this.ignoreTls) + return Boolean(this.ignoreTls); } /** @@ -94,12 +109,12 @@ class Monitor extends BeanModel { if (! previousBeat) { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ this.id, - ]) + ]); } const isFirstBeat = !previousBeat; - let bean = R.dispense("heartbeat") + let bean = R.dispense("heartbeat"); bean.monitor_id = this.id; bean.time = R.isoDateTime(dayjs.utc()); bean.status = DOWN; @@ -135,7 +150,7 @@ class Monitor extends BeanModel { return checkStatusCode(status, this.getAcceptedStatuscodes()); }, }); - bean.msg = `${res.status} - ${res.statusText}` + bean.msg = `${res.status} - ${res.statusText}`; bean.ping = dayjs().valueOf() - startTime; // Check certificate if https is used @@ -145,12 +160,12 @@ class Monitor extends BeanModel { tlsInfo = await this.updateTlsInfo(checkCertificate(res)); } catch (e) { if (e.message !== "No TLS certificate in response") { - console.error(e.message) + console.error(e.message); } } } - debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms") + debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); if (this.type === "http") { bean.status = UP; @@ -160,26 +175,26 @@ class Monitor extends BeanModel { // Convert to string for object/array if (typeof data !== "string") { - data = JSON.stringify(data) + data = JSON.stringify(data); } if (data.includes(this.keyword)) { - bean.msg += ", keyword is found" + bean.msg += ", keyword is found"; bean.status = UP; } else { - throw new Error(bean.msg + ", but keyword is not found") + throw new Error(bean.msg + ", but keyword is not found"); } } } else if (this.type === "port") { bean.ping = await tcping(this.hostname, this.port); - bean.msg = "" + bean.msg = ""; bean.status = UP; } else if (this.type === "ping") { bean.ping = await ping(this.hostname); - bean.msg = "" + bean.msg = ""; bean.status = UP; } else if (this.type === "dns") { let startTime = dayjs().valueOf(); @@ -199,7 +214,7 @@ class Monitor extends BeanModel { dnsRes.forEach(record => { dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `; }); - dnsMessage = dnsMessage.slice(0, -2) + dnsMessage = dnsMessage.slice(0, -2); } else if (this.dns_resolve_type == "NS") { dnsMessage += "Servers: "; dnsMessage += dnsRes.join(" | "); @@ -209,7 +224,7 @@ class Monitor extends BeanModel { dnsRes.forEach(record => { dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `; }); - dnsMessage = dnsMessage.slice(0, -2) + dnsMessage = dnsMessage.slice(0, -2); } if (this.dnsLastResult !== dnsMessage) { @@ -272,20 +287,20 @@ class Monitor extends BeanModel { if (!isFirstBeat || bean.status === DOWN) { let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ this.id, - ]) + ]); let text; if (bean.status === UP) { - text = "✅ Up" + text = "✅ Up"; } else { - text = "🔴 Down" + text = "🔴 Down"; } let msg = `[${this.name}] [${text}] ${bean.msg}`; for (let notification of notificationList) { try { - await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()) + await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()); } catch (e) { console.error("Cannot send notification to " + notification.name); console.log(e); @@ -300,18 +315,18 @@ class Monitor extends BeanModel { let beatInterval = this.interval; if (bean.status === UP) { - console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`) + console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`); } else if (bean.status === PENDING) { if (this.retryInterval !== this.interval) { beatInterval = this.retryInterval; } - console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`) + console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`); } else { - console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`) + console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`); } io.to(this.user_id).emit("heartbeat", bean.toJSON()); - Monitor.sendStats(io, this.id, this.user_id) + Monitor.sendStats(io, this.id, this.user_id); await R.store(bean); prometheus.update(bean, tlsInfo); @@ -322,7 +337,7 @@ class Monitor extends BeanModel { this.heartbeatInterval = setTimeout(beat, beatInterval * 1000); } - } + }; beat(); } @@ -415,7 +430,7 @@ class Monitor extends BeanModel { * https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime * @param duration : int Hours */ - static async sendUptime(duration, io, monitorID, userID) { + static async calcUptime(duration, monitorID) { const timeLogger = new TimeLogger(); const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour")); @@ -468,12 +483,21 @@ class Monitor extends BeanModel { } else { // Handle new monitor with only one beat, because the beat's duration = 0 let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ])); - console.log("here???" + status); + if (status === UP) { uptime = 1; } } + return uptime; + } + + /** + * Send Uptime + * @param duration : int Hours + */ + static async sendUptime(duration, io, monitorID, userID) { + const uptime = await this.calcUptime(duration, monitorID); io.to(userID).emit("uptime", monitorID, duration, uptime); } } diff --git a/server/modules/apicache/apicache.js b/server/modules/apicache/apicache.js new file mode 100644 index 00000000..22d1fed7 --- /dev/null +++ b/server/modules/apicache/apicache.js @@ -0,0 +1,749 @@ +let url = require("url"); +let MemoryCache = require("./memory-cache"); + +let t = { + ms: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 3600000 * 24, + week: 3600000 * 24 * 7, + month: 3600000 * 24 * 30, +}; + +let instances = []; + +let matches = function (a) { + return function (b) { + return a === b; + }; +}; + +let doesntMatch = function (a) { + return function (b) { + return !matches(a)(b); + }; +}; + +let logDuration = function (d, prefix) { + let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms"; + return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m"; +}; + +function getSafeHeaders(res) { + return res.getHeaders ? res.getHeaders() : res._headers; +} + +function ApiCache() { + let memCache = new MemoryCache(); + + let globalOptions = { + debug: false, + defaultDuration: 3600000, + enabled: true, + appendKey: [], + jsonp: false, + redisClient: false, + headerBlacklist: [], + statusCodes: { + include: [], + exclude: [], + }, + events: { + expire: undefined, + }, + headers: { + // 'cache-control': 'no-cache' // example of header overwrite + }, + trackPerformance: false, + respectCacheControl: false, + }; + + let middlewareOptions = []; + let instance = this; + let index = null; + let timers = {}; + let performanceArray = []; // for tracking cache hit rate + + instances.push(this); + this.id = instances.length; + + function debug(a, b, c, d) { + let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { + return arg !== undefined; + }); + let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1; + + return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); + } + + function shouldCacheResponse(request, response, toggle) { + let opt = globalOptions; + let codes = opt.statusCodes; + + if (!response) { + return false; + } + + if (toggle && !toggle(request, response)) { + return false; + } + + if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) { + return false; + } + if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) { + return false; + } + + return true; + } + + function addIndexEntries(key, req) { + let groupName = req.apicacheGroup; + + if (groupName) { + debug("group detected \"" + groupName + "\""); + let group = (index.groups[groupName] = index.groups[groupName] || []); + group.unshift(key); + } + + index.all.unshift(key); + } + + function filterBlacklistedHeaders(headers) { + return Object.keys(headers) + .filter(function (key) { + return globalOptions.headerBlacklist.indexOf(key) === -1; + }) + .reduce(function (acc, header) { + acc[header] = headers[header]; + return acc; + }, {}); + } + + function createCacheObject(status, headers, data, encoding) { + return { + status: status, + headers: filterBlacklistedHeaders(headers), + data: data, + encoding: encoding, + timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses. + }; + } + + function cacheResponse(key, value, duration) { + let redis = globalOptions.redisClient; + let expireCallback = globalOptions.events.expire; + + if (redis && redis.connected) { + try { + redis.hset(key, "response", JSON.stringify(value)); + redis.hset(key, "duration", duration); + redis.expire(key, duration / 1000, expireCallback || function () {}); + } catch (err) { + debug("[apicache] error in redis.hset()"); + } + } else { + memCache.add(key, value, duration, expireCallback); + } + + // add automatic cache clearing from duration, includes max limit on setTimeout + timers[key] = setTimeout(function () { + instance.clear(key, true); + }, Math.min(duration, 2147483647)); + } + + function accumulateContent(res, content) { + if (content) { + if (typeof content == "string") { + res._apicache.content = (res._apicache.content || "") + content; + } else if (Buffer.isBuffer(content)) { + let oldContent = res._apicache.content; + + if (typeof oldContent === "string") { + oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent); + } + + if (!oldContent) { + oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0); + } + + res._apicache.content = Buffer.concat( + [oldContent, content], + oldContent.length + content.length + ); + } else { + res._apicache.content = content; + } + } + } + + function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { + // monkeypatch res.end to create cache object + res._apicache = { + write: res.write, + writeHead: res.writeHead, + end: res.end, + cacheable: true, + content: undefined, + }; + + // append header overwrites if applicable + Object.keys(globalOptions.headers).forEach(function (name) { + res.setHeader(name, globalOptions.headers[name]); + }); + + res.writeHead = function () { + // add cache control headers + if (!globalOptions.headers["cache-control"]) { + if (shouldCacheResponse(req, res, toggle)) { + res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0)); + } else { + res.setHeader("cache-control", "no-cache, no-store, must-revalidate"); + } + } + + res._apicache.headers = Object.assign({}, getSafeHeaders(res)); + return res._apicache.writeHead.apply(this, arguments); + }; + + // patch res.write + res.write = function (content) { + accumulateContent(res, content); + return res._apicache.write.apply(this, arguments); + }; + + // patch res.end + res.end = function (content, encoding) { + if (shouldCacheResponse(req, res, toggle)) { + accumulateContent(res, content); + + if (res._apicache.cacheable && res._apicache.content) { + addIndexEntries(key, req); + let headers = res._apicache.headers || getSafeHeaders(res); + let cacheObject = createCacheObject( + res.statusCode, + headers, + res._apicache.content, + encoding + ); + cacheResponse(key, cacheObject, duration); + + // display log entry + let elapsed = new Date() - req.apicacheTimer; + debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed)); + debug("_apicache.headers: ", res._apicache.headers); + debug("res.getHeaders(): ", getSafeHeaders(res)); + debug("cacheObject: ", cacheObject); + } + } + + return res._apicache.end.apply(this, arguments); + }; + + next(); + } + + function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { + if (toggle && !toggle(request, response)) { + return next(); + } + + let headers = getSafeHeaders(response); + + // Modified by @louislam, removed Cache-control, since I don't need client side cache! + // Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254 + Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {})); + + // only embed apicache headers when not in production environment + if (process.env.NODE_ENV !== "production") { + Object.assign(headers, { + "apicache-store": globalOptions.redisClient ? "redis" : "memory", + "apicache-version": "1.6.2-modified", + }); + } + + // unstringify buffers + let data = cacheObject.data; + if (data && data.type === "Buffer") { + data = + typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data); + } + + // test Etag against If-None-Match for 304 + let cachedEtag = cacheObject.headers.etag; + let requestEtag = request.headers["if-none-match"]; + + if (requestEtag && cachedEtag === requestEtag) { + response.writeHead(304, headers); + return response.end(); + } + + response.writeHead(cacheObject.status || 200, headers); + + return response.end(data, cacheObject.encoding); + } + + function syncOptions() { + for (let i in middlewareOptions) { + Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions); + } + } + + this.clear = function (target, isAutomatic) { + let group = index.groups[target]; + let redis = globalOptions.redisClient; + + if (group) { + debug("clearing group \"" + target + "\""); + + group.forEach(function (key) { + debug("clearing cached entry for \"" + key + "\""); + clearTimeout(timers[key]); + delete timers[key]; + if (!globalOptions.redisClient) { + memCache.delete(key); + } else { + try { + redis.del(key); + } catch (err) { + console.log("[apicache] error in redis.del(\"" + key + "\")"); + } + } + index.all = index.all.filter(doesntMatch(key)); + }); + + delete index.groups[target]; + } else if (target) { + debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\""); + clearTimeout(timers[target]); + delete timers[target]; + // clear actual cached entry + if (!redis) { + memCache.delete(target); + } else { + try { + redis.del(target); + } catch (err) { + console.log("[apicache] error in redis.del(\"" + target + "\")"); + } + } + + // remove from global index + index.all = index.all.filter(doesntMatch(target)); + + // remove target from each group that it may exist in + Object.keys(index.groups).forEach(function (groupName) { + index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target)); + + // delete group if now empty + if (!index.groups[groupName].length) { + delete index.groups[groupName]; + } + }); + } else { + debug("clearing entire index"); + + if (!redis) { + memCache.clear(); + } else { + // clear redis keys one by one from internal index to prevent clearing non-apicache entries + index.all.forEach(function (key) { + clearTimeout(timers[key]); + delete timers[key]; + try { + redis.del(key); + } catch (err) { + console.log("[apicache] error in redis.del(\"" + key + "\")"); + } + }); + } + this.resetIndex(); + } + + return this.getIndex(); + }; + + function parseDuration(duration, defaultDuration) { + if (typeof duration === "number") { + return duration; + } + + if (typeof duration === "string") { + let split = duration.match(/^([\d\.,]+)\s?(\w+)$/); + + if (split.length === 3) { + let len = parseFloat(split[1]); + let unit = split[2].replace(/s$/i, "").toLowerCase(); + if (unit === "m") { + unit = "ms"; + } + + return (len || 1) * (t[unit] || 0); + } + } + + return defaultDuration; + } + + this.getDuration = function (duration) { + return parseDuration(duration, globalOptions.defaultDuration); + }; + + /** + * Return cache performance statistics (hit rate). Suitable for putting into a route: + * + * app.get('/api/cache/performance', (req, res) => { + * res.json(apicache.getPerformance()) + * }) + * + */ + this.getPerformance = function () { + return performanceArray.map(function (p) { + return p.report(); + }); + }; + + this.getIndex = function (group) { + if (group) { + return index.groups[group]; + } else { + return index; + } + }; + + this.middleware = function cache(strDuration, middlewareToggle, localOptions) { + let duration = instance.getDuration(strDuration); + let opt = {}; + + middlewareOptions.push({ + options: opt, + }); + + let options = function (localOptions) { + if (localOptions) { + middlewareOptions.find(function (middleware) { + return middleware.options === opt; + }).localOptions = localOptions; + } + + syncOptions(); + + return opt; + }; + + options(localOptions); + + /** + * A Function for non tracking performance + */ + function NOOPCachePerformance() { + this.report = this.hit = this.miss = function () {}; // noop; + } + + /** + * A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above. + */ + function CachePerformance() { + /** + * Tracks the hit rate for the last 100 requests. + * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened. + */ + this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits + + /** + * Tracks the hit rate for the last 1000 requests. + * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened. + */ + this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits + + /** + * Tracks the hit rate for the last 10000 requests. + * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened. + */ + this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits + + /** + * Tracks the hit rate for the last 100000 requests. + * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened. + */ + this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits + + /** + * The number of calls that have passed through the middleware since the server started. + */ + this.callCount = 0; + + /** + * The total number of hits since the server started + */ + this.hitCount = 0; + + /** + * The key from the last cache hit. This is useful in identifying which route these statistics apply to. + */ + this.lastCacheHit = null; + + /** + * The key from the last cache miss. This is useful in identifying which route these statistics apply to. + */ + this.lastCacheMiss = null; + + /** + * Return performance statistics + */ + this.report = function () { + return { + lastCacheHit: this.lastCacheHit, + lastCacheMiss: this.lastCacheMiss, + callCount: this.callCount, + hitCount: this.hitCount, + missCount: this.callCount - this.hitCount, + hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount, + hitRateLast100: this.hitRate(this.hitsLast100), + hitRateLast1000: this.hitRate(this.hitsLast1000), + hitRateLast10000: this.hitRate(this.hitsLast10000), + hitRateLast100000: this.hitRate(this.hitsLast100000), + }; + }; + + /** + * Computes a cache hit rate from an array of hits and misses. + * @param {Uint8Array} array An array representing hits and misses. + * @returns a number between 0 and 1, or null if the array has no hits or misses + */ + this.hitRate = function (array) { + let hits = 0; + let misses = 0; + for (let i = 0; i < array.length; i++) { + let n8 = array[i]; + for (let j = 0; j < 4; j++) { + switch (n8 & 3) { + case 1: + hits++; + break; + case 2: + misses++; + break; + } + n8 >>= 2; + } + } + let total = hits + misses; + if (total == 0) { + return null; + } + return hits / total; + }; + + /** + * Record a hit or miss in the given array. It will be recorded at a position determined + * by the current value of the callCount variable. + * @param {Uint8Array} array An array representing hits and misses. + * @param {boolean} hit true for a hit, false for a miss + * Each element in the array is 8 bits, and encodes 4 hit/miss records. + * Each hit or miss is encoded as to bits as follows: + * 00 means no hit or miss has been recorded in these bits + * 01 encodes a hit + * 10 encodes a miss + */ + this.recordHitInArray = function (array, hit) { + let arrayIndex = ~~(this.callCount / 4) % array.length; + let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element + let clearMask = ~(3 << bitOffset); + let record = (hit ? 1 : 2) << bitOffset; + array[arrayIndex] = (array[arrayIndex] & clearMask) | record; + }; + + /** + * Records the hit or miss in the tracking arrays and increments the call count. + * @param {boolean} hit true records a hit, false records a miss + */ + this.recordHit = function (hit) { + this.recordHitInArray(this.hitsLast100, hit); + this.recordHitInArray(this.hitsLast1000, hit); + this.recordHitInArray(this.hitsLast10000, hit); + this.recordHitInArray(this.hitsLast100000, hit); + if (hit) { + this.hitCount++; + } + this.callCount++; + }; + + /** + * Records a hit event, setting lastCacheMiss to the given key + * @param {string} key The key that had the cache hit + */ + this.hit = function (key) { + this.recordHit(true); + this.lastCacheHit = key; + }; + + /** + * Records a miss event, setting lastCacheMiss to the given key + * @param {string} key The key that had the cache miss + */ + this.miss = function (key) { + this.recordHit(false); + this.lastCacheMiss = key; + }; + } + + let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance(); + + performanceArray.push(perf); + + let cache = function (req, res, next) { + function bypass() { + debug("bypass detected, skipping cache."); + return next(); + } + + // initial bypass chances + if (!opt.enabled) { + return bypass(); + } + if ( + req.headers["x-apicache-bypass"] || + req.headers["x-apicache-force-fetch"] || + (opt.respectCacheControl && req.headers["cache-control"] == "no-cache") + ) { + return bypass(); + } + + // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER + // if (typeof middlewareToggle === 'function') { + // if (!middlewareToggle(req, res)) return bypass() + // } else if (middlewareToggle !== undefined && !middlewareToggle) { + // return bypass() + // } + + // embed timer + req.apicacheTimer = new Date(); + + // In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url + let key = req.originalUrl || req.url; + + // Remove querystring from key if jsonp option is enabled + if (opt.jsonp) { + key = url.parse(key).pathname; + } + + // add appendKey (either custom function or response path) + if (typeof opt.appendKey === "function") { + key += "$$appendKey=" + opt.appendKey(req, res); + } else if (opt.appendKey.length > 0) { + let appendKey = req; + + for (let i = 0; i < opt.appendKey.length; i++) { + appendKey = appendKey[opt.appendKey[i]]; + } + key += "$$appendKey=" + appendKey; + } + + // attempt cache hit + let redis = opt.redisClient; + let cached = !redis ? memCache.getValue(key) : null; + + // send if cache hit from memory-cache + if (cached) { + let elapsed = new Date() - req.apicacheTimer; + debug("sending cached (memory-cache) version of", key, logDuration(elapsed)); + + perf.hit(key); + return sendCachedResponse(req, res, cached, middlewareToggle, next, duration); + } + + // send if cache hit from redis + if (redis && redis.connected) { + try { + redis.hgetall(key, function (err, obj) { + if (!err && obj && obj.response) { + let elapsed = new Date() - req.apicacheTimer; + debug("sending cached (redis) version of", key, logDuration(elapsed)); + + perf.hit(key); + return sendCachedResponse( + req, + res, + JSON.parse(obj.response), + middlewareToggle, + next, + duration + ); + } else { + perf.miss(key); + return makeResponseCacheable( + req, + res, + next, + key, + duration, + strDuration, + middlewareToggle + ); + } + }); + } catch (err) { + // bypass redis on error + perf.miss(key); + return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); + } + } else { + perf.miss(key); + return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); + } + }; + + cache.options = options; + + return cache; + }; + + this.options = function (options) { + if (options) { + Object.assign(globalOptions, options); + syncOptions(); + + if ("defaultDuration" in options) { + // Convert the default duration to a number in milliseconds (if needed) + globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000); + } + + if (globalOptions.trackPerformance) { + debug("WARNING: using trackPerformance flag can cause high memory usage!"); + } + + return this; + } else { + return globalOptions; + } + }; + + this.resetIndex = function () { + index = { + all: [], + groups: {}, + }; + }; + + this.newInstance = function (config) { + let instance = new ApiCache(); + + if (config) { + instance.options(config); + } + + return instance; + }; + + this.clone = function () { + return this.newInstance(this.options()); + }; + + // initialize index + this.resetIndex(); +} + +module.exports = new ApiCache(); diff --git a/server/modules/apicache/index.js b/server/modules/apicache/index.js new file mode 100644 index 00000000..b8bb9b35 --- /dev/null +++ b/server/modules/apicache/index.js @@ -0,0 +1,14 @@ +const apicache = require("./apicache"); + +apicache.options({ + headerBlacklist: [ + "cache-control" + ], + headers: { + // Disable client side cache, only server side cache. + // BUG! Not working for the second request + "cache-control": "no-cache", + }, +}); + +module.exports = apicache; diff --git a/server/modules/apicache/memory-cache.js b/server/modules/apicache/memory-cache.js new file mode 100644 index 00000000..ad831e2e --- /dev/null +++ b/server/modules/apicache/memory-cache.js @@ -0,0 +1,59 @@ +function MemoryCache() { + this.cache = {}; + this.size = 0; +} + +MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { + let old = this.cache[key]; + let instance = this; + + let entry = { + value: value, + expire: time + Date.now(), + timeout: setTimeout(function () { + instance.delete(key); + return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); + }, time) + }; + + this.cache[key] = entry; + this.size = Object.keys(this.cache).length; + + return entry; +}; + +MemoryCache.prototype.delete = function (key) { + let entry = this.cache[key]; + + if (entry) { + clearTimeout(entry.timeout); + } + + delete this.cache[key]; + + this.size = Object.keys(this.cache).length; + + return null; +}; + +MemoryCache.prototype.get = function (key) { + let entry = this.cache[key]; + + return entry; +}; + +MemoryCache.prototype.getValue = function (key) { + let entry = this.get(key); + + return entry && entry.value; +}; + +MemoryCache.prototype.clear = function () { + Object.keys(this.cache).forEach(function (key) { + this.delete(key); + }, this); + + return true; +}; + +module.exports = MemoryCache; diff --git a/server/routers/api-router.js b/server/routers/api-router.js new file mode 100644 index 00000000..0940668f --- /dev/null +++ b/server/routers/api-router.js @@ -0,0 +1,150 @@ +let express = require("express"); +const { allowDevAllOrigin, getSettings, setting } = require("../util-server"); +const { R } = require("redbean-node"); +const server = require("../server"); +const apicache = require("../modules/apicache"); +const Monitor = require("../model/monitor"); +let router = express.Router(); + +let cache = apicache.middleware; + +router.get("/api/entry-page", async (_, response) => { + allowDevAllOrigin(response); + response.json(server.entryPage); +}); + +// Status Page Config +router.get("/api/status-page/config", async (_request, response) => { + allowDevAllOrigin(response); + + let config = await getSettings("statusPage"); + + if (! config.statusPageTheme) { + config.statusPageTheme = "light"; + } + + if (! config.statusPagePublished) { + config.statusPagePublished = true; + } + + if (! config.title) { + config.title = "Uptime Kuma"; + } + + response.json(config); +}); + +// Status Page - Get the current Incident +// Can fetch only if published +router.get("/api/status-page/incident", async (_, response) => { + allowDevAllOrigin(response); + + try { + await checkPublished(); + + let incident = await R.findOne("incident", " pin = 1 AND active = 1"); + + if (incident) { + incident = incident.toPublicJSON(); + } + + response.json({ + ok: true, + incident, + }); + + } catch (error) { + send403(response, error.message); + } +}); + +// Status Page - Monitor List +// Can fetch only if published +router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => { + allowDevAllOrigin(response); + + try { + await checkPublished(); + const publicGroupList = []; + let list = await R.find("group", " public = 1 ORDER BY weight "); + + for (let groupBean of list) { + publicGroupList.push(await groupBean.toPublicJSON()); + } + + response.json(publicGroupList); + + } catch (error) { + send403(response, error.message); + } +}); + +// Status Page Polling Data +// Can fetch only if published +router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => { + allowDevAllOrigin(response); + try { + await checkPublished(); + + let heartbeatList = {}; + let uptimeList = {}; + + let monitorIDList = await R.getCol(` + SELECT monitor_group.monitor_id FROM monitor_group, \`group\` + WHERE monitor_group.group_id = \`group\`.id + AND public = 1 + `); + + for (let monitorID of monitorIDList) { + let list = await R.getAll(` + SELECT * FROM heartbeat + WHERE monitor_id = ? + ORDER BY time DESC + LIMIT 50 + `, [ + monitorID, + ]); + + list = R.convertToBeans("heartbeat", list); + heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); + + const type = 24; + uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID); + } + + response.json({ + heartbeatList, + uptimeList + }); + + } catch (error) { + send403(response, error.message); + } +}); + +async function checkPublished() { + if (! await isPublished()) { + throw new Error("The status page is not published"); + } +} + +/** + * Default is published + * @returns {Promise} + */ +async function isPublished() { + const value = await setting("statusPagePublished"); + if (value === null) { + return true; + } + return value; +} + +function send403(res, msg = "") { + res.status(403).json({ + "status": "fail", + "msg": msg, + }); +} + +module.exports = router; diff --git a/server/server.js b/server/server.js index c23781ae..51095365 100644 --- a/server/server.js +++ b/server/server.js @@ -8,12 +8,12 @@ console.log("Node Env: " + process.env.NODE_ENV); const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util"); -console.log("Importing Node libraries") +console.log("Importing Node libraries"); const fs = require("fs"); const http = require("http"); const https = require("https"); -console.log("Importing 3rd-party libraries") +console.log("Importing 3rd-party libraries"); debug("Importing express"); const express = require("express"); debug("Importing socket.io"); @@ -35,7 +35,7 @@ console.log("Importing this project modules"); debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); -const { getSettings, setSettings, setting, initJWTSecret, genSecret } = require("./util-server"); +const { getSettings, setSettings, setting, initJWTSecret, genSecret, allowDevAllOrigin, checkLogin } = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); @@ -62,14 +62,15 @@ const port = parseInt(process.env.PORT || args.port || 3001); const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined; const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined; -// Demo Mode? -const demoMode = args["demo"] || false; - -if (demoMode) { - console.log("==== Demo Mode ===="); +// Data Directory (must be end with "/") +Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; +Database.path = Database.dataDir + "kuma.db"; +if (! fs.existsSync(Database.dataDir)) { + fs.mkdirSync(Database.dataDir, { recursive: true }); } +console.log(`Data Dir: ${Database.dataDir}`); -console.log("Creating express and socket.io instance") +console.log("Creating express and socket.io instance"); const app = express(); let server; @@ -90,6 +91,7 @@ module.exports.io = io; // Must be after io instantiation const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client"); +const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); app.use(express.json()); @@ -123,13 +125,19 @@ let needSetup = false; */ let indexHTML = fs.readFileSync("./dist/index.html").toString(); +exports.entryPage = "dashboard"; + (async () => { Database.init(args); await initDatabase(); - console.log("Adding route") + exports.entryPage = await setting("entryPage"); + + console.log("Adding route"); + // *************************** // Normal Router here + // *************************** // Robots.txt app.get("/robots.txt", async (_request, response) => { @@ -149,28 +157,39 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); app.use("/", express.static("dist")); + // ./data/upload + app.use("/upload", express.static(Database.uploadDir)); + app.get("/.well-known/change-password", async (_, response) => { response.redirect("https://github.com/louislam/uptime-kuma/wiki/Reset-Password-via-CLI"); }); - // Universal Route Handler, must be at the end + // API Router + const apiRouter = require("./routers/api-router"); + app.use(apiRouter); + + // Universal Route Handler, must be at the end of all express route. app.get("*", async (_request, response) => { - response.send(indexHTML); + if (_request.originalUrl.startsWith("/upload/")) { + response.status(404).send("File not found."); + } else { + response.send(indexHTML); + } }); - console.log("Adding socket handler") + console.log("Adding socket handler"); io.on("connection", async (socket) => { socket.emit("info", { version: checkVersion.version, latestVersion: checkVersion.latestVersion, - }) + }); totalClient++; if (needSetup) { - console.log("Redirect to setup page") - socket.emit("setup") + console.log("Redirect to setup page"); + socket.emit("setup"); } socket.on("disconnect", () => { @@ -178,7 +197,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); // *************************** - // Public API + // Public Socket API // *************************** socket.on("loginByToken", async (token, callback) => { @@ -186,44 +205,44 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); try { let decoded = jwt.verify(token, jwtSecret); - console.log("Username from JWT: " + decoded.username) + console.log("Username from JWT: " + decoded.username); let user = await R.findOne("user", " username = ? AND active = 1 ", [ decoded.username, - ]) + ]); if (user) { - debug("afterLogin") + debug("afterLogin"); - afterLogin(socket, user) + afterLogin(socket, user); - debug("afterLogin ok") + debug("afterLogin ok"); callback({ ok: true, - }) + }); } else { callback({ ok: false, msg: "The user is inactive or deleted.", - }) + }); } } catch (error) { callback({ ok: false, msg: "Invalid token.", - }) + }); } }); socket.on("login", async (data, callback) => { - console.log("Login") + console.log("Login"); - let user = await login(data.username, data.password) + let user = await login(data.username, data.password); if (user) { - afterLogin(socket, user) + afterLogin(socket, user); if (user.twofaStatus == 0) { callback({ @@ -231,13 +250,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); token: jwt.sign({ username: data.username, }, jwtSecret), - }) + }); } if (user.twofaStatus == 1 && !data.token) { callback({ tokenRequired: true, - }) + }); } if (data.token) { @@ -249,39 +268,39 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); token: jwt.sign({ username: data.username, }, jwtSecret), - }) + }); } else { callback({ ok: false, msg: "Invalid Token!", - }) + }); } } } else { callback({ ok: false, msg: "Incorrect username or password.", - }) + }); } }); socket.on("logout", async (callback) => { - socket.leave(socket.userID) + socket.leave(socket.userID); socket.userID = null; callback(); }); socket.on("prepare2FA", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); if (user.twofa_status == 0) { - let newSecret = await genSecret() + let newSecret = await genSecret(); let encodedSecret = base32.encode(newSecret); let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`; @@ -293,24 +312,24 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); callback({ ok: true, uri: uri, - }) + }); } else { callback({ ok: false, msg: "2FA is already enabled.", - }) + }); } } catch (error) { callback({ ok: false, msg: "Error while trying to prepare 2FA.", - }) + }); } }); socket.on("save2FA", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ socket.userID, @@ -319,18 +338,18 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); callback({ ok: true, msg: "2FA Enabled.", - }) + }); } catch (error) { callback({ ok: false, msg: "Error while trying to change 2FA.", - }) + }); } }); socket.on("disable2FA", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [ socket.userID, @@ -339,19 +358,19 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); callback({ ok: true, msg: "2FA Disabled.", - }) + }); } catch (error) { callback({ ok: false, msg: "Error while trying to change 2FA.", - }) + }); } }); socket.on("verifyToken", async (token, callback) => { let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); let verify = notp.totp.verify(token, user.twofa_secret); @@ -359,40 +378,40 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); callback({ ok: true, valid: true, - }) + }); } else { callback({ ok: false, msg: "Invalid Token.", valid: false, - }) + }); } }); socket.on("twoFAStatus", async (callback) => { - checkLogin(socket) + checkLogin(socket); try { let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); if (user.twofa_status == 1) { callback({ ok: true, status: true, - }) + }); } else { callback({ ok: true, status: false, - }) + }); } } catch (error) { callback({ ok: false, msg: "Error while trying to get 2FA status.", - }) + }); } }); @@ -403,13 +422,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("setup", async (username, password, callback) => { try { if ((await R.count("user")) !== 0) { - throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database.") + throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database."); } - let user = R.dispense("user") + let user = R.dispense("user"); user.username = username; - user.password = passwordHash.generate(password) - await R.store(user) + user.password = passwordHash.generate(password); + await R.store(user); needSetup = false; @@ -433,8 +452,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); // Add a new monitor socket.on("add", async (monitor, callback) => { try { - checkLogin(socket) - let bean = R.dispense("monitor") + checkLogin(socket); + let bean = R.dispense("monitor"); let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; @@ -442,11 +461,11 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); delete monitor.accepted_statuscodes; - bean.import(monitor) - bean.user_id = socket.userID - await R.store(bean) + bean.import(monitor); + bean.user_id = socket.userID; + await R.store(bean); - await updateMonitorNotification(bean.id, notificationIDList) + await updateMonitorNotification(bean.id, notificationIDList); await startMonitor(socket.userID, bean.id); await sendMonitorList(socket); @@ -468,18 +487,18 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); // Edit a monitor socket.on("editMonitor", async (monitor, callback) => { 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) { - throw new Error("Permission denied.") + throw new Error("Permission denied."); } - bean.name = monitor.name - bean.type = monitor.type - bean.url = monitor.url - bean.interval = monitor.interval + bean.name = monitor.name; + bean.type = monitor.type; + bean.url = monitor.url; + bean.interval = monitor.interval; bean.retryInterval = monitor.retryInterval; bean.hostname = monitor.hostname; bean.maxretries = monitor.maxretries; @@ -492,12 +511,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); bean.dns_resolve_type = monitor.dns_resolve_type; bean.dns_resolve_server = monitor.dns_resolve_server; - await R.store(bean) + await R.store(bean); - await updateMonitorNotification(bean.id, monitor.notificationIDList) + await updateMonitorNotification(bean.id, monitor.notificationIDList); if (bean.active) { - await restartMonitor(socket.userID, bean.id) + await restartMonitor(socket.userID, bean.id); } await sendMonitorList(socket); @@ -509,7 +528,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, msg: e.message, @@ -519,13 +538,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("getMonitorList", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); await sendMonitorList(socket); callback({ ok: true, }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, msg: e.message, @@ -535,14 +554,14 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("getMonitor", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`); let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [ monitorID, socket.userID, - ]) + ]); callback({ ok: true, @@ -560,7 +579,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); // Start or Resume the monitor socket.on("resumeMonitor", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); await startMonitor(socket.userID, monitorID); await sendMonitorList(socket); @@ -579,8 +598,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("pauseMonitor", async (monitorID, callback) => { try { - checkLogin(socket) - await pauseMonitor(socket.userID, monitorID) + checkLogin(socket); + await pauseMonitor(socket.userID, monitorID); await sendMonitorList(socket); callback({ @@ -598,13 +617,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("deleteMonitor", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`); if (monitorID in monitorList) { monitorList[monitorID].stop(); - delete monitorList[monitorID] + delete monitorList[monitorID]; } await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ @@ -629,9 +648,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("getTags", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); - const list = await R.findAll("tag") + const list = await R.findAll("tag"); callback({ ok: true, @@ -648,12 +667,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("addTag", async (tag, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let bean = R.dispense("tag") - bean.name = tag.name - bean.color = tag.color - await R.store(bean) + let bean = R.dispense("tag"); + bean.name = tag.name; + bean.color = tag.color; + await R.store(bean); callback({ ok: true, @@ -670,12 +689,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("editTag", async (tag, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]) - bean.name = tag.name - bean.color = tag.color - await R.store(bean) + let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]); + bean.name = tag.name; + bean.color = tag.color; + await R.store(bean); callback({ ok: true, @@ -692,9 +711,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("deleteTag", async (tagID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]) + await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]); callback({ ok: true, @@ -711,13 +730,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("addMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [ tagID, monitorID, value, - ]) + ]); callback({ ok: true, @@ -734,13 +753,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("editMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("UPDATE monitor_tag SET value = ? WHERE tag_id = ? AND monitor_id = ?", [ value, tagID, monitorID, - ]) + ]); callback({ ok: true, @@ -757,13 +776,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("deleteMonitorTag", async (tagID, monitorID, value, callback) => { try { - checkLogin(socket) + checkLogin(socket); await R.exec("DELETE FROM monitor_tag WHERE tag_id = ? AND monitor_id = ? AND value = ?", [ tagID, monitorID, value, - ]) + ]); // Cleanup unused Tags await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0"); @@ -783,15 +802,15 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("changePassword", async (password, callback) => { try { - checkLogin(socket) + checkLogin(socket); if (! password.currentPassword) { - throw new Error("Invalid new password") + throw new Error("Invalid new password"); } let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, - ]) + ]); if (user && passwordHash.verify(password.currentPassword, user.password)) { @@ -800,9 +819,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); callback({ ok: true, msg: "Password has been updated successfully.", - }) + }); } else { - throw new Error("Incorrect current password") + throw new Error("Incorrect current password"); } } catch (e) { @@ -815,7 +834,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("getSettings", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); callback({ ok: true, @@ -832,9 +851,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("setSettings", async (data, callback) => { try { - checkLogin(socket) + checkLogin(socket); - await setSettings("general", data) + await setSettings("general", data); + exports.entryPage = data.entryPage; callback({ ok: true, @@ -852,10 +872,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); // Add or Edit socket.on("addNotification", async (notification, notificationID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let notificationBean = await Notification.save(notification, notificationID, socket.userID) - await sendNotificationList(socket) + let notificationBean = await Notification.save(notification, notificationID, socket.userID); + await sendNotificationList(socket); callback({ ok: true, @@ -873,10 +893,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("deleteNotification", async (notificationID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - await Notification.delete(notificationID, socket.userID) - await sendNotificationList(socket) + await Notification.delete(notificationID, socket.userID); + await sendNotificationList(socket); callback({ ok: true, @@ -893,9 +913,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("testNotification", async (notification, callback) => { try { - checkLogin(socket) + checkLogin(socket); - let msg = await Notification.send(notification, notification.name + " Testing") + let msg = await Notification.send(notification, notification.name + " Testing"); callback({ ok: true, @@ -903,7 +923,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, @@ -914,7 +934,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("checkApprise", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); callback(Notification.checkApprise()); } catch (e) { callback(false); @@ -923,19 +943,19 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("uploadBackup", async (uploadedJSON, importHandle, callback) => { try { - checkLogin(socket) + checkLogin(socket); let backupData = JSON.parse(uploadedJSON); - console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`) + console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`); let notificationListData = backupData.notificationList; let monitorListData = backupData.monitorList; if (importHandle == "overwrite") { for (let id in monitorList) { - let monitor = monitorList[id] - await monitor.stop() + let monitor = monitorList[id]; + await monitor.stop(); } await R.exec("DELETE FROM heartbeat"); await R.exec("DELETE FROM monitor_notification"); @@ -952,7 +972,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") { let notification = JSON.parse(notificationListData[i].config); - await Notification.save(notification, null, socket.userID) + await Notification.save(notification, null, socket.userID); } } @@ -981,9 +1001,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); dns_resolve_type: monitorListData[i].dns_resolve_type, dns_resolve_server: monitorListData[i].dns_resolve_server, notificationIDList: {}, - } + }; - let bean = R.dispense("monitor") + let bean = R.dispense("monitor"); let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; @@ -991,11 +1011,11 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); delete monitor.accepted_statuscodes; - bean.import(monitor) - bean.user_id = socket.userID - await R.store(bean) + bean.import(monitor); + bean.user_id = socket.userID; + await R.store(bean); - await updateMonitorNotification(bean.id, notificationIDList) + await updateMonitorNotification(bean.id, notificationIDList); if (monitorListData[i].active == 1) { await startMonitor(socket.userID, bean.id); @@ -1006,7 +1026,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } } - await sendNotificationList(socket) + await sendNotificationList(socket); await sendMonitorList(socket); } @@ -1025,9 +1045,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("clearEvents", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`) + console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`); await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [ "", @@ -1051,9 +1071,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("clearHeartbeats", async (monitorID, callback) => { try { - checkLogin(socket) + checkLogin(socket); - 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 = ?", [ monitorID @@ -1075,9 +1095,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("clearStatistics", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); - console.log(`Clear Statistics User ID: ${socket.userID}`) + console.log(`Clear Statistics User ID: ${socket.userID}`); await R.exec("DELETE FROM heartbeat"); @@ -1093,24 +1113,27 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } }); - debug("added all socket handlers") + // Status Page Socket Handler for admin only + statusPageSocketHandler(socket); + + debug("added all socket handlers"); // *************************** // Better do anything after added all socket handlers here // *************************** - debug("check auto login") + debug("check auto login"); if (await setting("disableAuth")) { - console.log("Disabled Auth: auto login to admin") - afterLogin(socket, await R.findOne("user")) - socket.emit("autoLogin") + console.log("Disabled Auth: auto login to admin"); + afterLogin(socket, await R.findOne("user")); + socket.emit("autoLogin"); } else { - debug("need auth") + debug("need auth"); } }); - console.log("Init the server") + console.log("Init the server"); server.once("error", async (err) => { console.error("Cannot listen: " + err.message); @@ -1132,14 +1155,14 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); async function updateMonitorNotification(monitorID, notificationIDList) { await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ monitorID, - ]) + ]); for (let notificationID in notificationIDList) { if (notificationIDList[notificationID]) { let relation = R.dispense("monitor_notification"); relation.monitor_id = monitorID; relation.notification_id = notificationID; - await R.store(relation) + await R.store(relation); } } } @@ -1148,7 +1171,7 @@ async function checkOwner(userID, monitorID) { let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [ monitorID, userID, - ]) + ]); if (! row) { throw new Error("You do not own this monitor."); @@ -1157,16 +1180,16 @@ async function checkOwner(userID, monitorID) { async function sendMonitorList(socket) { let list = await getMonitorJSONList(socket.userID); - io.to(socket.userID).emit("monitorList", list) + io.to(socket.userID).emit("monitorList", list); return list; } async function afterLogin(socket, user) { socket.userID = user.id; - socket.join(user.id) + socket.join(user.id); - let monitorList = await sendMonitorList(socket) - sendNotificationList(socket) + let monitorList = await sendMonitorList(socket); + sendNotificationList(socket); await sleep(500); @@ -1179,7 +1202,7 @@ async function afterLogin(socket, user) { } for (let monitorID in monitorList) { - await Monitor.sendStats(io, monitorID, user.id) + await Monitor.sendStats(io, monitorID, user.id); } } @@ -1188,7 +1211,7 @@ async function getMonitorJSONList(userID) { let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [ userID, - ]) + ]); for (let monitor of monitorList) { result[monitor.id] = await monitor.toJSON(); @@ -1197,24 +1220,18 @@ async function getMonitorJSONList(userID) { return result; } -function checkLogin(socket) { - if (! socket.userID) { - throw new Error("You are not logged in."); - } -} - async function initDatabase() { if (! fs.existsSync(Database.path)) { - console.log("Copying Database") + console.log("Copying Database"); fs.copyFileSync(Database.templatePath, Database.path); } - console.log("Connecting to Database") + console.log("Connecting to Database"); await Database.connect(); - console.log("Connected") + console.log("Connected"); // Patch the database - await Database.patch() + await Database.patch(); let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ "jwtSecret", @@ -1230,7 +1247,7 @@ async function initDatabase() { // If there is no record in user table, it is a new Uptime Kuma instance, need to setup if ((await R.count("user")) === 0) { - console.log("No user, need setup") + console.log("No user, need setup"); needSetup = true; } @@ -1238,9 +1255,9 @@ async function initDatabase() { } async function startMonitor(userID, monitorID) { - await checkOwner(userID, monitorID) + await checkOwner(userID, monitorID); - console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`) + console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`); await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ monitorID, @@ -1249,24 +1266,24 @@ async function startMonitor(userID, monitorID) { let monitor = await R.findOne("monitor", " id = ? ", [ monitorID, - ]) + ]); if (monitor.id in monitorList) { monitorList[monitor.id].stop(); } monitorList[monitor.id] = monitor; - monitor.start(io) + monitor.start(io); } async function restartMonitor(userID, monitorID) { - return await startMonitor(userID, monitorID) + return await startMonitor(userID, monitorID); } async function pauseMonitor(userID, monitorID) { - await checkOwner(userID, monitorID) + await checkOwner(userID, monitorID); - console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`) + console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`); await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ monitorID, @@ -1282,7 +1299,7 @@ async function pauseMonitor(userID, monitorID) { * Resume active monitors */ async function startMonitors() { - let list = await R.find("monitor", " active = 1 ") + let list = await R.find("monitor", " active = 1 "); for (let monitor of list) { monitorList[monitor.id] = monitor; @@ -1299,10 +1316,10 @@ async function shutdownFunction(signal) { console.log("Shutdown requested"); console.log("Called signal: " + signal); - console.log("Stopping all monitors") + console.log("Stopping all monitors"); for (let id in monitorList) { - let monitor = monitorList[id] - monitor.stop() + let monitor = monitorList[id]; + monitor.stop(); } await sleep(2000); await Database.close(); diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js new file mode 100644 index 00000000..5826277c --- /dev/null +++ b/server/socket-handlers/status-page-socket-handler.js @@ -0,0 +1,161 @@ +const { R } = require("redbean-node"); +const { checkLogin, setSettings } = require("../util-server"); +const dayjs = require("dayjs"); +const { debug } = require("../../src/util"); +const ImageDataURI = require("../image-data-uri"); +const Database = require("../database"); +const apicache = require("../modules/apicache"); + +module.exports.statusPageSocketHandler = (socket) => { + + // Post or edit incident + socket.on("postIncident", async (incident, callback) => { + try { + checkLogin(socket); + + await R.exec("UPDATE incident SET pin = 0 "); + + let incidentBean; + + if (incident.id) { + incidentBean = await R.findOne("incident", " id = ?", [ + incident.id + ]); + } + + if (incidentBean == null) { + incidentBean = R.dispense("incident"); + } + + incidentBean.title = incident.title; + incidentBean.content = incident.content; + incidentBean.style = incident.style; + incidentBean.pin = true; + + if (incident.id) { + incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); + } else { + incidentBean.createdDate = R.isoDateTime(dayjs.utc()); + } + + await R.store(incidentBean); + + callback({ + ok: true, + incident: incidentBean.toPublicJSON(), + }); + } catch (error) { + callback({ + ok: false, + msg: error.message, + }); + } + }); + + socket.on("unpinIncident", async (callback) => { + try { + checkLogin(socket); + + await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1"); + + callback({ + ok: true, + }); + } catch (error) { + callback({ + ok: false, + msg: error.message, + }); + } + }); + + // Save Status Page + // imgDataUrl Only Accept PNG! + socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => { + + try { + checkLogin(socket); + + apicache.clear(); + + const header = "data:image/png;base64,"; + + // Check logo format + // If is image data url, convert to png file + // Else assume it is a url, nothing to do + if (imgDataUrl.startsWith("data:")) { + if (! imgDataUrl.startsWith(header)) { + throw new Error("Only allowed PNG logo."); + } + + // Convert to file + await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png"); + config.logo = "/upload/logo.png?t=" + Date.now(); + + } else { + config.icon = imgDataUrl; + } + + // Save Config + await setSettings("statusPage", config); + + // Save Public Group List + const groupIDList = []; + let groupOrder = 1; + + for (let group of publicGroupList) { + let groupBean; + if (group.id) { + groupBean = await R.findOne("group", " id = ? AND public = 1 ", [ + group.id + ]); + } else { + groupBean = R.dispense("group"); + } + + groupBean.name = group.name; + groupBean.public = true; + groupBean.weight = groupOrder++; + + await R.store(groupBean); + + await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [ + groupBean.id + ]); + + let monitorOrder = 1; + console.log(group.monitorList); + + for (let monitor of group.monitorList) { + let relationBean = R.dispense("monitor_group"); + relationBean.weight = monitorOrder++; + relationBean.group_id = groupBean.id; + relationBean.monitor_id = monitor.id; + await R.store(relationBean); + } + + groupIDList.push(groupBean.id); + group.id = groupBean.id; + } + + // Delete groups that not in the list + debug("Delete groups that not in the list"); + const slots = groupIDList.map(() => "?").join(","); + await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList); + + callback({ + ok: true, + publicGroupList, + }); + + } catch (error) { + console.log(error); + + callback({ + ok: false, + msg: error.message, + }); + } + }); + +}; diff --git a/server/util-server.js b/server/util-server.js index 079bd82f..4d2b6cbe 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -23,7 +23,7 @@ exports.initJWTSecret = async () => { jwtSecretBean.value = passwordHash.generate(dayjs() + ""); await R.store(jwtSecretBean); return jwtSecretBean; -} +}; exports.tcping = function (hostname, port) { return new Promise((resolve, reject) => { @@ -44,7 +44,7 @@ exports.tcping = function (hostname, port) { resolve(Math.round(data.max)); }); }); -} +}; exports.ping = async (hostname) => { try { @@ -57,7 +57,7 @@ exports.ping = async (hostname) => { throw e; } } -} +}; exports.pingAsync = function (hostname, ipv6 = false) { return new Promise((resolve, reject) => { @@ -69,13 +69,13 @@ exports.pingAsync = function (hostname, ipv6 = false) { if (err) { reject(err); } else if (ms === null) { - reject(new Error(stdout)) + reject(new Error(stdout)); } else { - resolve(Math.round(ms)) + resolve(Math.round(ms)); } }); }); -} +}; exports.dnsResolve = function (hostname, resolver_server, rrtype) { const resolver = new Resolver(); @@ -98,8 +98,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) { } }); } - }) -} + }); +}; exports.setting = async function (key) { let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ @@ -108,29 +108,29 @@ exports.setting = async function (key) { try { const v = JSON.parse(value); - debug(`Get Setting: ${key}: ${v}`) + debug(`Get Setting: ${key}: ${v}`); return v; } catch (e) { return value; } -} +}; exports.setSetting = async function (key, value) { let bean = await R.findOne("setting", " `key` = ? ", [ key, - ]) + ]); if (!bean) { - bean = R.dispense("setting") + bean = R.dispense("setting"); bean.key = key; } bean.value = JSON.stringify(value); - await R.store(bean) -} + await R.store(bean); +}; exports.getSettings = async function (type) { let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ type, - ]) + ]); let result = {}; @@ -143,7 +143,7 @@ exports.getSettings = async function (type) { } return result; -} +}; exports.setSettings = async function (type, data) { let keyList = Object.keys(data); @@ -163,12 +163,12 @@ exports.setSettings = async function (type, data) { if (bean.type === type) { bean.value = JSON.stringify(data[key]); - promiseList.push(R.store(bean)) + promiseList.push(R.store(bean)); } } await Promise.all(promiseList); -} +}; // ssl-checker by @dyaa // param: res - response object from axios @@ -218,7 +218,7 @@ exports.checkCertificate = function (res) { issuer, fingerprint, }; -} +}; // Check if the provided status code is within the accepted ranges // Param: status - the status code to check @@ -247,7 +247,7 @@ exports.checkStatusCode = function (status, accepted_codes) { } return false; -} +}; exports.getTotalClientInRoom = (io, roomName) => { @@ -270,7 +270,7 @@ exports.getTotalClientInRoom = (io, roomName) => { } else { return 0; } -} +}; exports.genSecret = () => { let secret = ""; @@ -280,4 +280,21 @@ exports.genSecret = () => { secret += chars.charAt(Math.floor(Math.random() * charsLength)); } return secret; -} +}; + +exports.allowDevAllOrigin = (res) => { + if (process.env.NODE_ENV === "development") { + exports.allowAllOrigin(res); + } +}; + +exports.allowAllOrigin = (res) => { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); +}; + +exports.checkLogin = (socket) => { + if (! socket.userID) { + throw new Error("You are not logged in."); + } +}; diff --git a/src/assets/app.scss b/src/assets/app.scss index 58164573..8e96a4a5 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -144,7 +144,9 @@ h2 { } .shadow-box { - background-color: $dark-bg; + &:not(.alert) { + background-color: $dark-bg; + } } .form-check-input { @@ -255,6 +257,18 @@ h2 { background-color: $dark-bg; } + .monitor-list { + .item { + &:hover { + background-color: $dark-bg2; + } + + &.active { + background-color: $dark-bg2; + } + } + } + @media (max-width: 550px) { .table-shadow-box { tbody { @@ -268,6 +282,16 @@ h2 { } } } + + .alert { + &.bg-info, + &.bg-warning, + &.bg-danger, + &.bg-light { + color: $dark-font-color2; + } + } + } /* @@ -288,3 +312,119 @@ h2 { transform: translateY(50px); opacity: 0; } + +.slide-fade-right-enter-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-right-leave-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-right-enter-from, +.slide-fade-right-leave-to { + transform: translateX(50px); + opacity: 0; +} + +.monitor-list { + &.scrollbar { + min-height: calc(100vh - 240px); + max-height: calc(100vh - 30px); + overflow-y: auto; + position: sticky; + top: 10px; + } + + .item { + display: block; + text-decoration: none; + padding: 13px 15px 10px 15px; + border-radius: 10px; + transition: all ease-in-out 0.15s; + + &.disabled { + opacity: 0.3; + } + + .info { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &:hover { + background-color: $highlight-white; + } + + &.active { + background-color: #cdf8f4; + } + } +} + +.alert-success { + color: #122f21; + background-color: $primary; + border-color: $primary; +} + +.alert-info { + color: #055160; + background-color: #cff4fc; + border-color: #cff4fc; +} + +.alert-danger { + color: #842029; + background-color: #f8d7da; + border-color: #f8d7da; +} + +.btn-success { + color: #fff; + background-color: #4caf50; + border-color: #4caf50; +} + +[contenteditable=true] { + transition: all $easing-in 0.2s; + background-color: rgba(239, 239, 239, 0.7); + border-radius: 8px; + + &:focus { + outline: 0 solid #eee; + background-color: rgba(245, 245, 245, 0.9); + } + + &:hover { + background-color: rgba(239, 239, 239, 0.8); + } + + .dark & { + background-color: rgba(239, 239, 239, 0.2); + } + + /* + &::after { + margin-left: 5px; + content: "🖊️"; + font-size: 13px; + color: #eee; + } + */ + +} + +.action { + transition: all $easing-in 0.2s; + + &:hover { + cursor: pointer; + transform: scale(1.2); + } +} + +.vue-image-crop-upload .vicp-wrap { + border-radius: 10px !important; +} diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index fb6086d0..4dc2c712 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -25,6 +25,10 @@ export default { type: Number, required: true, }, + heartbeatList: { + type: Array, + default: null, + } }, data() { return { @@ -38,8 +42,15 @@ export default { }, computed: { + /** + * If heartbeatList is null, get it from $root.heartbeatList + */ beatList() { - return this.$root.heartbeatList[this.monitorId] + if (this.heartbeatList === null) { + return this.$root.heartbeatList[this.monitorId]; + } else { + return this.heartbeatList; + } }, shortBeatList() { @@ -118,8 +129,10 @@ export default { window.removeEventListener("resize", this.resize); }, beforeMount() { - if (! (this.monitorId in this.$root.heartbeatList)) { - this.$root.heartbeatList[this.monitorId] = []; + if (this.heartbeatList === null) { + if (! (this.monitorId in this.$root.heartbeatList)) { + this.$root.heartbeatList[this.monitorId] = []; + } } }, diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue index cc76b85f..fb3fcfb0 100644 --- a/src/components/MonitorList.vue +++ b/src/components/MonitorList.vue @@ -12,7 +12,7 @@ -
+
{{ $t("No Monitors, please") }} {{ $t("add one") }}
@@ -163,56 +163,6 @@ export default { max-width: 15em; } -.list { - &.scrollbar { - min-height: calc(100vh - 240px); - max-height: calc(100vh - 30px); - overflow-y: auto; - position: sticky; - top: 10px; - } - - .item { - display: block; - text-decoration: none; - padding: 13px 15px 10px 15px; - border-radius: 10px; - transition: all ease-in-out 0.15s; - - &.disabled { - opacity: 0.3; - } - - .info { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &:hover { - background-color: $highlight-white; - } - - &.active { - background-color: #cdf8f4; - } - } -} - -.dark { - .list { - .item { - &:hover { - background-color: $dark-bg2; - } - - &.active { - background-color: $dark-bg2; - } - } - } -} - .monitorItem { width: 100%; } diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue new file mode 100644 index 00000000..23d19e6c --- /dev/null +++ b/src/components/PublicGroupList.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/i18n.js b/src/i18n.js index fe2612fb..6ef82006 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -43,6 +43,6 @@ export const i18n = createI18n({ locale: localStorage.locale || "en", fallbackLocale: "en", silentFallbackWarn: true, - silentTranslationWarn: false, + silentTranslationWarn: true, messages: languageList, }); diff --git a/src/icon.js b/src/icon.js index c824210b..67eb2a76 100644 --- a/src/icon.js +++ b/src/icon.js @@ -1,4 +1,8 @@ import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; + +// Add Free Font Awesome Icons +// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free import { faArrowAltCircleUp, faCog, @@ -12,13 +16,19 @@ import { faSearch, faTachometerAlt, faTimes, - faTrash + faTimesCircle, + faTrash, + faCheckCircle, + faStream, + faSave, + faExclamationCircle, + faBullhorn, + faArrowsAltV, + faUnlink, + faQuestionCircle, + faImages, faUpload, } from "@fortawesome/free-solid-svg-icons"; -//import { fa } from '@fortawesome/free-regular-svg-icons' -import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -// Add Free Font Awesome Icons here -// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free library.add( faArrowAltCircleUp, faCog, @@ -32,7 +42,18 @@ library.add( faSearch, faTachometerAlt, faTimes, + faTimesCircle, faTrash, + faCheckCircle, + faStream, + faSave, + faExclamationCircle, + faBullhorn, + faArrowsAltV, + faUnlink, + faQuestionCircle, + faImages, + faUpload, ); export { FontAwesomeIcon }; diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue index 467ae53a..2342ed1a 100644 --- a/src/layouts/Layout.vue +++ b/src/layouts/Layout.vue @@ -18,6 +18,11 @@