diff --git a/.dockerignore b/.dockerignore index fedba8895..3d92084d7 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 21fb56088..8b45337f8 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 56007fb07..2bf60f7c5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ dist-ssr /data !/data/.gitkeep .vscode + +/private +/out diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11a599ad5..7a0ee2d1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,12 +82,10 @@ npm install --legacy-peer-deps --dev # Backend Dev +(2021-09-23 Update) + ```bash -npm run start-server - -# Or - -node server/server.js +npm run start-server-dev ``` It binds to `0.0.0.0:3001` by default. diff --git a/db/demo_kuma.db b/db/demo_kuma.db deleted file mode 100644 index 2042fcf2f..000000000 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 000000000..1c6f366bd --- /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 000000000..531cfb382 --- /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/dockerfile b/dockerfile index 891b03d5b..979360127 100644 --- a/dockerfile +++ b/dockerfile @@ -1,14 +1,8 @@ # DON'T UPDATE TO node:14-bullseye-slim, see #372. +# If the image changed, the second stage image should be changed too FROM node:14-buster-slim AS build WORKDIR /app -# split the sqlite install here, so that it can caches the arm prebuilt -# do not modify it, since we don't want to re-compile the arm prebuilt again -RUN apt update && \ - apt --yes install python3 python3-pip python3-dev git g++ make && \ - ln -s /usr/bin/python3 /usr/bin/python && \ - npm install mapbox/node-sqlite3#593c9d --build-from-source - COPY . . RUN npm install --legacy-peer-deps && \ npm run build && \ @@ -16,7 +10,7 @@ RUN npm install --legacy-peer-deps && \ chmod +x /app/extra/entrypoint.sh -FROM node:14-bullseye-slim AS release +FROM node:14-buster-slim AS release WORKDIR /app # Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv diff --git a/dockerfile-alpine b/dockerfile-alpine index 5e34d84a8..f30da5b05 100644 --- a/dockerfile-alpine +++ b/dockerfile-alpine @@ -2,13 +2,6 @@ FROM node:14-alpine3.12 AS build WORKDIR /app -# split the sqlite install here, so that it can caches the arm prebuilt -RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \ - ln -s /usr/bin/python3 /usr/bin/python && \ - npm install mapbox/node-sqlite3#593c9d && \ - apk del .build-deps && \ - rm -f /usr/bin/python - COPY . . RUN npm install --legacy-peer-deps && \ npm run build && \ diff --git a/extra/reset-password.js b/extra/reset-password.js index b849848b8..be0395897 100644 --- a/extra/reset-password.js +++ b/extra/reset-password.js @@ -6,12 +6,14 @@ const Database = require("../server/database"); const { R } = require("redbean-node"); const readline = require("readline"); const { initJWTSecret } = require("../server/util-server"); +const args = require("args-parser")(process.argv); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); (async () => { + Database.init(args); await Database.connect(); try { diff --git a/package-lock.json b/package-lock.json index c44884194..511f02d24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/vue-fontawesome": "^3.0.0-4", + "@louislam/sqlite3": "^5.0.6", "@popperjs/core": "^2.10.1", "args-parser": "^1.3.0", "axios": "^0.21.4", @@ -28,7 +29,7 @@ "form-data": "^4.0.0", "http-graceful-shutdown": "^3.1.4", "jsonwebtoken": "^8.5.1", - "nodemailer": "^6.6.3", + "nodemailer": "^6.6.5", "notp": "^2.0.3", "password-hash": "^1.2.2", "prom-client": "^13.2.0", @@ -37,35 +38,38 @@ "redbean-node": "0.1.2", "socket.io": "^4.2.0", "socket.io-client": "^4.2.0", - "sqlite3": "github:mapbox/node-sqlite3#593c9d", "tcp-ping": "^0.1.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", - "@types/bootstrap": "^5.1.4", + "@babel/eslint-parser": "^7.15.7", + "@types/bootstrap": "^5.1.6", "@vitejs/plugin-legacy": "^1.5.3", - "@vitejs/plugin-vue": "^1.6.2", - "@vue/compiler-sfc": "^3.2.11", - "core-js": "^3.17.3", + "@vitejs/plugin-vue": "^1.9.1", + "@vue/compiler-sfc": "^3.2.16", + "core-js": "^3.18.0", + "cross-env": "^7.0.3", "dns2": "^2.0.1", "eslint": "^7.32.0", - "eslint-plugin-vue": "^7.17.0", - "sass": "^1.39.2", + "eslint-plugin-vue": "^7.18.0", + "sass": "^1.42.1", "stylelint": "^13.13.1", "stylelint-config-standard": "^22.0.0", "typescript": "^4.4.3", - "vite": "^2.5.7" + "vite": "^2.5.10" }, "engines": { "node": "14.*" @@ -156,9 +160,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.4.tgz", - "integrity": "sha512-hPMIAmGNbmQzXJIo2P43Zj9UhRmGev5f9nqdBFOWNGDGh6XKmjby79woBvg6y0Jur6yRfQBneDbUQ8ZVc1krFw==", + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.7.tgz", + "integrity": "sha512-yJkHyomClm6A2Xzb8pdAo4HzYMSXFn1O5zrCYvbFP0yQFvHueLedV8WiEno8yJOKStjUXzBZzJFeWQ7b3YMsqQ==", "dev": true, "dependencies": { "eslint-scope": "^5.1.1", @@ -737,6 +741,27 @@ "node": ">= 10" } }, + "node_modules/@louislam/sqlite3": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-5.0.6.tgz", + "integrity": "sha512-uitL0jdbki5XSrmGKGgvHVMHEe00O6GAMoPrVOnh4KTcFOJ1T8SWypbnyqSxBr7PrjAVfgnIGu3kzYCCqIxd4g==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^3.0.0" + }, + "optionalDependencies": { + "node-gyp": "^7.1.2" + }, + "peerDependencies": { + "node-gyp": "7.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", @@ -875,9 +900,9 @@ } }, "node_modules/@types/bootstrap": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.4.tgz", - "integrity": "sha512-VAY+o6sCKrJ7Xix/lugdvQz0PpOn7Go+fQzCXOZvIdp7E/TDaiJddInVhNB/84bk9NX6uuKFSfl2pqslNYH9aA==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz", + "integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==", "dev": true, "dependencies": { "@popperjs/core": "^2.9.2", @@ -923,12 +948,6 @@ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, - "node_modules/@types/estree": { - "version": "0.0.48", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", - "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", - "dev": true - }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -1085,15 +1104,15 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.6.2.tgz", - "integrity": "sha512-Pf+dqkT4pWPfziPm51VtDXsPwE74CEGRiK6Vgm5EDBewHw1EgcxG7V2ZI/Yqj5gcDy5nVtjgx0AbsTL+F3gddg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.9.1.tgz", + "integrity": "sha512-9YuxaU2nLoSS/S1Ep4QTG/pEIh96LlauNM1g7LN/EOJ14Nj8HBeSy1OL26ydxb+MPhKn5XKGARh5wQF0UjHbLw==", "dev": true, "engines": { "node": ">=12.0.0" }, "peerDependencies": { - "@vue/compiler-sfc": "^3.2.6" + "vite": "^2.5.10" } }, "node_modules/@vue/compiler-core": { @@ -1118,41 +1137,89 @@ } }, "node_modules/@vue/compiler-sfc": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.11.tgz", - "integrity": "sha512-cUIaS8mgJrQ6yucj2AupWAwBRITK3W/a8wCOn9g5fJGtOl8h4APY8vN3lzP8HIJDyEeRF3I8SfRhL+oX97kSnw==", + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.16.tgz", + "integrity": "sha512-AxaDDg0ZjY7lCoVnCq7V+K3SIEfhyIHtten7k/LRupVC/VzSbelBmW0J8bawgsjLJAfTsdWZjeezZ5JJp2DM/A==", "dev": true, "dependencies": { "@babel/parser": "^7.15.0", - "@babel/types": "^7.15.0", - "@types/estree": "^0.0.48", - "@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", + "@vue/compiler-core": "3.2.16", + "@vue/compiler-dom": "3.2.16", + "@vue/compiler-ssr": "3.2.16", + "@vue/ref-transform": "3.2.16", + "@vue/shared": "3.2.16", "estree-walker": "^2.0.2", - "hash-sum": "^2.0.0", - "lru-cache": "^5.1.1", "magic-string": "^0.25.7", - "merge-source-map": "^1.1.0", "postcss": "^8.1.10", - "postcss-modules": "^4.0.0", - "postcss-selector-parser": "^6.0.4", "source-map": "^0.6.1" } }, - "node_modules/@vue/compiler-ssr": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.11.tgz", - "integrity": "sha512-+ptAdUlFDij+Z0VGCbRRkxQlNev5LkbZAntvkxrFjc08CTMhZmiV4Js48n2hAmuSXaKNEpmGkDGU26c/vf1+xw==", + "node_modules/@vue/compiler-sfc/node_modules/@vue/compiler-core": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.16.tgz", + "integrity": "sha512-60LD3f1GpMtoCPWKP7HacFxv97/EUY8m4WNqfFYmfaILVGO0icojdOCYOfgGFiYC+kgk1MOVdiI4vrWci0CnhQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.2.11", - "@vue/shared": "3.2.11" + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.16", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" } }, + "node_modules/@vue/compiler-sfc/node_modules/@vue/compiler-dom": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.16.tgz", + "integrity": "sha512-K7lYfwvsp5OLb0+/rKI9XT2RJy2RB7TyJBjvlfCDAF0KOJGqWAx++DLJPm+F3D29Mhxgt6ozSKP+rC3dSabvYA==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.2.16", + "@vue/shared": "3.2.16" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.16.tgz", + "integrity": "sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw==", + "dev": true + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.16.tgz", + "integrity": "sha512-u2Inuqp3QpEV3E03ppBLdba40mU0dz/fisbfGjRPlxH5uuQ9v9i5qgrFl7xZ+N5C0ugg5+5KI7MgsbsCAPn0mQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.2.16", + "@vue/shared": "3.2.16" + } + }, + "node_modules/@vue/compiler-ssr/node_modules/@vue/compiler-core": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.16.tgz", + "integrity": "sha512-60LD3f1GpMtoCPWKP7HacFxv97/EUY8m4WNqfFYmfaILVGO0icojdOCYOfgGFiYC+kgk1MOVdiI4vrWci0CnhQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.16", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-ssr/node_modules/@vue/compiler-dom": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.16.tgz", + "integrity": "sha512-K7lYfwvsp5OLb0+/rKI9XT2RJy2RB7TyJBjvlfCDAF0KOJGqWAx++DLJPm+F3D29Mhxgt6ozSKP+rC3dSabvYA==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.2.16", + "@vue/shared": "3.2.16" + } + }, + "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.16.tgz", + "integrity": "sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw==", + "dev": true + }, "node_modules/@vue/devtools-api": { "version": "6.0.0-beta.15", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.15.tgz", @@ -1167,18 +1234,36 @@ } }, "node_modules/@vue/ref-transform": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.11.tgz", - "integrity": "sha512-7rX0YsfYb7+1PeKPME1tQyUQcQgt0sIXRRnPD1Vw8Zs2KIo90YLy9CrvwalcRCxGw0ScsjBEhVjJtWIT79TElg==", + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.16.tgz", + "integrity": "sha512-IXFgxGnyd5jIXPQ/QlOoz+daeikeR1AA6DujgqalmW/ndCX9ZKW1rhFsoMGR0WAUZ4VHbT3eluUJhBF8ikNzPg==", "dev": true, "dependencies": { "@babel/parser": "^7.15.0", - "@vue/compiler-core": "3.2.11", - "@vue/shared": "3.2.11", + "@vue/compiler-core": "3.2.16", + "@vue/shared": "3.2.16", "estree-walker": "^2.0.2", "magic-string": "^0.25.7" } }, + "node_modules/@vue/ref-transform/node_modules/@vue/compiler-core": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.16.tgz", + "integrity": "sha512-60LD3f1GpMtoCPWKP7HacFxv97/EUY8m4WNqfFYmfaILVGO0icojdOCYOfgGFiYC+kgk1MOVdiI4vrWci0CnhQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.16", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/ref-transform/node_modules/@vue/shared": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.16.tgz", + "integrity": "sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw==", + "dev": true + }, "node_modules/@vue/runtime-core": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.11.tgz", @@ -1278,9 +1363,9 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { "node": ">=8" @@ -1553,6 +1638,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", @@ -1633,15 +1739,6 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1656,12 +1753,6 @@ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -2088,18 +2179,6 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, - "node_modules/consolidate": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", - "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==", - "dev": true, - "dependencies": { - "bluebird": "^3.7.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -2142,9 +2221,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "node_modules/core-js": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", - "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.0.tgz", + "integrity": "sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==", "dev": true, "hasInstallScript": true, "funding": { @@ -2185,6 +2264,24 @@ "node": ">=10" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2212,9 +2309,9 @@ } }, "node_modules/csstype": { - "version": "2.6.17", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", - "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" + "version": "2.6.18", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", + "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==" }, "node_modules/dashdash": { "version": "1.14.1", @@ -2437,9 +2534,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "node_modules/electron-to-chromium": { - "version": "1.3.836", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.836.tgz", - "integrity": "sha512-Ney3pHOJBWkG/AqYjrW0hr2AUCsao+2uvq9HUlRP8OlpSdk/zOHOUJP7eu0icDvePC9DlgffuelP4TnOJmMRUg==", + "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": { @@ -2447,15 +2544,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -2554,9 +2642,9 @@ } }, "node_modules/esbuild": { - "version": "0.12.27", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.27.tgz", - "integrity": "sha512-G42siADcTdRU1qRBxhiIiVLG4gcEMyWV4CWfLBdSii+olCueZJHFRHc7EqQRnRvNkSQq88i0k1Oufw/YVueUWQ==", + "version": "0.12.28", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.28.tgz", + "integrity": "sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA==", "dev": true, "hasInstallScript": true, "bin": { @@ -2646,9 +2734,9 @@ } }, "node_modules/eslint-plugin-vue": { - "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==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.18.0.tgz", + "integrity": "sha512-ceDXlXYMMPMSXw7tdKUR42w9jlzthJGJ3Kvm3YrZ0zuQfvAySNxe8sm6VHuksBW0+060GzYXhHJG6IHVOfF83Q==", "dev": true, "dependencies": { "eslint-utils": "^2.1.0", @@ -3083,9 +3171,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", - "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", "funding": [ { "type": "individual", @@ -3214,15 +3302,6 @@ "node": ">=0.10.0" } }, - "node_modules/generic-names": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz", - "integrity": "sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ==", - "dev": true, - "dependencies": { - "loader-utils": "^1.1.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3468,12 +3547,6 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, - "node_modules/hash-sum": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", - "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", - "dev": true - }, "node_modules/hosted-git-info": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", @@ -3605,24 +3678,6 @@ "node": ">=0.10.0" } }, - "node_modules/icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", - "dev": true - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3987,18 +4042,6 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "optional": true }, - "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -4152,20 +4195,6 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, - "node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4183,12 +4212,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -4273,15 +4296,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -4428,15 +4442,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4558,9 +4563,9 @@ } }, "node_modules/minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", "dependencies": { "yallist": "^4.0.0" }, @@ -4708,9 +4713,9 @@ "dev": true }, "node_modules/nodemailer": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz", - "integrity": "sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew==", + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.5.tgz", + "integrity": "sha512-C/v856DBijUzHcHIgGpQoTrfsH3suKIRAGliIzCstatM2cAa+MYX3LuyCrABiO/cdJTxgBBHXxV1ztiqUwst5A==", "engines": { "node": ">=6.0.0" } @@ -5232,84 +5237,6 @@ "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", "dev": true }, - "node_modules/postcss-modules": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.2.2.tgz", - "integrity": "sha512-/H08MGEmaalv/OU8j6bUKi/kZr2kqGF6huAW8m9UAgOLWtpFdhA14+gPBoymtqyv+D4MLsmqaF2zvIegdCxJXg==", - "dev": true, - "dependencies": { - "generic-names": "^2.0.1", - "icss-replace-symbols": "^1.1.0", - "lodash.camelcase": "^4.3.0", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "string-hash": "^1.1.1" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-modules-extract-imports": { - "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, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -5978,9 +5905,9 @@ } }, "node_modules/redbean-node/node_modules/@types/node": { - "version": "14.17.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz", - "integrity": "sha512-D1sdW0EcSCmNdLKBGMYb38YsHUS6JcM7yQ6sLQ9KuZ35ck7LYCKE7kYFHOO59ayFOY3zobWVZxf4KXhYHcHYFA==" + "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", @@ -6234,9 +6161,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.39.2.tgz", - "integrity": "sha512-4/6Vn2RPc+qNwSclUSKvssh7dqK1Ih3FfHBW16I/GfH47b3scbYeOw65UIrYG7PkweFiKbpJjgkf5CV8EMmvzw==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.1.tgz", + "integrity": "sha512-/zvGoN8B7dspKc5mC6HlaygyCBRvnyzzgD5khiaCfglWztY99cYoiTUksVx11NlnemrcfH5CEaCpsUKoW0cQqg==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0" @@ -6343,9 +6270,9 @@ } }, "node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", + "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" }, "node_modules/slash": { "version": "3.0.0", @@ -6436,6 +6363,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", @@ -6506,27 +6438,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "node_modules/sqlite3": { - "version": "5.0.2", - "resolved": "git+ssh://git@github.com/mapbox/node-sqlite3.git#593c9d498be2510d286349134537e3bf89401c4a", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "node-addon-api": "^3.0.0" - }, - "optionalDependencies": { - "node-gyp": "7.x" - }, - "peerDependencies": { - "node-gyp": "7.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, "node_modules/sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -6568,12 +6479,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true - }, "node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -7489,9 +7394,9 @@ } }, "node_modules/vite": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.7.tgz", - "integrity": "sha512-hyUoWmRPhjN1aI+ZSBqDINKdIq7aokHE2ZXiztOg4YlmtpeQtMwMeyxv6X9YxHZmvGzg/js/eATM9Z1nwyakxg==", + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", "dev": true, "dependencies": { "esbuild": "^0.12.17", @@ -7550,6 +7455,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", @@ -7636,6 +7549,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", @@ -7708,6 +7629,17 @@ "vue": "^3.0.2" } }, + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "dependencies": { + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7872,12 +7804,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -8086,9 +8012,9 @@ } }, "@babel/eslint-parser": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.4.tgz", - "integrity": "sha512-hPMIAmGNbmQzXJIo2P43Zj9UhRmGev5f9nqdBFOWNGDGh6XKmjby79woBvg6y0Jur6yRfQBneDbUQ8ZVc1krFw==", + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.7.tgz", + "integrity": "sha512-yJkHyomClm6A2Xzb8pdAo4HzYMSXFn1O5zrCYvbFP0yQFvHueLedV8WiEno8yJOKStjUXzBZzJFeWQ7b3YMsqQ==", "dev": true, "requires": { "eslint-scope": "^5.1.1", @@ -8445,7 +8371,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", @@ -8525,6 +8452,16 @@ "@intlify/shared": "9.1.7" } }, + "@louislam/sqlite3": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-5.0.6.tgz", + "integrity": "sha512-uitL0jdbki5XSrmGKGgvHVMHEe00O6GAMoPrVOnh4KTcFOJ1T8SWypbnyqSxBr7PrjAVfgnIGu3kzYCCqIxd4g==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^3.0.0", + "node-gyp": "^7.1.2" + } + }, "@mapbox/node-pre-gyp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", @@ -8632,9 +8569,9 @@ } }, "@types/bootstrap": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.4.tgz", - "integrity": "sha512-VAY+o6sCKrJ7Xix/lugdvQz0PpOn7Go+fQzCXOZvIdp7E/TDaiJddInVhNB/84bk9NX6uuKFSfl2pqslNYH9aA==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz", + "integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==", "dev": true, "requires": { "@popperjs/core": "^2.9.2", @@ -8680,12 +8617,6 @@ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, - "@types/estree": { - "version": "0.0.48", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", - "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", - "dev": true - }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -8836,10 +8767,11 @@ } }, "@vitejs/plugin-vue": { - "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 + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.9.1.tgz", + "integrity": "sha512-9YuxaU2nLoSS/S1Ep4QTG/pEIh96LlauNM1g7LN/EOJ14Nj8HBeSy1OL26ydxb+MPhKn5XKGARh5wQF0UjHbLw==", + "dev": true, + "requires": {} }, "@vue/compiler-core": { "version": "3.2.11", @@ -8863,39 +8795,91 @@ } }, "@vue/compiler-sfc": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.11.tgz", - "integrity": "sha512-cUIaS8mgJrQ6yucj2AupWAwBRITK3W/a8wCOn9g5fJGtOl8h4APY8vN3lzP8HIJDyEeRF3I8SfRhL+oX97kSnw==", + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.16.tgz", + "integrity": "sha512-AxaDDg0ZjY7lCoVnCq7V+K3SIEfhyIHtten7k/LRupVC/VzSbelBmW0J8bawgsjLJAfTsdWZjeezZ5JJp2DM/A==", "dev": true, "requires": { "@babel/parser": "^7.15.0", - "@babel/types": "^7.15.0", - "@types/estree": "^0.0.48", - "@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", + "@vue/compiler-core": "3.2.16", + "@vue/compiler-dom": "3.2.16", + "@vue/compiler-ssr": "3.2.16", + "@vue/ref-transform": "3.2.16", + "@vue/shared": "3.2.16", "estree-walker": "^2.0.2", - "hash-sum": "^2.0.0", - "lru-cache": "^5.1.1", "magic-string": "^0.25.7", - "merge-source-map": "^1.1.0", "postcss": "^8.1.10", - "postcss-modules": "^4.0.0", - "postcss-selector-parser": "^6.0.4", "source-map": "^0.6.1" + }, + "dependencies": { + "@vue/compiler-core": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.16.tgz", + "integrity": "sha512-60LD3f1GpMtoCPWKP7HacFxv97/EUY8m4WNqfFYmfaILVGO0icojdOCYOfgGFiYC+kgk1MOVdiI4vrWci0CnhQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.16", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-dom": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.16.tgz", + "integrity": "sha512-K7lYfwvsp5OLb0+/rKI9XT2RJy2RB7TyJBjvlfCDAF0KOJGqWAx++DLJPm+F3D29Mhxgt6ozSKP+rC3dSabvYA==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.2.16", + "@vue/shared": "3.2.16" + } + }, + "@vue/shared": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.16.tgz", + "integrity": "sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw==", + "dev": true + } } }, "@vue/compiler-ssr": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.11.tgz", - "integrity": "sha512-+ptAdUlFDij+Z0VGCbRRkxQlNev5LkbZAntvkxrFjc08CTMhZmiV4Js48n2hAmuSXaKNEpmGkDGU26c/vf1+xw==", + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.16.tgz", + "integrity": "sha512-u2Inuqp3QpEV3E03ppBLdba40mU0dz/fisbfGjRPlxH5uuQ9v9i5qgrFl7xZ+N5C0ugg5+5KI7MgsbsCAPn0mQ==", "dev": true, "requires": { - "@vue/compiler-dom": "3.2.11", - "@vue/shared": "3.2.11" + "@vue/compiler-dom": "3.2.16", + "@vue/shared": "3.2.16" + }, + "dependencies": { + "@vue/compiler-core": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.16.tgz", + "integrity": "sha512-60LD3f1GpMtoCPWKP7HacFxv97/EUY8m4WNqfFYmfaILVGO0icojdOCYOfgGFiYC+kgk1MOVdiI4vrWci0CnhQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.16", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-dom": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.16.tgz", + "integrity": "sha512-K7lYfwvsp5OLb0+/rKI9XT2RJy2RB7TyJBjvlfCDAF0KOJGqWAx++DLJPm+F3D29Mhxgt6ozSKP+rC3dSabvYA==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.2.16", + "@vue/shared": "3.2.16" + } + }, + "@vue/shared": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.16.tgz", + "integrity": "sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw==", + "dev": true + } } }, "@vue/devtools-api": { @@ -8912,16 +8896,36 @@ } }, "@vue/ref-transform": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.11.tgz", - "integrity": "sha512-7rX0YsfYb7+1PeKPME1tQyUQcQgt0sIXRRnPD1Vw8Zs2KIo90YLy9CrvwalcRCxGw0ScsjBEhVjJtWIT79TElg==", + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.16.tgz", + "integrity": "sha512-IXFgxGnyd5jIXPQ/QlOoz+daeikeR1AA6DujgqalmW/ndCX9ZKW1rhFsoMGR0WAUZ4VHbT3eluUJhBF8ikNzPg==", "dev": true, "requires": { "@babel/parser": "^7.15.0", - "@vue/compiler-core": "3.2.11", - "@vue/shared": "3.2.11", + "@vue/compiler-core": "3.2.16", + "@vue/shared": "3.2.16", "estree-walker": "^2.0.2", "magic-string": "^0.25.7" + }, + "dependencies": { + "@vue/compiler-core": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.16.tgz", + "integrity": "sha512-60LD3f1GpMtoCPWKP7HacFxv97/EUY8m4WNqfFYmfaILVGO0icojdOCYOfgGFiYC+kgk1MOVdiI4vrWci0CnhQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.16", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "@vue/shared": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.16.tgz", + "integrity": "sha512-zpv8lxuatl3ruCJCsGzrO/F4+IlLug4jbu3vaIi/wJVZKQgnsW1R/xSRJMQS6K57cl4fT/2zkrYsWh1/6H7Esw==", + "dev": true + } } }, "@vue/runtime-core": { @@ -8972,7 +8976,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", @@ -9001,9 +9006,9 @@ "dev": true }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -9221,6 +9226,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", @@ -9274,12 +9300,6 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -9291,12 +9311,6 @@ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -9332,7 +9346,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", @@ -9478,7 +9493,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", @@ -9614,15 +9630,6 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, - "consolidate": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", - "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==", - "dev": true, - "requires": { - "bluebird": "^3.7.2" - } - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -9656,9 +9663,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-js": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", - "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.0.tgz", + "integrity": "sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==", "dev": true }, "core-util-is": { @@ -9688,6 +9695,15 @@ "yaml": "^1.10.0" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9706,9 +9722,9 @@ "dev": true }, "csstype": { - "version": "2.6.17", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", - "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" + "version": "2.6.18", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", + "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==" }, "dashdash": { "version": "1.14.1", @@ -9888,9 +9904,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.836", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.836.tgz", - "integrity": "sha512-Ney3pHOJBWkG/AqYjrW0hr2AUCsao+2uvq9HUlRP8OlpSdk/zOHOUJP7eu0icDvePC9DlgffuelP4TnOJmMRUg==", + "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": { @@ -9898,12 +9914,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -9986,9 +9996,9 @@ } }, "esbuild": { - "version": "0.12.27", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.27.tgz", - "integrity": "sha512-G42siADcTdRU1qRBxhiIiVLG4gcEMyWV4CWfLBdSii+olCueZJHFRHc7EqQRnRvNkSQq88i0k1Oufw/YVueUWQ==", + "version": "0.12.28", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.28.tgz", + "integrity": "sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA==", "dev": true }, "escalade": { @@ -10082,9 +10092,9 @@ } }, "eslint-plugin-vue": { - "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==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.18.0.tgz", + "integrity": "sha512-ceDXlXYMMPMSXw7tdKUR42w9jlzthJGJ3Kvm3YrZ0zuQfvAySNxe8sm6VHuksBW0+060GzYXhHJG6IHVOfF83Q==", "dev": true, "requires": { "eslint-utils": "^2.1.0", @@ -10413,9 +10423,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", - "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" }, "forever-agent": { "version": "0.6.1", @@ -10504,15 +10514,6 @@ } } }, - "generic-names": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz", - "integrity": "sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0" - } - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10695,12 +10696,6 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, - "hash-sum": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", - "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", - "dev": true - }, "hosted-git-info": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", @@ -10808,18 +10803,6 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", - "dev": true - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true - }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -11074,15 +11057,6 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "optional": true }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -11198,17 +11172,6 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -11223,12 +11186,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -11303,15 +11260,6 @@ "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", "dev": true }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -11417,15 +11365,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - } - }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11507,9 +11446,9 @@ } }, "minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", "requires": { "yallist": "^4.0.0" }, @@ -11624,9 +11563,9 @@ "dev": true }, "nodemailer": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz", - "integrity": "sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew==" + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.5.tgz", + "integrity": "sha512-C/v856DBijUzHcHIgGpQoTrfsH3suKIRAGliIzCstatM2cAa+MYX3LuyCrABiO/cdJTxgBBHXxV1ztiqUwst5A==" }, "nopt": { "version": "5.0.0", @@ -12012,57 +11951,6 @@ "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", "dev": true }, - "postcss-modules": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.2.2.tgz", - "integrity": "sha512-/H08MGEmaalv/OU8j6bUKi/kZr2kqGF6huAW8m9UAgOLWtpFdhA14+gPBoymtqyv+D4MLsmqaF2zvIegdCxJXg==", - "dev": true, - "requires": { - "generic-names": "^2.0.1", - "icss-replace-symbols": "^1.1.0", - "lodash.camelcase": "^4.3.0", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "string-hash": "^1.1.1" - } - }, - "postcss-modules-extract-imports": { - "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 - }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0" - } - }, "postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -12351,7 +12239,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", @@ -12590,9 +12479,9 @@ }, "dependencies": { "@types/node": { - "version": "14.17.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz", - "integrity": "sha512-D1sdW0EcSCmNdLKBGMYb38YsHUS6JcM7yQ6sLQ9KuZ35ck7LYCKE7kYFHOO59ayFOY3zobWVZxf4KXhYHcHYFA==" + "version": "14.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.16.tgz", + "integrity": "sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==" } } }, @@ -12774,9 +12663,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.39.2.tgz", - "integrity": "sha512-4/6Vn2RPc+qNwSclUSKvssh7dqK1Ih3FfHBW16I/GfH47b3scbYeOw65UIrYG7PkweFiKbpJjgkf5CV8EMmvzw==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.1.tgz", + "integrity": "sha512-/zvGoN8B7dspKc5mC6HlaygyCBRvnyzzgD5khiaCfglWztY99cYoiTUksVx11NlnemrcfH5CEaCpsUKoW0cQqg==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0" @@ -12866,9 +12755,9 @@ "dev": true }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", + "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" }, "slash": { "version": "3.0.0", @@ -12940,6 +12829,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", @@ -13001,15 +12895,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sqlite3": { - "version": "git+ssh://git@github.com/mapbox/node-sqlite3.git#593c9d498be2510d286349134537e3bf89401c4a", - "from": "sqlite3@github:mapbox/node-sqlite3#593c9d", - "requires": { - "@mapbox/node-pre-gyp": "^1.0.0", - "node-addon-api": "^3.0.0", - "node-gyp": "7.x" - } - }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -13040,12 +12925,6 @@ "safe-buffer": "~5.1.0" } }, - "string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -13284,7 +13163,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", @@ -13770,9 +13650,9 @@ } }, "vite": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.7.tgz", - "integrity": "sha512-hyUoWmRPhjN1aI+ZSBqDINKdIq7aokHE2ZXiztOg4YlmtpeQtMwMeyxv6X9YxHZmvGzg/js/eATM9Z1nwyakxg==", + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", "dev": true, "requires": { "esbuild": "^0.12.17", @@ -13808,12 +13688,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", @@ -13860,6 +13748,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", @@ -13877,7 +13773,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": {} } } }, @@ -13892,7 +13789,16 @@ "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==" + "integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==", + "requires": {} + }, + "vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "requires": { + "sortablejs": "1.14.0" + } }, "which": { "version": "2.0.2", @@ -14003,7 +13909,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", @@ -14015,12 +13922,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index 0f7a71575..5c3f8e034 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "repository": { "type": "git", @@ -10,22 +10,25 @@ "node": "14.*" }, "scripts": { - "install-legacy-peer-deps": "npm install --legacy-peer-deps", - "update-legacy-peer-deps": "npm update --legacy-peer-deps", + "install-legacy": "npm install --legacy-peer-deps", + "update-legacy": "npm update --legacy-peer-deps", "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", "lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore", "lint": "npm run lint:js && npm run lint:style", "dev": "vite --host", "start": "npm run start-server", "start-server": "node server/server.js", + "start-server-dev": "cross-env NODE_ENV=development 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", - "build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.6.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.6.0-debian --target release . --push", + "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.7.0-alpine --target release . --push", + "build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.7.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.0-debian --target release . --push", "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", + "build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", - "setup": "git checkout 1.6.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", + "setup": "git checkout 1.7.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", "update-version": "node extra/update-version.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", @@ -36,7 +39,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": { @@ -44,6 +47,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/vue-fontawesome": "^3.0.0-4", + "@louislam/sqlite3": "^5.0.6", "@popperjs/core": "^2.10.1", "args-parser": "^1.3.0", "axios": "^0.21.4", @@ -59,7 +63,7 @@ "form-data": "^4.0.0", "http-graceful-shutdown": "^3.1.4", "jsonwebtoken": "^8.5.1", - "nodemailer": "^6.6.3", + "nodemailer": "^6.6.5", "notp": "^2.0.3", "password-hash": "^1.2.2", "prom-client": "^13.2.0", @@ -68,34 +72,37 @@ "redbean-node": "0.1.2", "socket.io": "^4.2.0", "socket.io-client": "^4.2.0", - "sqlite3": "github:mapbox/node-sqlite3#593c9d", "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", - "@types/bootstrap": "^5.1.4", + "@babel/eslint-parser": "^7.15.7", + "@types/bootstrap": "^5.1.6", "@vitejs/plugin-legacy": "^1.5.3", - "@vitejs/plugin-vue": "^1.6.2", - "@vue/compiler-sfc": "^3.2.11", - "core-js": "^3.17.3", + "@vitejs/plugin-vue": "^1.9.1", + "@vue/compiler-sfc": "^3.2.16", + "core-js": "^3.18.0", + "cross-env": "^7.0.3", "dns2": "^2.0.1", "eslint": "^7.32.0", - "eslint-plugin-vue": "^7.17.0", - "sass": "^1.39.2", + "eslint-plugin-vue": "^7.18.0", + "sass": "^1.42.1", "stylelint": "^13.13.1", "stylelint-config-standard": "^22.0.0", "typescript": "^4.4.3", - "vite": "^2.5.7" + "vite": "^2.5.10" } } diff --git a/server/check-version.js b/server/check-version.js index 96e8aecf2..3ac2eee48 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 55d79df64..2f6c1c5fe 100644 --- a/server/database.js +++ b/server/database.js @@ -3,11 +3,25 @@ const { R } = require("redbean-node"); const { setSetting, setting } = require("./util-server"); 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; /** @@ -32,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, } /** @@ -42,27 +58,53 @@ class Database { static noReject = true; + static init(args) { + // 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 }); + } + + Database.uploadDir = Database.dataDir + "upload/"; + + if (! fs.existsSync(Database.uploadDir)) { + fs.mkdirSync(Database.uploadDir, { recursive: true }); + } + + console.log(`Data Dir: ${Database.dataDir}`); + } + static async connect() { const acquireConnectionTimeout = 120 * 1000; - R.setup("sqlite", { - filename: Database.path, + const Dialect = require("knex/lib/dialects/sqlite3/index.js"); + Dialect.prototype._driver = () => require("@louislam/sqlite3"); + + const knexInstance = knex({ + client: Dialect, + connection: { + filename: Database.path, + acquireConnectionTimeout: acquireConnectionTimeout, + }, useNullAsDefault: true, - acquireConnectionTimeout: acquireConnectionTimeout, - }, { - min: 1, - max: 1, - idleTimeoutMillis: 120 * 1000, - propagateCreateError: false, - acquireTimeoutMillis: acquireConnectionTimeout, + pool: { + min: 1, + max: 1, + idleTimeoutMillis: 120 * 1000, + propagateCreateError: false, + acquireTimeoutMillis: acquireConnectionTimeout, + } }); + R.setup(knexInstance); + if (process.env.SQL_LOG === "1") { R.debug(true); } // Auto map the model to a bean object - R.freeze(true) + R.freeze(true); await R.autoloadModels("./server/model"); // Change to WAL @@ -72,6 +114,7 @@ class Database { console.log("SQLite config:"); console.log(await R.getAll("PRAGMA journal_mode")); console.log(await R.getAll("PRAGMA cache_size")); + console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()")); } static async patch() { @@ -89,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); @@ -104,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); } } @@ -133,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) { @@ -142,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); } @@ -186,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"); } } @@ -204,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) => { @@ -217,7 +263,7 @@ class Database { }) .filter((statement) => { return statement !== ""; - }) + }); for (let statement of statements) { await R.exec(statement); @@ -263,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 000000000..3ccaab7d5 --- /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 000000000..567f3865b --- /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 546794140..e0a77c069 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 000000000..89c117e90 --- /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 c574df775..9a80225e0 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 000000000..22d1fed71 --- /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 000000000..b8bb9b354 --- /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 000000000..ad831e2e4 --- /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 000000000..b56efcb22 --- /dev/null +++ b/server/routers/api-router.js @@ -0,0 +1,151 @@ +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 b8d0c90f8..f5a8b16e3 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"); @@ -26,6 +26,8 @@ debug("Importing http-graceful-shutdown"); const gracefulShutdown = require("http-graceful-shutdown"); debug("Importing prometheus-api-metrics"); const prometheusAPIMetrics = require("prometheus-api-metrics"); +debug("Importing compare-versions"); +const compareVersions = require("compare-versions"); debug("Importing 2FA Modules"); const notp = require("notp"); @@ -35,7 +37,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"); @@ -69,14 +71,6 @@ 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") const app = express(); @@ -98,6 +92,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()); @@ -131,12 +126,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) => { @@ -156,28 +158,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", () => { @@ -185,7 +198,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); // *************************** - // Public API + // Public Socket API // *************************** socket.on("loginByToken", async (token, callback) => { @@ -193,44 +206,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({ @@ -238,13 +251,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) { @@ -256,39 +269,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}`; @@ -300,24 +313,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, @@ -326,18 +339,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, @@ -346,19 +359,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); @@ -366,40 +379,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.", - }) + }); } }); @@ -410,13 +423,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; @@ -440,8 +453,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; @@ -449,11 +462,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); @@ -475,18 +488,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; @@ -499,12 +512,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); @@ -516,7 +529,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, msg: e.message, @@ -526,13 +539,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, @@ -542,14 +555,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, @@ -567,7 +580,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); @@ -586,8 +599,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({ @@ -605,13 +618,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 = ? ", [ @@ -636,9 +649,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, @@ -655,12 +668,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, @@ -677,12 +690,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, @@ -699,9 +712,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, @@ -718,13 +731,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, @@ -741,13 +754,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, @@ -764,13 +777,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"); @@ -790,15 +803,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)) { @@ -807,9 +820,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) { @@ -822,7 +835,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); socket.on("getSettings", async (callback) => { try { - checkLogin(socket) + checkLogin(socket); callback({ ok: true, @@ -839,9 +852,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, @@ -859,10 +873,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, @@ -880,10 +894,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, @@ -900,9 +914,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, @@ -910,7 +924,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); }); } catch (e) { - console.error(e) + console.error(e); callback({ ok: false, @@ -921,7 +935,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); @@ -930,53 +944,83 @@ 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; + let version17x = compareVersions.compare(backupData.version, "1.7.0", ">="); + + // If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user" if (importHandle == "overwrite") { + // Stops every monitor first, so it doesn't execute any heartbeat while importing 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"); await R.exec("DELETE FROM monitor_tls_info"); await R.exec("DELETE FROM notification"); + await R.exec("DELETE FROM monitor_tag"); + await R.exec("DELETE FROM tag"); await R.exec("DELETE FROM monitor"); } + // Only starts importing if the backup file contains at least one notification if (notificationListData.length >= 1) { + // Get every existing notification name and puts them in one simple string let notificationNameList = await R.getAll("SELECT name FROM notification"); let notificationNameListString = JSON.stringify(notificationNameList); for (let i = 0; i < notificationListData.length; i++) { + // Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists 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); } } } + // Only starts importing if the backup file contains at least one monitor if (monitorListData.length >= 1) { + // Get every existing monitor name and puts them in one simple string let monitorNameList = await R.getAll("SELECT name FROM monitor"); let monitorNameListString = JSON.stringify(monitorNameList); for (let i = 0; i < monitorListData.length; i++) { + // Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists if ((importHandle == "skip" && monitorNameListString.includes(monitorListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") { + // Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0) + // --- Start --- + + // Define default values + let retryInterval = 0; + + /* + Only replace the default value with the backup file data for the specific version, where it appears the first time + More information about that where "let version" will be defined + */ + if (version17x) { + retryInterval = monitorListData[i].retryInterval; + } + + // --- End --- + let monitor = { + // Define the new variable from earlier here name: monitorListData[i].name, type: monitorListData[i].type, url: monitorListData[i].url, interval: monitorListData[i].interval, + retryInterval: retryInterval, hostname: monitorListData[i].hostname, maxretries: monitorListData[i].maxretries, port: monitorListData[i].port, @@ -988,9 +1032,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; @@ -998,12 +1042,47 @@ 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) + // Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented + if (version17x) { + // Only import if the specific monitor has tags assigned + for (const oldTag of monitorListData[i].tags) { + // Check if tag already exists and get data -> + let tag = await R.findOne("tag", " name = ?", [ + oldTag.name, + ]); + + let tagId; + if (! tag) { + // -> If it doesn't exist, create new tag from backup file + let beanTag = R.dispense("tag"); + beanTag.name = oldTag.name; + beanTag.color = oldTag.color; + await R.store(beanTag); + + tagId = beanTag.id; + } else { + // -> If it already exist, set tagId to value from database + tagId = tag.id; + } + + // Assign the new created tag to the monitor + await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [ + tagId, + bean.id, + oldTag.value, + ]); + + } + } + + await updateMonitorNotification(bean.id, notificationIDList); + + // If monitor was active start it immediately, otherwise pause it if (monitorListData[i].active == 1) { await startMonitor(socket.userID, bean.id); } else { @@ -1013,7 +1092,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } } - await sendNotificationList(socket) + await sendNotificationList(socket); await sendMonitorList(socket); } @@ -1032,9 +1111,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 = ? ", [ "", @@ -1058,9 +1137,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 @@ -1082,9 +1161,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"); @@ -1100,24 +1179,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); @@ -1139,14 +1221,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); } } } @@ -1155,7 +1237,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."); @@ -1164,16 +1246,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); @@ -1186,7 +1268,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); } } @@ -1195,7 +1277,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(); @@ -1204,24 +1286,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", @@ -1237,7 +1313,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; } @@ -1245,9 +1321,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, @@ -1256,24 +1332,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, @@ -1289,7 +1365,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; @@ -1306,10 +1382,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 000000000..5826277c7 --- /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 079bd82f3..4d2b6cbe1 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 581645735..8e96a4a5e 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 fb6086d00..4dc2c712c 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 cc76b85f7..fb3fcfb05 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 000000000..23d19e6cd --- /dev/null +++ b/src/components/PublicGroupList.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/i18n.js b/src/i18n.js index fe2612fbc..633a53acb 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -3,6 +3,7 @@ import daDK from "./languages/da-DK"; import deDE from "./languages/de-DE"; import en from "./languages/en"; import esEs from "./languages/es-ES"; +import ptBR from "./languages/pt-BR"; import etEE from "./languages/et-EE"; import frFR from "./languages/fr-FR"; import itIT from "./languages/it-IT"; @@ -24,6 +25,7 @@ const languageList = { "de-DE": deDE, "nl-NL": nlNL, "es-ES": esEs, + "pt-BR": ptBR, "fr-FR": frFR, "it-IT": itIT, "ja": ja, @@ -43,6 +45,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 c824210b2..67eb2a769 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/languages/da-DK.js b/src/languages/da-DK.js index eaba6e2cb..b778c8189 100644 --- a/src/languages/da-DK.js +++ b/src/languages/da-DK.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/de-DE.js b/src/languages/de-DE.js index 8f8792853..4014d0533 100644 --- a/src/languages/de-DE.js +++ b/src/languages/de-DE.js @@ -168,4 +168,14 @@ export default { "Export Backup": "Export Backup", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/en.js b/src/languages/en.js index 37704225a..b79660f1b 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -168,6 +168,16 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", "telegram": "Telegram", "webhook": "Webhook", "smtp": "Email (SMTP)", @@ -185,4 +195,4 @@ export default { "pushbullet": "Pushbullet", "line": "Line Messenger", "mattermost": "Mattermost", -} +}; diff --git a/src/languages/es-ES.js b/src/languages/es-ES.js index cb8733792..7571b1308 100644 --- a/src/languages/es-ES.js +++ b/src/languages/es-ES.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/et-EE.js b/src/languages/et-EE.js index 991a6a35e..b7d9722c3 100644 --- a/src/languages/et-EE.js +++ b/src/languages/et-EE.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/fr-FR.js b/src/languages/fr-FR.js index 0050c5c02..370de5947 100644 --- a/src/languages/fr-FR.js +++ b/src/languages/fr-FR.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/it-IT.js b/src/languages/it-IT.js index 09d0bece6..599761a23 100644 --- a/src/languages/it-IT.js +++ b/src/languages/it-IT.js @@ -168,4 +168,14 @@ export default { "Search...": "Cerca...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/ja.js b/src/languages/ja.js index 44f96ee44..bf8e3129a 100644 --- a/src/languages/ja.js +++ b/src/languages/ja.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/ko-KR.js b/src/languages/ko-KR.js index 70948a31d..fb3d65ea3 100644 --- a/src/languages/ko-KR.js +++ b/src/languages/ko-KR.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/nl-NL.js b/src/languages/nl-NL.js index 7ec386d01..354429155 100644 --- a/src/languages/nl-NL.js +++ b/src/languages/nl-NL.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/pl.js b/src/languages/pl.js index 2b34492e6..65f43aac1 100644 --- a/src/languages/pl.js +++ b/src/languages/pl.js @@ -169,4 +169,14 @@ export default { "Search...": "Szukaj...", "Avg. Ping": "Średni ping", "Avg. Response": "Średnia odpowiedź", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/pt-BR.js b/src/languages/pt-BR.js new file mode 100644 index 000000000..4fc22fefd --- /dev/null +++ b/src/languages/pt-BR.js @@ -0,0 +1,182 @@ +export default { + languageName: "Português (Brasileiro)", + checkEverySecond: "Verificar cada {0} segundos.", + retryCheckEverySecond: "Tentar novamente a cada {0} segundos.", + retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada", + ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS", + upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.", + maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.", + acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.", + passwordNotMatchMsg: "A senha repetida não corresponde.", + notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.", + keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas", + pauseDashboardHome: "Pausar", + deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?", + deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?", + resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.", + rrtypeDescription: "Selecione o RR-Type que você deseja monitorar", + pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?", + enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.", + clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?", + clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?", + confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?", + importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.", + confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.", + twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando", + tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.", + confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?", + confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?", + Settings: "Configurações", + Dashboard: "Dashboard", + "New Update": "Nova Atualização", + Language: "Linguagem", + Appearance: "Aparência", + Theme: "Tema", + General: "Geral", + Version: "Versão", + "Check Update On GitHub": "Verificar atualização no Github", + List: "Lista", + Add: "Adicionar", + "Add New Monitor": "Adicionar novo monitor", + "Quick Stats": "Estatísticas rápidas", + Up: "On", + Down: "Off", + Pending: "Pendente", + Unknown: "Desconhecido", + Pause: "Pausar", + Name: "Nome", + Status: "Status", + DateTime: "Data hora", + Message: "Mensagem", + "No important events": "Nenhum evento importante", + Resume: "Resumo", + Edit: "Editar", + Delete: "Deletar", + Current: "Atual", + Uptime: "Tempo de atividade", + "Cert Exp.": "Cert Exp.", + days: "dias", + day: "dia", + "-day": "-dia", + hour: "hora", + "-hour": "-hora", + Response: "Resposta", + Ping: "Ping", + "Monitor Type": "Tipo de Monitor", + Keyword: "Palavra-Chave", + "Friendly Name": "Nome Amigável", + URL: "URL", + Hostname: "Hostname", + Port: "Porta", + "Heartbeat Interval": "Intervalo de Heartbeat", + Retries: "Novas tentativas", + "Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat", + Advanced: "Avançado", + "Upside Down Mode": "Modo de cabeça para baixo", + "Max. Redirects": "Redirecionamento Máx.", + "Accepted Status Codes": "Status Code Aceitáveis", + Save: "Salvar", + Notifications: "Notificações", + "Not available, please setup.": "Não disponível, por favor configure.", + "Setup Notification": "Configurar Notificação", + Light: "Claro", + Dark: "Escuro", + Auto: "Auto", + "Theme - Heartbeat Bar": "Tema - Barra de Heartbeat", + Normal: "Normal", + Bottom: "Inferior", + None: "Nenhum", + Timezone: "Fuso horário", + "Search Engine Visibility": "Visibilidade do mecanismo de pesquisa", + "Allow indexing": "Permitir Indexação", + "Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site", + "Change Password": "Mudar senha", + "Current Password": "Senha atual", + "New Password": "Nova Senha", + "Repeat New Password": "Repetir Nova Senha", + "Update Password": "Atualizar Senha", + "Disable Auth": "Desativar Autenticação", + "Enable Auth": "Ativar Autenticação", + Logout: "Deslogar", + Leave: "Sair", + "I understand, please disable": "Eu entendo, por favor desative.", + Confirm: "Confirmar", + Yes: "Sim", + No: "Não", + Username: "Usuário", + Password: "Senha", + "Remember me": "Lembre-me", + Login: "Autenticar", + "No Monitors, please": "Nenhum monitor, por favor", + "add one": "adicionar um", + "Notification Type": "Tipo de Notificação", + Email: "Email", + Test: "Testar", + "Certificate Info": "Info. do Certificado ", + "Resolver Server": "Resolver Servidor", + "Resource Record Type": "Tipo de registro de aplicação", + "Last Result": "Último resultado", + "Create your admin account": "Crie sua conta de admin", + "Repeat Password": "Repita a senha", + "Import Backup": "Importar Backup", + "Export Backup": "Exportar Backup", + Export: "Exportar", + Import: "Importar", + respTime: "Tempo de Resp. (ms)", + notAvailableShort: "N/A", + "Default enabled": "Padrão habilitado", + "Apply on all existing monitors": "Aplicar em todos os monitores existentes", + Create: "Criar", + "Clear Data": "Limpar Dados", + Events: "Eventos", + Heartbeats: "Heartbeats", + "Auto Get": "Obter Automático", + backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.", + backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.", + backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.", + alertNoFile: "Selecione um arquivo para importar.", + alertWrongFileType: "Selecione um arquivo JSON.", + "Clear all statistics": "Limpar todas as estatísticas", + "Skip existing": "Pular existente", + Overwrite: "Sobrescrever", + Options: "Opções", + "Keep both": "Manter os dois", + "Verify Token": "Verificar Token", + "Setup 2FA": "Configurar 2FA", + "Enable 2FA": "Ativar 2FA", + "Disable 2FA": "Desativar 2FA", + "2FA Settings": "Configurações do 2FA ", + "Two Factor Authentication": "Autenticação e Dois Fatores", + Active: "Ativo", + Inactive: "Inativo", + Token: "Token", + "Show URI": "Mostrar URI", + Tags: "Tag", + "Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...", + "Tag with this name already exist.": "Já existe uma etiqueta com este nome.", + "Tag with this value already exist.": "Já existe uma etiqueta com este valor.", + color: "cor", + "value (optional)": "valor (opcional)", + Gray: "Cinza", + Red: "Vermelho", + Orange: "Laranja", + Green: "Verde", + Blue: "Azul", + Indigo: "Índigo", + Purple: "Roxo", + Pink: "Rosa", + "Search...": "Buscar...", + "Avg. Ping": "Ping Médio.", + "Avg. Response": "Resposta Média. ", + "Status Page": "Página de Status", + "Entry Page": "Página de entrada", + "statusPageNothing": "Nada aqui, por favor, adicione um grupo ou monitor.", + "No Services": "Nenhum Serviço", + "All Systems Operational": "Todos os Serviços Operacionais", + "Partially Degraded Service": "Serviço parcialmente degradado", + "Degraded Service": "Serviço Degradado", + "Add Group": "Adicionar Grupo", + "Add a monitor": "Adicionar um monitor", + "Edit Status Page": "Editar Página de Status", + "Go to Dashboard": "Ir para a dashboard", +}; diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js index 956235de4..b27baad13 100644 --- a/src/languages/ru-RU.js +++ b/src/languages/ru-RU.js @@ -169,4 +169,14 @@ export default { "Search...": "Поиск...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/sr-latn.js b/src/languages/sr-latn.js index 2ebeb32fd..51cb7f8f0 100644 --- a/src/languages/sr-latn.js +++ b/src/languages/sr-latn.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/sr.js b/src/languages/sr.js index b5cf1cb87..0c8b6ef44 100644 --- a/src/languages/sr.js +++ b/src/languages/sr.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/sv-SE.js b/src/languages/sv-SE.js index f4c42627e..de4fbcc7d 100644 --- a/src/languages/sv-SE.js +++ b/src/languages/sv-SE.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/tr-TR.js b/src/languages/tr-TR.js index c521680c9..c73775bb1 100644 --- a/src/languages/tr-TR.js +++ b/src/languages/tr-TR.js @@ -168,4 +168,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/zh-CN.js b/src/languages/zh-CN.js index c2b3dcc59..cf393cc79 100644 --- a/src/languages/zh-CN.js +++ b/src/languages/zh-CN.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/languages/zh-HK.js b/src/languages/zh-HK.js index 1e1e0c7dc..92714d713 100644 --- a/src/languages/zh-HK.js +++ b/src/languages/zh-HK.js @@ -169,4 +169,14 @@ export default { "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", -} + "Entry Page": "Entry Page", + "statusPageNothing": "Nothing here, please add a group or a monitor.", + "No Services": "No Services", + "All Systems Operational": "All Systems Operational", + "Partially Degraded Service": "Partially Degraded Service", + "Degraded Service": "Degraded Service", + "Add Group": "Add Group", + "Add a monitor": "Add a monitor", + "Edit Status Page": "Edit Status Page", + "Go to Dashboard": "Go to Dashboard", +}; diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue index 467ae53a2..6228d4fb8 100644 --- a/src/layouts/Layout.vue +++ b/src/layouts/Layout.vue @@ -18,7 +18,12 @@