Merge branch 'master' into master

This commit is contained in:
Moritz R 2022-05-19 14:24:02 +02:00 committed by GitHub
commit a9f3142cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
155 changed files with 6708 additions and 1941 deletions

View File

@ -1,4 +1,9 @@
module.exports = {
ignorePatterns: [
"test/*",
"server/modules/apicache/*",
"src/util.js"
],
root: true,
env: {
browser: true,
@ -17,14 +22,16 @@ module.exports = {
requireConfigFile: false,
},
rules: {
"yoda": "error",
eqeqeq: [ "warn", "smart" ],
"linebreak-style": [ "error", "unix" ],
"camelcase": [ "warn", {
"properties": "never",
"ignoreImports": true
}],
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
"no-unused-vars": "warn",
"no-unused-vars": [ "warn", {
"args": "none"
}],
indent: [
"error",
4,
@ -33,16 +40,23 @@ module.exports = {
SwitchCase: 1,
},
],
quotes: ["warn", "double"],
semi: "warn",
"vue/html-indent": ["warn", 4], // default: 2
quotes: [ "error", "double" ],
semi: "error",
"vue/html-indent": [ "error", 4 ], // default: 2
"vue/max-attributes-per-line": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/html-self-closing": "off",
"vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
"vue/multi-word-component-names": "off",
"no-multi-spaces": [ "error", {
ignoreEOLComments: true,
}],
"array-bracket-spacing": [ "warn", "always", {
"singleValue": true,
"objectsInArrays": false,
"arraysInArrays": false
}],
"space-before-function-paren": [ "error", {
"anonymous": "always",
"named": "never",
@ -59,7 +73,7 @@ module.exports = {
"keyword-spacing": "warn",
"space-infix-ops": "warn",
"arrow-spacing": "warn",
"no-trailing-spaces": "warn",
"no-trailing-spaces": "error",
"no-constant-condition": [ "error", {
"checkLoops": false,
}],

View File

@ -20,6 +20,7 @@ Please delete any options that are not relevant.
- [ ] I ran ESLint and other linters for modified files
- [ ] I have performed a self-review of my own code and tested it
- [ ] I have commented my code, particularly in hard-to-understand areas
(including JSDoc for methods)
- [ ] My changes generate no new warnings
- [ ] My code needed automated testing. I have added them (this is optional task)

View File

@ -11,25 +11,42 @@ on:
jobs:
auto-test:
needs: [ check-linters ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
node-version: [14.x, 16.x, 17.x]
node: [ 14, 16, 17, 18 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm run install-legacy
- run: npm install
- run: npm run build
- run: npm test
env:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
check-linters:
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Use Node.js 14
uses: actions/setup-node@v3
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run lint

View File

@ -1,9 +1,13 @@
{
"extends": "stylelint-config-standard",
"customSyntax": "postcss-html",
"rules": {
"indentation": 4,
"no-descending-specificity": null,
"selector-list-comma-newline-after": null,
"declaration-empty-line-before": null
"declaration-empty-line-before": null,
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"shorthand-property-no-redundant-values": null
}
}

View File

@ -27,24 +27,20 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma?
⚠️ 2022-03-02 Update:
Since I found that merging pull requests is a pretty heavy task for me, I try to rearrange it.
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first.
✅ Accept:
- Bug/Security fix
- Translations
- Adding notification providers
❌ Avoid:
⚠️ Discuss First
- Large pull requests
- New big features
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
- New features
### Recommended Pull Request Guideline
Before deep into coding, disscussion first is preferred. Creating an empty pull request for disscussion would be recommended.
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
1. Fork the project
1. Clone your fork repo to local
@ -79,6 +75,7 @@ I personally do not like something need to learn so much and need to config so m
- 4 spaces indentation
- Follow `.editorconfig`
- Follow ESLint
- Methods and functions should be documented with JSDoc
## Name convention
@ -89,9 +86,10 @@ I personally do not like something need to learn so much and need to config so m
## Tools
- Node.js >= 14
- NPM >= 8.5
- Git
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
- A SQLite tool (SQLite Expert Personal is suggested)
- A SQLite GUI tool (SQLite Expert Personal is suggested)
## Install dependencies
@ -99,39 +97,45 @@ I personally do not like something need to learn so much and need to config so m
npm ci
```
## How to start the Backend Dev Server
## Dev Server
(2021-09-23 Update)
(2022-04-26 Update)
We can start the frontend dev server and the backend dev server in one command.
Port `3000` and port `3001` will be used.
```bash
npm run start-server-dev
npm run dev
```
## Backend Server
It binds to `0.0.0.0:3001` by default.
### Backend Details
It is mainly a socket.io app + express.js.
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
express.js is used for:
- entry point such as redirecting to a status page or the dashboard
- serving the frontend built files (index.html, .js and .css etc.)
- serving internal APIs of status page
### Structure in /server/
- model/ (Object model, auto mapping to the database table name)
- modules/ (Modified 3rd-party modules)
- notification-providers/ (individual notification logic)
- routers/ (Express Routers)
- socket-handler (Socket.io Handlers)
- server.js (Server main logic)
- server.js (Server entry point and main logic)
## How to start the Frontend Dev Server
## Frontend Dev Server
1. Set the env var `NODE_ENV` to "development".
2. Start the frontend dev server by the following command.
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
```bash
npm run dev
```
It binds to `0.0.0.0:3000` by default.
For production, it is not used. It will be compiled to `dist` directory instead.
You can use Vue.js devtools Chrome extension for debugging.

View File

@ -25,12 +25,15 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
* Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* 20 second intervals.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
* Simple Status Page
* Multiple Status Pages
* Map Status Page to Domain
* Ping Chart
* Certificate Info
* Proxy Support
* 2FA available
## 🔧 How to Install
@ -154,10 +157,17 @@ https://www.reddit.com/r/UptimeKuma/
## Contribute
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
### Beta Version
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
### Bug Reports / Feature Requests
If you want to report a bug or request a new feature, feel free to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
### Translations
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.
### Pull Requests
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md

View File

@ -13,10 +13,7 @@ currently being supported with security updates.
### Uptime Kuma Versions
| Version | Supported |
| ------- | ------------------ |
| 1.9.X | :white_check_mark: |
| <= 1.8.X | ❌ |
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
### Upgradable Docker Tags
@ -24,8 +21,8 @@ currently being supported with security updates.
| ------- | ------------------ |
| 1 | :white_check_mark: |
| 1-debian | :white_check_mark: |
| 1-alpine | :white_check_mark: |
| latest | :white_check_mark: |
| debian | :white_check_mark: |
| alpine | :white_check_mark: |
| 1-alpine | ⚠️ Deprecated |
| alpine | ⚠️ Deprecated |
| All other tags | ❌ |

View File

@ -0,0 +1,16 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD mqtt_topic TEXT;
ALTER TABLE monitor
ADD mqtt_success_message VARCHAR(255);
ALTER TABLE monitor
ADD mqtt_username VARCHAR(255);
ALTER TABLE monitor
ADD mqtt_password VARCHAR(255);
COMMIT;

View File

@ -0,0 +1,6 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE status_page ADD footer_text TEXT;
ALTER TABLE status_page ADD custom_css TEXT;
ALTER TABLE status_page ADD show_powered_by BOOLEAN NOT NULL DEFAULT 1;
COMMIT;

View File

@ -4,5 +4,5 @@ WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise==0.9.7 && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \
rm -rf /root/.cache

View File

@ -11,7 +11,7 @@ WORKDIR /app
RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.7 && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \
rm -rf /var/lib/apt/lists/*
# Install cloudflared

View File

@ -8,7 +8,7 @@ services:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
volumes:
- ./uptime-kuma:/app/data
- ./uptime-kuma-data:/app/data
ports:
- 3001:3001
- 3001:3001 # <Host Port>:<Container Port>
restart: always

View File

@ -3,4 +3,4 @@ module.exports = {
name: "uptime-kuma",
script: "./server/server.js",
}]
}
};

View File

@ -1,11 +1,10 @@
const pkg = require("../../package.json");
const fs = require("fs");
const child_process = require("child_process");
const childProcess = require("child_process");
const util = require("../../src/util");
util.polyfill();
const oldVersion = pkg.version;
const version = process.env.VERSION;
console.log("Beta Version: " + version);
@ -21,6 +20,10 @@ if (! exists) {
// Process package.json
pkg.version = version;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Also update package-lock.json
childProcess.spawnSync("npm", [ "install" ]);
commit(version);
tag(version);
@ -32,7 +35,7 @@ if (! exists) {
function commit(version) {
let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
@ -40,15 +43,15 @@ function commit(version) {
throw new Error("commit error");
}
res = child_process.spawnSync("git", ["push", "origin", "master"]);
res = childProcess.spawnSync("git", [ "push", "origin", "master" ]);
console.log(res.stdout.toString().trim());
}
function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]);
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
res = child_process.spawnSync("git", ["push", "origin", version]);
res = childProcess.spawnSync("git", [ "push", "origin", version ]);
console.log(res.stdout.toString().trim());
}
@ -57,15 +60,7 @@ function tagExists(version) {
throw new Error("invalid version");
}
let res = child_process.spawnSync("git", ["tag", "-l", version]);
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
return res.stdout.toString().trim() === version;
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

View File

@ -4,21 +4,21 @@ const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version
const newVersion = oldVersion + "-nightly"
const oldVersion = pkg.version;
const newVersion = oldVersion + "-nightly";
console.log("Old Version: " + oldVersion)
console.log("New Version: " + newVersion)
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);
if (newVersion) {
// Process package.json
pkg.version = newVersion
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Process README.md
if (fs.existsSync("README.md")) {
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion))
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
}
}

View File

@ -4,6 +4,7 @@ const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const User = require("../server/model/user");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
@ -30,7 +31,7 @@ const main = async () => {
let confirmPassword = await question("Confirm New Password: ");
if (password === confirmPassword) {
await user.resetPassword(password);
await User.resetPassword(user.id, password);
// Reset all sessions by reset jwt secret
await initJWTSecret();

View File

@ -26,7 +26,7 @@ server.on("request", (request, send, rinfo) => {
ttl: 300,
address: "1.2.3.4"
});
} if (question.type === Packet.TYPE.AAAA) {
} else if (question.type === Packet.TYPE.AAAA) {
response.answers.push({
name: question.name,
type: question.type,

View File

@ -0,0 +1,50 @@
const { log } = require("../src/util");
const mqttUsername = "louis1";
const mqttPassword = "!@#$LLam";
class SimpleMqttServer {
aedes = require("aedes")();
server = require("net").createServer(this.aedes.handle);
constructor(port) {
this.port = port;
}
start() {
this.server.listen(this.port, () => {
console.log("server started and listening on port ", this.port);
});
}
}
let server1 = new SimpleMqttServer(10000);
server1.aedes.authenticate = function (client, username, password, callback) {
if (username && password) {
console.log(password.toString("utf-8"));
callback(null, username === mqttUsername && password.toString("utf-8") === mqttPassword);
} else {
callback(null, false);
}
};
server1.aedes.on("subscribe", (subscriptions, client) => {
console.log(subscriptions);
for (let s of subscriptions) {
if (s.topic === "test") {
server1.aedes.publish({
topic: "test",
payload: Buffer.from("ok"),
}, (error) => {
if (error) {
log.error("mqtt_server", error);
}
});
}
}
});
server1.start();

View File

@ -1,7 +1,6 @@
const pkg = require("../package.json");
const fs = require("fs");
const rmSync = require("./fs-rmSync.js");
const child_process = require("child_process");
const childProcess = require("child_process");
const util = require("../src/util");
util.polyfill();
@ -26,6 +25,9 @@ if (! exists) {
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Also update package-lock.json
childProcess.spawnSync("npm", [ "install" ]);
commit(newVersion);
tag(newVersion);
@ -42,7 +44,7 @@ if (! exists) {
function commit(version) {
let msg = "Update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
@ -52,7 +54,7 @@ function commit(version) {
}
function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]);
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
}
@ -67,7 +69,7 @@ function tagExists(version) {
throw new Error("invalid version");
}
let res = child_process.spawnSync("git", ["tag", "-l", version]);
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
return res.stdout.toString().trim() === version;
}

View File

@ -1,4 +1,4 @@
const child_process = require("child_process");
const childProcess = require("child_process");
const fs = require("fs");
const newVersion = process.env.VERSION;
@ -16,23 +16,23 @@ function updateWiki(newVersion) {
safeDelete(wikiDir);
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
childProcess.spawnSync("git", [ "clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir ]);
let content = fs.readFileSync(howToUpdateFilename).toString();
// Replace the version: https://regex101.com/r/hmj2Bc/1
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
fs.writeFileSync(howToUpdateFilename, content);
child_process.spawnSync("git", ["add", "-A"], {
childProcess.spawnSync("git", [ "add", "-A" ], {
cwd: wikiDir,
});
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
childProcess.spawnSync("git", [ "commit", "-m", `Update to ${newVersion}` ], {
cwd: wikiDir,
});
console.log("Pushing to Github");
child_process.spawnSync("git", ["push"], {
childProcess.spawnSync("git", [ "push" ], {
cwd: wikiDir,
});

3544
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.14.0",
"version": "1.16.0-beta.0",
"license": "MIT",
"repository": {
"type": "git",
@ -10,12 +10,15 @@
"node": "14.* || >=16.*"
},
"scripts": {
"install-legacy": "npm install --legacy-peer-deps",
"update-legacy": "npm update --legacy-peer-deps",
"install-legacy": "npm install",
"update-legacy": "npm update",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"dev": "vite --host --config ./config/vite.config.js",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
@ -36,7 +39,7 @@
"build-docker-nightly-alpine": "docker buildx build -f docker/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 -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.14.0 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -48,6 +51,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",
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
"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",
"ncu-patch": "npm-check-updates -u -t patch",
@ -60,10 +64,11 @@
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@louislam/sqlite3": "~6.0.1",
"@louislam/sqlite3": "~15.0.6",
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.26.1",
"badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3",
"bootstrap": "5.1.3",
"bree": "~7.1.5",
@ -71,6 +76,7 @@
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5",
"chroma-js": "^2.1.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"dayjs": "~1.10.8",
@ -85,12 +91,14 @@
"jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2",
"limiter": "^2.1.0",
"mqtt": "^4.2.8",
"node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.3",
"prismjs": "^1.27.0",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0",
@ -110,6 +118,7 @@
"vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
@ -117,26 +126,30 @@
},
"devDependencies": {
"@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.15.8",
"@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.4",
"@vue/compiler-sfc": "~3.2.31",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"concurrently": "^7.1.0",
"core-js": "~3.18.3",
"cross-env": "~7.0.3",
"dns2": "~2.0.1",
"eslint": "~7.32.0",
"eslint-plugin-vue": "~7.18.0",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"npm-check-updates": "^12.5.5",
"npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1",
"puppeteer": "~13.1.3",
"sass": "~1.42.1",
"stylelint": "~14.2.0",
"stylelint-config-standard": "~24.0.0",
"stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0",
"typescript": "~4.4.4",
"vite": "~2.6.14"
"vite": "~2.6.14",
"wait-on": "^6.0.1"
}
}

View File

@ -1,8 +1,12 @@
const { checkLogin } = require("./util-server");
const { R } = require("redbean-node");
class TwoFA {
/**
* Disable 2FA for specified user
* @param {number} userID ID of user to disable
* @returns {Promise<void>}
*/
static async disable2FA(userID) {
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
userID,

View File

@ -2,14 +2,13 @@ const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { debug } = require("../src/util");
const { loginRateLimiter } = require("./rate-limiter");
/**
*
* @param username : string
* @param password : string
* @returns {Promise<Bean|null>}
* Login to web app
* @param {string} username
* @param {string} password
* @returns {Promise<(Bean|null)>}
*/
exports.login = async function (username, password) {
if (typeof username !== "string" || typeof password !== "string") {
@ -35,11 +34,17 @@ exports.login = async function (username, password) {
};
/**
* A function that checks if a user is logged in.
* @param {string} username The username of the user to check for.
* @param {function} callback The callback to call when done, with an error and result parameter.
*
* Generated by Trelent
* Callback for myAuthorizer
* @callback myAuthorizerCB
* @param {any} err Any error encountered
* @param {boolean} authorized Is the client authorized?
*/
/**
* Custom authorizer for express-basic-auth
* @param {string} username
* @param {string} password
* @param {myAuthorizerCB} callback
*/
function myAuthorizer(username, password, callback) {
// Login Rate Limit

View File

@ -7,6 +7,7 @@ exports.latestVersion = null;
let interval;
/** Start 48 hour check interval */
exports.startInterval = () => {
let check = async () => {
try {
@ -42,6 +43,11 @@ exports.startInterval = () => {
interval = setInterval(check, 3600 * 1000 * 48);
};
/**
* Enable the check update feature
* @param {boolean} value Should the check update feature be enabled?
* @returns {Promise<void>}
*/
exports.enableCheckUpdate = async (value) => {
await setSetting("checkUpdate", value);

View File

@ -3,15 +3,15 @@
*/
const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node");
const { io } = require("./server");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
/**
* Send a list of notifications to the user.
* @param {Socket} socket The socket object that is connected to the client.
*
* Generated by Trelent
* Send list of notification providers to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>}
*/
async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
@ -34,8 +34,11 @@ async function sendNotificationList(socket) {
/**
* Send Heartbeat History list to socket
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param overwrite Overwrite client-side's heartbeat list
* @param {Socket} socket Socket.io instance
* @param {number} monitorID ID of monitor to send heartbeat history
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
* @returns {Promise<void>}
*/
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
const timeLogger = new TimeLogger();
@ -62,10 +65,11 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
/**
* Important Heart beat list (aka event list)
* @param socket
* @param monitorID
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param overwrite Overwrite client-side's heartbeat list
* @param {Socket} socket Socket.io instance
* @param {number} monitorID ID of monitor to send heartbeat history
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
* @returns {Promise<void>}
*/
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
const timeLogger = new TimeLogger();
@ -90,9 +94,8 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
}
/**
* Delivers proxy list
*
* @param socket
* Emit proxy list to client
* @param {Socket} socket Socket.io socket instance
* @return {Promise<Bean[]>}
*/
async function sendProxyList(socket) {
@ -108,9 +111,8 @@ async function sendProxyList(socket) {
/**
* Emits the version information to the client.
* @param {Socket} socket The socket object that is connected to the client.
*
* Generated by Trelent
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<void>}
*/
async function sendInfo(socket) {
socket.emit("info", {

View File

@ -1,7 +1,20 @@
const args = require("args-parser")(process.argv);
const demoMode = args["demo"] || false;
const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultDownColor: "#c2290a",
defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
};
module.exports = {
args,
demoMode
demoMode,
badgeConstants,
};

View File

@ -1,7 +1,7 @@
const fs = require("fs");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { debug, sleep } = require("../src/util");
const { log, sleep } = require("../src/util");
const dayjs = require("dayjs");
const knex = require("knex");
@ -57,7 +57,9 @@ class Database {
"patch-status-page.sql": true,
"patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true,
}
"patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true,
};
/**
* The final version should be 10 after merged tag feature
@ -67,6 +69,10 @@ class Database {
static noReject = true;
/**
* Initialize the database
* @param {Object} args Arguments to initialize DB with
*/
static init(args) {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
@ -81,9 +87,18 @@ class Database {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
console.log(`Data Dir: ${Database.dataDir}`);
log.info("db", `Data Dir: ${Database.dataDir}`);
}
/**
* Connect to the database
* @param {boolean} [testMode=false] Should the connection be
* started in test mode?
* @param {boolean} [autoloadModels=true] Should models be
* automatically loaded?
* @param {boolean} [noLog=false] Should logs not be output?
* @returns {Promise<void>}
*/
static async connect(testMode = false, autoloadModels = true, noLog = false) {
const acquireConnectionTimeout = 120 * 1000;
@ -136,13 +151,14 @@ class Database {
await R.exec("PRAGMA synchronous = FULL");
if (!noLog) {
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()"));
log.info("db", "SQLite config:");
log.info("db", await R.getAll("PRAGMA journal_mode"));
log.info("db", await R.getAll("PRAGMA cache_size"));
log.info("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
}
}
/** Patch the database */
static async patch() {
let version = parseInt(await setting("database_version"));
@ -150,15 +166,15 @@ class Database {
version = 0;
}
console.info("Your database version: " + version);
console.info("Latest database version: " + this.latestVersion);
log.info("db", "Your database version: " + version);
log.info("db", "Latest database version: " + this.latestVersion);
if (version === this.latestVersion) {
console.info("Database patch not needed");
log.info("db", "Database patch not needed");
} else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
log.info("db", "Warning: Database version is newer than expected");
} else {
console.info("Database patch is needed");
log.info("db", "Database patch is needed");
this.backup(version);
@ -166,17 +182,17 @@ class Database {
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`;
console.info(`Patching ${sqlFile}`);
log.info("db", `Patching ${sqlFile}`);
await Database.importSQLFile(sqlFile);
console.info(`Patched ${sqlFile}`);
log.info("db", `Patched ${sqlFile}`);
await setSetting("database_version", i);
}
} catch (ex) {
await Database.close();
console.error(ex);
console.error("Start Uptime-Kuma failed due to issue patching the database");
console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
log.error("db", ex);
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1);
@ -188,19 +204,21 @@ class Database {
}
/**
* Patch DB using new process
* Call it from patch() only
* @private
* @returns {Promise<void>}
*/
static async patch2() {
console.log("Database Patch 2.0 Process");
log.info("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
}
debug("Patched files:");
debug(databasePatchedFiles);
log.debug("db", "Patched files:");
log.debug("db", databasePatchedFiles);
try {
for (let sqlFilename in this.patchList) {
@ -208,15 +226,15 @@ class Database {
}
if (this.patched) {
console.log("Database Patched Successfully");
log.info("db", "Database Patched Successfully");
}
} catch (ex) {
await Database.close();
console.error(ex);
console.error("Start Uptime-Kuma failed due to issue patching the database");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
log.error("db", ex);
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
@ -295,24 +313,27 @@ class Database {
}
/**
* Patch database using new patching process
* Used it patch2() only
* @private
* @param sqlFilename
* @param databasePatchedFiles
* @returns {Promise<void>}
*/
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename];
if (! value) {
console.log(sqlFilename + " skip");
log.info("db", sqlFilename + " skip");
return;
}
// Check if patched
if (! databasePatchedFiles[sqlFilename]) {
console.log(sqlFilename + " is not patched");
log.info("db", sqlFilename + " is not patched");
if (value.parents) {
console.log(sqlFilename + " need parents");
log.info("db", sqlFilename + " need parents");
for (let parentSQLFilename of value.parents) {
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
}
@ -320,24 +341,24 @@ class Database {
this.backup(dayjs().format("YYYYMMDDHHmmss"));
console.log(sqlFilename + " is patching");
log.info("db", sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true;
console.log(sqlFilename + " was patched successfully");
log.info("db", sqlFilename + " was patched successfully");
} else {
debug(sqlFilename + " is already patched, skip");
log.debug("db", sqlFilename + " is already patched, skip");
}
}
/**
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
* @param filename
* Load an SQL file and execute it
* @param filename Filename of SQL file to import
* @returns {Promise<void>}
*/
static async importSQLFile(filename) {
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
await R.getCell("SELECT 1");
let text = fs.readFileSync(filename).toString();
@ -365,6 +386,10 @@ class Database {
}
}
/**
* Aquire a direct connection to database
* @returns {any}
*/
static getBetterSQLite3Database() {
return R.knex.client.acquireConnection();
}
@ -379,7 +404,7 @@ class Database {
};
process.addListener("unhandledRejection", listener);
console.log("Closing the database");
log.info("db", "Closing the database");
while (true) {
Database.noReject = true;
@ -389,10 +414,10 @@ class Database {
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the database");
log.info("db", "Waiting to close the database");
}
}
console.log("SQLite closed");
log.info("db", "SQLite closed");
process.removeListener("unhandledRejection", listener);
}
@ -400,11 +425,11 @@ class Database {
/**
* One backup one time in this process.
* Reset this.backupPath if you want to backup again
* @param version
* @param {string} version Version code of backup
*/
static backup(version) {
if (! this.backupPath) {
console.info("Backing up the database");
log.info("db", "Backing up the database");
this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath);
@ -422,12 +447,10 @@ class Database {
}
}
/**
*
*/
/** Restore from most recent backup */
static restore() {
if (this.backupPath) {
console.error("Patching the database failed!!! Restoring the backup");
log.error("db", "Patching the database failed!!! Restoring the backup");
const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal";
@ -446,7 +469,7 @@ class Database {
fs.unlinkSync(walPath);
}
} catch (e) {
console.log("Restore failed; you may need to restore the backup manually");
log.error("db", "Restore failed; you may need to restore the backup manually");
process.exit(1);
}
@ -462,17 +485,22 @@ class Database {
}
} else {
console.log("Nothing to restore");
log.info("db", "Nothing to restore");
}
}
/** Get the size of the database */
static getSize() {
debug("Database.getSize()");
log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.path);
debug(stats);
log.debug("db", stats);
return stats.size;
}
/**
* Shrink the database
* @returns {Promise<void>}
*/
static async shrink() {
await R.exec("VACUUM");
}

View File

@ -3,18 +3,21 @@
Modified with 0 dependencies
*/
let fs = require("fs");
const { log } = require("../src/util");
let ImageDataURI = (() => {
/**
* @param {string} dataURI - A string that is a valid Data URI.
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object.
*
* Generated by Trelent
* Decode the data:image/ URI
* @param {string} dataURI data:image/ URI to decode
* @returns {?Object} An object with properties "imageType" and "dataBase64".
* The former is the image type, e.g., "png", and the latter is a base64
* encoded string of the image's binary data. If it fails to parse, returns
* null instead of an object.
*/
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/\"");
log.error("image-data-uri", "It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
return null;
}
@ -27,15 +30,15 @@ let ImageDataURI = (() => {
}
/**
* @param {Buffer} data - The image data to be encoded.
* @param {String} mediaType - The type of the image, e.g., "image/png".
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred.
*
* Generated by Trelent
* Endcode an image into data:image/ URI
* @param {(Buffer|string)} data Data to encode
* @param {string} mediaType Media type of data
* @returns {(string|null)} A string representing the base64-encoded
* version of the given Buffer object or null if an error occurred.
*/
function encode(data, mediaType) {
if (!data || !mediaType) {
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
log.error("image-data-uri", "Missing some of the required params: data, mediaType");
return null;
}
@ -47,11 +50,10 @@ let ImageDataURI = (() => {
}
/**
* Converts a data URI to a file path.
* @param {string} dataURI The Data URI of the image.
* @param {string} [filePath] The path where the image will be saved, defaults to "./".
*
* Generated by Trelent
* Write data URI to file
* @param {string} dataURI data:image/ URI
* @param {string} [filePath] Path to write file to
* @returns {Promise<string>}
*/
function outputFile(dataURI, filePath) {
filePath = filePath || "./";

View File

@ -1,6 +1,7 @@
const path = require("path");
const Bree = require("bree");
const { SHARE_ENV } = require("worker_threads");
const { log } = require("../src/util");
let bree;
const jobs = [
{
@ -9,6 +10,11 @@ const jobs = [
},
];
/**
* Initialize background jobs
* @param {Object} args Arguments to pass to workers
* @returns {Bree}
*/
const initBackgroundJobs = function (args) {
bree = new Bree({
root: path.resolve("server", "jobs"),
@ -18,7 +24,7 @@ const initBackgroundJobs = function (args) {
workerData: args,
},
workerMessageHandler: (message) => {
console.log("[Background Job]:", message);
log.info("jobs", message);
}
});

View File

@ -2,14 +2,24 @@ const { parentPort, workerData } = require("worker_threads");
const Database = require("../database");
const path = require("path");
/**
* Send message to parent process for logging
* since worker_thread does not have access to stdout, this is used
* instead of console.log()
* @param {any} any The message to log
*/
const log = function (any) {
if (parentPort) {
parentPort.postMessage(any);
}
};
/**
* Exit the worker process
* @param {number} error The status code to exit
*/
const exit = function (error) {
if (error && error != 0) {
if (error && error !== 0) {
process.exit(error);
} else {
if (parentPort) {
@ -20,6 +30,7 @@ const exit = function (error) {
}
};
/** Connects to the database */
const connectDb = async function () {
const dbPath = path.join(
process.env.DATA_DIR || workerData["data-dir"] || "./data/"

View File

@ -3,6 +3,12 @@ const { R } = require("redbean-node");
class Group extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @param {boolean} [showTags=false] Should the JSON include monitor tags
* @returns {Object}
*/
async toPublicJSON(showTags = false) {
let monitorBeanList = await this.getMonitorList();
let monitorList = [];
@ -19,6 +25,10 @@ class Group extends BeanModel {
};
}
/**
* Get all monitors
* @returns {Bean[]}
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
SELECT monitor.* FROM monitor, monitor_group

View File

@ -13,6 +13,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
*/
class Heartbeat extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
toPublicJSON() {
return {
status: this.status,
@ -22,6 +27,10 @@ class Heartbeat extends BeanModel {
};
}
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
monitorID: this.monitor_id,

View File

@ -2,6 +2,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
class Incident extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
toPublicJSON() {
return {
id: this.id,

View File

@ -6,8 +6,8 @@ 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, setting, errorLog } = require("../util-server");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@ -15,6 +15,7 @@ const { Proxy } = require("../proxy");
const { demoMode } = require("../config");
const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
/**
* status:
@ -27,6 +28,7 @@ class Monitor extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON(showTags = false) {
let obj = {
@ -41,8 +43,9 @@ class Monitor extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
async toJSON() {
async toJSON(includeSensitiveData = true) {
let notificationIDList = {};
@ -56,15 +59,11 @@ class Monitor extends BeanModel {
const tags = await this.getTags();
return {
let data = {
id: this.id,
name: this.name,
url: this.url,
method: this.method,
body: this.body,
headers: this.headers,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
@ -89,9 +88,30 @@ class Monitor extends BeanModel {
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage
};
if (includeSensitiveData) {
data = {
...data,
headers: this.headers,
body: this.body,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
pushToken: this.pushToken,
};
}
return data;
}
/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>}
*/
async getTags() {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
}
@ -105,6 +125,10 @@ class Monitor extends BeanModel {
return Buffer.from(user + ":" + pass).toString("base64");
}
/**
* Is the TLS expiry notification enabled?
* @returns {boolean}
*/
isEnabledExpiryNotification() {
return Boolean(this.expiryNotification);
}
@ -125,10 +149,18 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
/**
* Get accepted status codes
* @returns {Object}
*/
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
/**
* Start monitor
* @param {Server} io Socket server instance
*/
start(io) {
let previousBeat = null;
let retries = 0;
@ -154,7 +186,7 @@ class Monitor extends BeanModel {
// undefined if not https
let tlsInfo = undefined;
if (! previousBeat) {
if (!previousBeat || this.type === "push") {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id,
]);
@ -196,7 +228,7 @@ class Monitor extends BeanModel {
rejectUnauthorized: !this.getIgnoreTls(),
};
debug(`[${this.name}] Prepare Options for axios`);
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
const options = {
url: this.url,
@ -233,8 +265,8 @@ class Monitor extends BeanModel {
options.httpsAgent = new https.Agent(httpsAgentOptions);
}
debug(`[${this.name}] Axios Options: ${JSON.stringify(options)}`);
debug(`[${this.name}] Axios Request`);
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
bean.msg = `${res.status} - ${res.statusText}`;
@ -243,29 +275,30 @@ class Monitor extends BeanModel {
// Check certificate if https is used
let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") {
debug(`[${this.name}] Check cert`);
log.debug("monitor", `[${this.name}] Check cert`);
try {
let tlsInfoObject = checkCertificate(res);
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
debug(`[${this.name}] call sendCertNotification`);
log.debug("monitor", `[${this.name}] call sendCertNotification`);
await this.sendCertNotification(tlsInfoObject);
}
} catch (e) {
if (e.message !== "No TLS certificate in response") {
console.error(e.message);
log.error("monitor", "Caught error");
log.error("monitor", e.message);
}
}
}
if (process.env.TIMELOGGER === "1") {
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
}
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
console.log(res.data);
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
log.info("monitor", res.data);
}
if (this.type === "http") {
@ -304,24 +337,24 @@ class Monitor extends BeanModel {
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
} else if (this.dns_resolve_type === "CNAME" || this.dns_resolve_type === "PTR") {
dnsMessage = dnsRes[0];
} else if (this.dns_resolve_type == "CAA") {
} else if (this.dns_resolve_type === "CAA") {
dnsMessage = dnsRes[0].issue;
} else if (this.dns_resolve_type == "MX") {
} else if (this.dns_resolve_type === "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2);
} else if (this.dns_resolve_type == "NS") {
} else if (this.dns_resolve_type === "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "SOA") {
} else if (this.dns_resolve_type === "SOA") {
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
} else if (this.dns_resolve_type == "SRV") {
} else if (this.dns_resolve_type === "SRV") {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
@ -345,12 +378,9 @@ class Monitor extends BeanModel {
time
]);
debug("heartbeatCount" + heartbeatCount + " " + time);
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
if (heartbeatCount <= 0) {
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
throw new Error("No heartbeat in the time window");
} else {
// No need to insert successful heartbeat for push type, so end here
@ -398,7 +428,6 @@ class Monitor extends BeanModel {
} else {
throw new Error("Server not found on Steam");
}
} else if (this.type === "docker") {
debug(`[${this.name}] Prepare Options for Axios`);
@ -426,7 +455,14 @@ class Monitor extends BeanModel {
bean.status = UP;
bean.msg = "";
}
} else if (this.type === "mqtt") {
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
port: this.port,
username: this.mqttUsername,
password: this.mqttPassword,
interval: this.interval,
});
bean.status = UP;
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
@ -457,7 +493,7 @@ class Monitor extends BeanModel {
}
}
debug(`[${this.name}] Check isImportant`);
log.debug("monitor", `[${this.name}] Check isImportant`);
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
// Mark as important if status changed, ignore pending pings,
@ -465,11 +501,11 @@ class Monitor extends BeanModel {
if (isImportant) {
bean.important = true;
debug(`[${this.name}] sendNotification`);
log.debug("monitor", `[${this.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, this, bean);
// Clear Status Page Cache
debug(`[${this.name}] apicache clear`);
log.debug("monitor", `[${this.name}] apicache clear`);
apicache.clear();
} else {
@ -477,47 +513,48 @@ class Monitor extends BeanModel {
}
if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) {
if (this.retryInterval > 0) {
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}`);
log.warn("monitor", `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}`);
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
}
debug(`[${this.name}] Send to socket`);
log.debug("monitor", `[${this.name}] Send to socket`);
io.to(this.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, this.id, this.user_id);
debug(`[${this.name}] Store`);
log.debug("monitor", `[${this.name}] Store`);
await R.store(bean);
debug(`[${this.name}] prometheus.update`);
log.debug("monitor", `[${this.name}] prometheus.update`);
prometheus.update(bean, tlsInfo);
previousBeat = bean;
if (! this.isStop) {
debug(`[${this.name}] SetTimeout for next check.`);
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
} else {
console.log(`[${this.name}] isStop = true, no next check.`);
log.info("monitor", `[${this.name}] isStop = true, no next check.`);
}
};
/** Get a heartbeat and handle errors */
const safeBeat = async () => {
try {
await beat();
} catch (e) {
console.trace(e);
errorLog(e, false);
console.error("Please report to https://github.com/louislam/uptime-kuma/issues");
UptimeKumaServer.errorLog(e, false);
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
if (! this.isStop) {
console.log("Try to restart the monitor");
log.info("monitor", "Try to restart the monitor");
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
}
}
@ -533,6 +570,7 @@ class Monitor extends BeanModel {
}
}
/** Stop monitor */
stop() {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
@ -540,6 +578,10 @@ class Monitor extends BeanModel {
this.prometheus().remove();
}
/**
* Get a new prometheus instance
* @returns {Prometheus}
*/
prometheus() {
return new Prometheus(this);
}
@ -548,7 +590,7 @@ class Monitor extends BeanModel {
* Helper Method:
* returns URL object for further usage
* returns null if url is invalid
* @returns {null|URL}
* @returns {(null|URL)}
*/
getUrl() {
try {
@ -561,48 +603,54 @@ class Monitor extends BeanModel {
/**
* Store TLS info to database
* @param checkCertificateResult
* @returns {Promise<object>}
* @returns {Promise<Object>}
*/
async updateTlsInfo(checkCertificateResult) {
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
this.id,
]);
if (tls_info_bean == null) {
tls_info_bean = R.dispense("monitor_tls_info");
tls_info_bean.monitor_id = this.id;
if (tlsInfoBean == null) {
tlsInfoBean = R.dispense("monitor_tls_info");
tlsInfoBean.monitor_id = this.id;
} else {
// Clear sent history if the cert changed.
try {
let oldCertInfo = JSON.parse(tls_info_bean.info_json);
let oldCertInfo = JSON.parse(tlsInfoBean.info_json);
let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo;
if (isValidObjects) {
if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) {
debug("Resetting sent_history");
log.debug("monitor", "Resetting sent_history");
await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [
this.id
]);
} else {
debug("No need to reset sent_history");
debug(oldCertInfo.certInfo.fingerprint256);
debug(checkCertificateResult.certInfo.fingerprint256);
log.debug("monitor", "No need to reset sent_history");
log.debug("monitor", oldCertInfo.certInfo.fingerprint256);
log.debug("monitor", checkCertificateResult.certInfo.fingerprint256);
}
} else {
debug("Not valid object");
log.debug("monitor", "Not valid object");
}
} catch (e) { }
}
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tls_info_bean);
tlsInfoBean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tlsInfoBean);
return checkCertificateResult;
}
/**
* Send statistics to clients
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
*/
static async sendStats(io, monitorID, userID) {
const hasClients = getTotalClientInRoom(io, userID) > 0;
@ -612,13 +660,13 @@ class Monitor extends BeanModel {
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
await Monitor.sendCertInfo(io, monitorID, userID);
} else {
debug("No clients in the room, no need to send stats");
log.debug("monitor", "No clients in the room, no need to send stats");
}
}
/**
*
* @param duration : int Hours
* Send the average ping to user
* @param {number} duration Hours
*/
static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
@ -638,12 +686,18 @@ class Monitor extends BeanModel {
io.to(userID).emit("avgPing", monitorID, avgPing);
}
/**
* Send certificate information to client
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
*/
static async sendCertInfo(io, monitorID, userID) {
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
monitorID,
]);
if (tls_info != null) {
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
if (tlsInfo != null) {
io.to(userID).emit("certInfo", monitorID, tlsInfo.info_json);
}
}
@ -651,7 +705,8 @@ class Monitor extends BeanModel {
* Uptime with calculation
* Calculation based on:
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
* @param duration : int Hours
* @param {number} duration Hours
* @param {number} monitorID ID of monitor to calculate
*/
static async calcUptime(duration, monitorID) {
const timeLogger = new TimeLogger();
@ -717,13 +772,23 @@ class Monitor extends BeanModel {
/**
* Send Uptime
* @param duration : int Hours
* @param {number} duration Hours
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
*/
static async sendUptime(duration, io, monitorID, userID) {
const uptime = await this.calcUptime(duration, monitorID);
io.to(userID).emit("uptime", monitorID, duration, uptime);
}
/**
* Has status of monitor changed since last beat?
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
* @param {const} previousBeatStatus Status of the previous beat
* @param {const} currentBeatStatus Status of the current beat
* @returns {boolean} True if is an important beat else false
*/
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
@ -742,6 +807,12 @@ class Monitor extends BeanModel {
return isImportant;
}
/**
* Send a notification about a monitor
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
* @param {Monitor} monitor The monitor to send a notificaton about
* @param {Bean} bean Status information about monitor
*/
static async sendNotification(isFirstBeat, monitor, bean) {
if (!isFirstBeat || bean.status === DOWN) {
const notificationList = await Monitor.getNotificationList(monitor);
@ -757,15 +828,20 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), bean.toJSON());
} catch (e) {
console.error("Cannot send notification to " + notification.name);
console.log(e);
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
}
}
}
}
/**
* Get list of notification providers for a given monitor
* @param {Monitor} monitor Monitor to get notification providers for
* @returns {Promise<LooseObject<any>[]>}
*/
static async getNotificationList(monitor) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
monitor.id,
@ -773,21 +849,33 @@ class Monitor extends BeanModel {
return notificationList;
}
/**
* Send notification about a certificate
* @param {Object} tlsInfoObject Information about certificate
*/
async sendCertNotification(tlsInfoObject) {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
debug("call sendCertNotificationByTargetDays");
log.debug("monitor", "call sendCertNotificationByTargetDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
}
}
/**
* Send a certificate notification when certificate expires in less
* than target days
* @param {number} daysRemaining Number of days remaining on certifcate
* @param {number} targetDays Number of days to alert after
* @param {LooseObject<any>[]} notificationList List of notification providers
* @returns {Promise<void>}
*/
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
if (daysRemaining > targetDays) {
debug(`No need to send cert notification. ${daysRemaining} > ${targetDays}`);
log.debug("monitor", `No need to send cert notification. ${daysRemaining} > ${targetDays}`);
return;
}
@ -801,21 +889,21 @@ class Monitor extends BeanModel {
// Sent already, no need to send again
if (row) {
debug("Sent already, no need to send again");
log.debug("monitor", "Sent already, no need to send again");
return;
}
let sent = false;
debug("Send certificate notification");
log.debug("monitor", "Send certificate notification");
for (let notification of notificationList) {
try {
debug("Sending to " + notification.name);
log.debug("monitor", "Sending to " + notification.name);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
sent = true;
} catch (e) {
console.error("Cannot send cert notification to " + notification.name);
console.error(e);
log.error("monitor", "Cannot send cert notification to " + notification.name);
log.error("monitor", e);
}
}
@ -827,10 +915,15 @@ class Monitor extends BeanModel {
]);
}
} else {
debug("No notification, no need to send cert notification");
log.debug("monitor", "No notification, no need to send cert notification");
}
}
/**
* Get the status of the previous heartbeat
* @param {number} monitorID ID of monitor to check
* @returns {Promise<LooseObject<any>>}
*/
static async getPreviousHeartbeat(monitorID) {
return await R.getRow(`
SELECT status, time FROM heartbeat

View File

@ -1,6 +1,10 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class Proxy extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
id: this._id,

View File

@ -6,6 +6,7 @@ class StatusPage extends BeanModel {
static domainMappingList = { };
/**
* Loads domain mapping from DB
* Return object like this: { "test-uptime.kuma.pet": "default" }
* @returns {Promise<void>}
*/
@ -17,6 +18,12 @@ class StatusPage extends BeanModel {
`);
}
/**
* Send status page list to client
* @param {Server} io io Socket server instance
* @param {Socket} socket Socket.io instance
* @returns {Promise<Bean[]>}
*/
static async sendStatusPageList(io, socket) {
let result = {};
@ -30,6 +37,11 @@ class StatusPage extends BeanModel {
return list;
}
/**
* Update list of domain names
* @param {string[]} domainNameList
* @returns {Promise<void>}
*/
async updateDomainNameList(domainNameList) {
if (!Array.isArray(domainNameList)) {
@ -69,6 +81,10 @@ class StatusPage extends BeanModel {
}
}
/**
* Get list of domain names
* @returns {Object[]}
*/
getDomainNameList() {
let domainList = [];
for (let domain in StatusPage.domainMappingList) {
@ -81,6 +97,10 @@ class StatusPage extends BeanModel {
return domainList;
}
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
async toJSON() {
return {
id: this.id,
@ -92,9 +112,17 @@ class StatusPage extends BeanModel {
published: !!this.published,
showTags: !!this.show_tags,
domainNameList: this.getDomainNameList(),
customCSS: this.custom_css,
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
};
}
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() {
return {
slug: this.slug,
@ -104,15 +132,26 @@ class StatusPage extends BeanModel {
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,
customCSS: this.custom_css,
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
};
}
/**
* Convert slug to status page ID
* @param {string} slug
*/
static async slugToID(slug) {
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
slug
]);
}
/**
* Get path to the icon for the page
* @returns {string}
*/
getIcon() {
if (!this.icon) {
return "/icon.svg";

View File

@ -1,6 +1,11 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class Tag extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
id: this._id,

View File

@ -3,19 +3,30 @@ const passwordHash = require("../password-hash");
const { R } = require("redbean-node");
class User extends BeanModel {
/**
* Reset user password
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
* @param {number} userID ID of user to update
* @param {string} newPassword
* @returns {Promise<void>}
*/
static async resetPassword(userID, newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
userID
]);
}
/**
* Direct execute, no need R.store()
* @param newPassword
* Reset this users password
* @param {string} newPassword
* @returns {Promise<void>}
*/
async resetPassword(newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
this.id
]);
await User.resetPassword(this.id, newPassword);
this.password = newPassword;
}
}
module.exports = User;

View File

@ -13,27 +13,49 @@ let t = {
let instances = [];
/**
* Does a === b
* @param {any} a
* @returns {function(any): boolean}
*/
let matches = function (a) {
return function (b) {
return a === b;
};
};
/**
* Does a!==b
* @param {any} a
* @returns {function(any): boolean}
*/
let doesntMatch = function (a) {
return function (b) {
return !matches(a)(b);
};
};
/**
* Get log duration
* @param {number} d Time in ms
* @param {string} prefix Prefix for log
* @returns {string} Coloured log string
*/
let logDuration = function (d, prefix) {
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
};
/**
* Get safe headers
* @param {Object} res Express response object
* @returns {Object}
*/
function getSafeHeaders(res) {
return res.getHeaders ? res.getHeaders() : res._headers;
}
/** Constructor for ApiCache instance */
function ApiCache() {
let memCache = new MemoryCache();
@ -70,10 +92,10 @@ function ApiCache() {
/**
* Logs a message to the console if the `DEBUG` environment variable is set.
* @param {string} a - The first argument to log.
* @param {string} b - The second argument to log.
* @param {string} c - The third argument to log.
* @param {string} d - The fourth argument to log, and so on... (optional)
* @param {string} a The first argument to log.
* @param {string} b The second argument to log.
* @param {string} c The third argument to log.
* @param {string} d The fourth argument to log, and so on... (optional)
*
* Generated by Trelent
*/
@ -90,8 +112,8 @@ function ApiCache() {
* Returns true if the given request and response should be logged.
* @param {Object} request The HTTP request object.
* @param {Object} response The HTTP response object.
*
* Generated by Trelent
* @param {function(Object, Object):boolean} toggle
* @returns {boolean}
*/
function shouldCacheResponse(request, response, toggle) {
let opt = globalOptions;
@ -116,10 +138,9 @@ function ApiCache() {
}
/**
* Adds a key to the index.
* @param {string} key The key to add.
*
* Generated by Trelent
* Add key to index array
* @param {string} key Key to add
* @param {Object} req Express request object
*/
function addIndexEntries(key, req) {
let groupName = req.apicacheGroup;
@ -135,8 +156,11 @@ function ApiCache() {
/**
* Returns a new object containing only the whitelisted headers.
* @param {Object} headers The original object of header names and values.
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
* @param {Object} headers The original object of header names and
* values.
* @param {string[]} globalOptions.headerWhitelist An array of
* strings representing the whitelisted header names to keep in the
* output object.
*
* Generated by Trelent
*/
@ -152,8 +176,10 @@ function ApiCache() {
}
/**
* Create a cache object
* @param {Object} headers The response headers to filter.
* @returns {Object} A new object containing only the whitelisted response headers.
* @returns {Object} A new object containing only the whitelisted
* response headers.
*
* Generated by Trelent
*/
@ -170,8 +196,9 @@ function ApiCache() {
/**
* Sets a cache value for the given key.
* @param {string} key The cache key to set.
* @param {*} value The cache value to set.
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
* @param {any} value The cache value to set.
* @param {number} duration How long in milliseconds the cached
* response should be valid for (defaults to 1 hour).
*
* Generated by Trelent
*/
@ -199,7 +226,8 @@ function ApiCache() {
/**
* Appends content to the response.
* @param {string|Buffer} content The content to append.
* @param {Object} res Express response object
* @param {(string|Buffer)} content The content to append.
*
* Generated by Trelent
*/
@ -229,11 +257,15 @@ function ApiCache() {
}
/**
* Monkeypatches the response object to add cache control headers and create a cache object.
* @param {Object} req - The request object.
* @param {Object} res - The response object.
*
* Generated by Trelent
* Monkeypatches the response object to add cache control headers
* and create a cache object.
* @param {Object} req Express request object
* @param {Object} res Express response object
* @param {function} next Function to call next
* @param {string} key Key to add response as
* @param {number} duration Time to cache response for
* @param {string} strDuration Duration in string form
* @param {function(Object, Object):boolean} toggle
*/
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
// monkeypatch res.end to create cache object
@ -302,11 +334,15 @@ function ApiCache() {
}
/**
* @param {Request} request
* @param {Response} response
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
*
* Generated by Trelent
* Send a cached response to client
* @param {Request} request Express request object
* @param {Response} response Express response object
* @param {object} cacheObject Cache object to send
* @param {function(Object, Object):boolean} toggle
* @param {function} next Function to call next
* @param {number} duration Not used
* @returns {boolean|undefined} true if the request should be
* cached, false otherwise. If undefined, defaults to true.
*/
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
if (toggle && !toggle(request, response)) {
@ -348,12 +384,19 @@ function ApiCache() {
return response.end(data, cacheObject.encoding);
}
/** Sync caching options */
function syncOptions() {
for (let i in middlewareOptions) {
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
}
}
/**
* Clear key from cache
* @param {string} target Key to clear
* @param {boolean} isAutomatic Is the key being cleared automatically
* @returns {number}
*/
this.clear = function (target, isAutomatic) {
let group = index.groups[target];
let redis = globalOptions.redisClient;
@ -430,10 +473,11 @@ function ApiCache() {
/**
* Converts a duration string to an integer number of milliseconds.
* @param {string} duration - The string to convert.
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
*
* Generated by Trelent
* @param {(string|number)} duration The string to convert.
* @param {number} defaultDuration The default duration to return if
* can't parse duration
* @returns {number} The converted value in milliseconds, or the
* defaultDuration if it can't be parsed.
*/
function parseDuration(duration, defaultDuration) {
if (typeof duration === "number") {
@ -457,17 +501,24 @@ function ApiCache() {
return defaultDuration;
}
/**
* Parse duration
* @param {(number|string)} duration
* @returns {number} Duration parsed to a number
*/
this.getDuration = function (duration) {
return parseDuration(duration, globalOptions.defaultDuration);
};
/**
* Return cache performance statistics (hit rate). Suitable for putting into a route:
* Return cache performance statistics (hit rate). Suitable for
* putting into a route:
* <code>
* app.get('/api/cache/performance', (req, res) => {
* res.json(apicache.getPerformance())
* })
* </code>
* @returns {any[]}
*/
this.getPerformance = function () {
return performanceArray.map(function (p) {
@ -475,6 +526,11 @@ function ApiCache() {
});
};
/**
* Get index of a group
* @param {string} group
* @returns {number}
*/
this.getIndex = function (group) {
if (group) {
return index.groups[group];
@ -483,6 +539,14 @@ function ApiCache() {
}
};
/**
* Express middleware
* @param {(string|number)} strDuration Duration to cache responses
* for.
* @param {function(Object, Object):boolean} middlewareToggle
* @param {Object} localOptions Options for APICache
* @returns
*/
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
let duration = instance.getDuration(strDuration);
let opt = {};
@ -513,35 +577,41 @@ function ApiCache() {
}
/**
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
* 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.
* 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.
* 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.
* 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.
* 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.
* The number of calls that have passed through the
* middleware since the server started.
*/
this.callCount = 0;
@ -551,17 +621,20 @@ function ApiCache() {
this.hitCount = 0;
/**
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
* 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.
* The key from the last cache miss. This is useful in
* identifying which route these statistics apply to.
*/
this.lastCacheMiss = null;
/**
* Return performance statistics
* @returns {Object}
*/
this.report = function () {
return {
@ -579,9 +652,12 @@ function ApiCache() {
};
/**
* 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
* Computes a cache hit rate from an array of hits and
* misses.
* @param {Uint8Array} array An array representing hits and
* misses.
* @returns {?number} a number between 0 and 1, or null if
* the array has no hits or misses
*/
this.hitRate = function (array) {
let hits = 0;
@ -608,15 +684,16 @@ function ApiCache() {
};
/**
* 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.
* 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
* 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;
@ -627,8 +704,10 @@ function ApiCache() {
};
/**
* 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
* 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);
@ -664,6 +743,13 @@ function ApiCache() {
performanceArray.push(perf);
/**
* Cache a request
* @param {Object} req Express request object
* @param {Object} res Express response object
* @param {function} next Function to call next
* @returns {any}
*/
let cache = function (req, res, next) {
function bypass() {
debug("bypass detected, skipping cache.");
@ -771,6 +857,11 @@ function ApiCache() {
return cache;
};
/**
* Process options
* @param {Object} options
* @returns {Object}
*/
this.options = function (options) {
if (options) {
Object.assign(globalOptions, options);
@ -791,6 +882,7 @@ function ApiCache() {
}
};
/** Reset the index */
this.resetIndex = function () {
index = {
all: [],
@ -798,6 +890,11 @@ function ApiCache() {
};
};
/**
* Create a new instance of ApiCache
* @param {Object} config Config to pass
* @returns {ApiCache}
*/
this.newInstance = function (config) {
let instance = new ApiCache();
@ -808,6 +905,7 @@ function ApiCache() {
return instance;
};
/** Clone this instance */
this.clone = function () {
return this.newInstance(this.options());
};

View File

@ -3,6 +3,15 @@ function MemoryCache() {
this.size = 0;
}
/**
*
* @param {string} key Key to store cache as
* @param {any} value Value to store
* @param {number} time Time to store for
* @param {function(any, string)} timeoutCallback Callback to call in
* case of timeout
* @returns {Object}
*/
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
let old = this.cache[key];
let instance = this;
@ -22,6 +31,11 @@ MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
return entry;
};
/**
* Delete a cache entry
* @param {string} key Key to delete
* @returns {null}
*/
MemoryCache.prototype.delete = function (key) {
let entry = this.cache[key];
@ -36,18 +50,32 @@ MemoryCache.prototype.delete = function (key) {
return null;
};
/**
* Get value of key
* @param {string} key
* @returns {Object}
*/
MemoryCache.prototype.get = function (key) {
let entry = this.cache[key];
return entry;
};
/**
* Get value of cache entry
* @param {string} key
* @returns {any}
*/
MemoryCache.prototype.getValue = function (key) {
let entry = this.get(key);
return entry && entry.value;
};
/**
* Clear cache
* @returns {boolean}
*/
MemoryCache.prototype.clear = function () {
Object.keys(this.cache).forEach(function (key) {
this.delete(key);

View File

@ -46,11 +46,11 @@ class Alerta extends NotificationProvider {
resource: monitorJSON["name"],
}, data );
if (heartbeatJSON["status"] == DOWN) {
if (heartbeatJSON["status"] === DOWN) {
datadup.severity = notification.alertaAlertState; // critical
datadup.text = "Service " + monitorJSON["type"] + " is down.";
await axios.post(alertaUrl, datadup, config);
} else if (heartbeatJSON["status"] == UP) {
} else if (heartbeatJSON["status"] === UP) {
datadup.severity = notification.alertaRecoverState; // cleaned
datadup.text = "Service " + monitorJSON["type"] + " is up.";
await axios.post(alertaUrl, datadup, config);

View File

@ -37,6 +37,12 @@ class AliyunSMS extends NotificationProvider {
}
}
/**
* Send the SMS notification
* @param {BeanModel} notification Notification details
* @param {string} msgbody Message template
* @returns {boolean} True if successful else false
*/
async sendSms(notification, msgbody) {
let params = {
PhoneNumbers: notification.phonenumber,
@ -64,13 +70,18 @@ class AliyunSMS extends NotificationProvider {
};
let result = await axios(config);
if (result.data.Message == "OK") {
if (result.data.Message === "OK") {
return true;
}
return false;
}
/** Aliyun request sign */
/**
* Aliyun request sign
* @param {Object} param Parameters object to sign
* @param {string} AccessKeySecret Secret key to sign parameters with
* @returns {string}
*/
sign(param, AccessKeySecret) {
let param2 = {};
let data = [];
@ -82,8 +93,23 @@ class AliyunSMS extends NotificationProvider {
param2[key] = param[key];
}
// Escape more characters than encodeURIComponent does.
// For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded.
// See https://help.aliyun.com/document_detail/315526.html
// This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986)
let moreEscapesTable = function (m) {
return {
"!": "%21",
"*": "%2A",
"'": "%27",
"(": "%28",
")": "%29"
}[m];
};
for (let key in param2) {
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable);
data.push(`${encodeURIComponent(key)}=${value}`);
}
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
@ -93,6 +119,11 @@ class AliyunSMS extends NotificationProvider {
.digest("base64");
}
/**
* Convert status constant to string
* @param {const} status The status constant
* @returns {string}
*/
statusToString(status) {
switch (status) {
case DOWN:

View File

@ -1,14 +1,19 @@
const NotificationProvider = require("./notification-provider");
const child_process = require("child_process");
const childProcess = require("child_process");
class Apprise extends NotificationProvider {
name = "apprise";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
const args = [ "-vv", "-b", msg, notification.appriseURL ];
if (notification.title) {
args.push("-t");
args.push(notification.title);
}
const s = childProcess.spawnSync("apprise", args);
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
if (output) {
@ -16,7 +21,7 @@ class Apprise extends NotificationProvider {
return "Sent Successfully";
}
throw new Error(output)
throw new Error(output);
} else {
return "No output from apprise";
}

View File

@ -21,20 +21,19 @@ class Bark extends NotificationProvider {
name = "Bark";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
var barkEndpoint = notification.barkEndpoint;
let barkEndpoint = notification.barkEndpoint;
// check if the endpoint has a "/" suffix, if so, delete it first
if (barkEndpoint.endsWith("/")) {
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) {
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
let title = "UptimeKuma Monitor Up";
return await this.postNotification(title, msg, barkEndpoint);
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
let title = "UptimeKuma Monitor Down";
return await this.postNotification(title, msg, barkEndpoint);
}
@ -43,13 +42,14 @@ class Bark extends NotificationProvider {
let title = "UptimeKuma Message";
return await this.postNotification(title, msg, barkEndpoint);
}
} catch (error) {
throw error;
}
}
// add additional parameter for better on device styles (iOS 15 optimized)
/**
* Add additional parameter for better on device styles (iOS 15
* optimized)
* @param {string} postUrl URL to append parameters to
* @returns {string}
*/
appendAdditionalParameters(postUrl) {
// grouping all our notifications
postUrl += "?group=" + barkNotificationGroup;
@ -60,7 +60,11 @@ class Bark extends NotificationProvider {
return postUrl;
}
// thrown if failed to check result, result code should be in range 2xx
/**
* Check if result is successful
* @param {Object} result Axios response object
* @throws {Error} The status code is not in range 2xx
*/
checkResult(result) {
if (result.status == null) {
throw new Error("Bark notification failed with invalid response!");
@ -70,6 +74,13 @@ class Bark extends NotificationProvider {
}
}
/**
* Send the message
* @param {string} title Message title
* @param {string} subtitle Message
* @param {string} endpoint Endpoint to send request to
* @returns {string}
*/
async postNotification(title, subtitle, endpoint) {
// url encode title and subtitle
title = encodeURIComponent(title);

View File

@ -12,7 +12,7 @@ class ClickSendSMS extends NotificationProvider {
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString('base64'),
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString("base64"),
"Accept": "text/json",
}
};

View File

@ -37,6 +37,12 @@ class DingDing extends NotificationProvider {
}
}
/**
* Send message to DingDing
* @param {BeanModel} notification
* @param {Object} params Parameters of message
* @returns {boolean} True if successful else false
*/
async sendToDingDing(notification, params) {
let timestamp = Date.now();
@ -50,13 +56,18 @@ class DingDing extends NotificationProvider {
};
let result = await axios(config);
if (result.data.errmsg == "ok") {
if (result.data.errmsg === "ok") {
return true;
}
return false;
}
/** DingDing sign */
/**
* DingDing sign
* @param {Date} timestamp Timestamp of message
* @param {string} secretKey Secret key to sign data with
* @returns {string}
*/
sign(timestamp, secretKey) {
return Crypto
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
@ -64,7 +75,13 @@ class DingDing extends NotificationProvider {
.digest("base64");
}
/**
* Convert status constant to string
* @param {const} status The status constant
* @returns {string}
*/
statusToString(status) {
// TODO: Move to notification-provider.js to avoid repetition in classes
switch (status) {
case DOWN:
return "DOWN";

View File

@ -17,25 +17,32 @@ class Discord extends NotificationProvider {
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, discordtestdata)
};
await axios.post(notification.discordWebhookUrl, discordtestdata);
return okMsg;
}
let url;
let address;
if (monitorJSON["type"] === "port") {
url = monitorJSON["hostname"];
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
url += ":" + monitorJSON["port"];
address += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"];
break;
default:
address = monitorJSON["url"];
break;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == DOWN) {
if (heartbeatJSON["status"] === DOWN) {
let discorddowndata = {
username: discordDisplayName,
embeds: [{
@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["name"],
},
{
name: "Service URL",
value: url,
name: "Service URL / Address",
value: address,
},
{
name: "Time (UTC)",
@ -61,16 +68,16 @@ class Discord extends NotificationProvider {
},
],
}],
}
};
if (notification.discordPrefixMessage) {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discorddowndata)
await axios.post(notification.discordWebhookUrl, discorddowndata);
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
} else if (heartbeatJSON["status"] === UP) {
let discordupdata = {
username: discordDisplayName,
embeds: [{
@ -84,7 +91,7 @@ class Discord extends NotificationProvider {
},
{
name: "Service URL",
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
value: address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
},
{
name: "Time (UTC)",
@ -96,17 +103,17 @@ class Discord extends NotificationProvider {
},
],
}],
}
};
if (notification.discordPrefixMessage) {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discordupdata)
await axios.post(notification.discordWebhookUrl, discordupdata);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}

View File

@ -21,7 +21,7 @@ class Feishu extends NotificationProvider {
return okMsg;
}
if (heartbeatJSON["status"] == DOWN) {
if (heartbeatJSON["status"] === DOWN) {
let downdata = {
msg_type: "post",
content: {
@ -48,7 +48,7 @@ class Feishu extends NotificationProvider {
return okMsg;
}
if (heartbeatJSON["status"] == UP) {
if (heartbeatJSON["status"] === UP) {
let updata = {
msg_type: "post",
content: {

View File

@ -13,11 +13,11 @@ class GoogleChat extends NotificationProvider {
try {
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
let textMsg = ''
let textMsg = "";
if (heartbeatJSON && heartbeatJSON.status === UP) {
textMsg = `✅ Application is back online\n`;
textMsg = "✅ Application is back online\n";
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
textMsg = `🔴 Application went down\n`;
textMsg = "🔴 Application went down\n";
}
if (monitorJSON && monitorJSON.name) {

View File

@ -15,7 +15,7 @@ class Gotify extends NotificationProvider {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
})
});
return okMsg;

View File

@ -25,9 +25,9 @@ class Line extends NotificationProvider {
"text": "Test Successful!"
}
]
}
await axios.post(lineAPIUrl, testMessage, config)
} else if (heartbeatJSON["status"] == DOWN) {
};
await axios.post(lineAPIUrl, testMessage, config);
} else if (heartbeatJSON["status"] === DOWN) {
let downMessage = {
"to": notification.lineUserID,
"messages": [
@ -36,9 +36,9 @@ class Line extends NotificationProvider {
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, downMessage, config)
} else if (heartbeatJSON["status"] == UP) {
};
await axios.post(lineAPIUrl, downMessage, config);
} else if (heartbeatJSON["status"] === UP) {
let upMessage = {
"to": notification.lineUserID,
"messages": [
@ -47,12 +47,12 @@ class Line extends NotificationProvider {
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, upMessage, config)
};
await axios.post(lineAPIUrl, upMessage, config);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}
}

View File

@ -8,38 +8,38 @@ class LunaSea extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
try {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(lunaseadevice, testdata)
};
await axios.post(lunaseadevice, testdata);
return okMsg;
}
if (heartbeatJSON["status"] == DOWN) {
if (heartbeatJSON["status"] === DOWN) {
let downdata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, downdata)
};
await axios.post(lunaseadevice, downdata);
return okMsg;
}
if (heartbeatJSON["status"] == UP) {
if (heartbeatJSON["status"] === UP) {
let updata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, updata)
};
await axios.post(lunaseadevice, updata);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}

View File

@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const Crypto = require("crypto");
const { debug } = require("../../src/util");
const { log } = require("../../src/util");
class Matrix extends NotificationProvider {
name = "matrix";
@ -17,11 +17,11 @@ class Matrix extends NotificationProvider {
.slice(0, size)
);
debug("Random String: " + randomString);
log.debug("notification", "Random String: " + randomString);
const roomId = encodeURIComponent(notification.internalRoomId);
debug("Matrix Room ID: " + roomId);
log.debug("notification", "Matrix Room ID: " + roomId);
try {
let config = {

View File

@ -29,7 +29,7 @@ class Mattermost extends NotificationProvider {
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;
if (heartbeatJSON["status"] == DOWN) {
if (heartbeatJSON["status"] === DOWN) {
let mattermostdowndata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
@ -73,7 +73,7 @@ class Mattermost extends NotificationProvider {
mattermostdowndata
);
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
} else if (heartbeatJSON["status"] === UP) {
let mattermostupdata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",

View File

@ -7,17 +7,23 @@ class NotificationProvider {
name = undefined;
/**
* @param notification : BeanModel
* @param msg : string General Message
* @param monitorJSON : object Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
* Send a notification
* @param {BeanModel} notification
* @param {string} msg General Message
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Return Successful Message
* Throw Error with fail msg
* @throws Error with fail msg
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
throw new Error("Have to override Notification.send(...)");
}
/**
* Throws an error
* @param {any} error The error to throw
* @throws {any} The error specified
*/
throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
@ -25,11 +31,11 @@ class NotificationProvider {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data)
msg += JSON.stringify(error.response.data);
}
}
throw new Error(msg)
throw new Error(msg);
}
}

View File

@ -10,7 +10,7 @@ class Octopush extends NotificationProvider {
try {
// Default - V2
if (notification.octopushVersion == 2 || !notification.octopushVersion) {
if (notification.octopushVersion === 2 || !notification.octopushVersion) {
let config = {
headers: {
"api-key": notification.octopushAPIKey,
@ -30,14 +30,14 @@ class Octopush extends NotificationProvider {
"purpose": "alert",
"sender": notification.octopushSenderName
};
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
} else if (notification.octopushVersion == 1) {
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
} else if (notification.octopushVersion === 1) {
let data = {
"user_login": notification.octopushDMLogin,
"api_key": notification.octopushDMAPIKey,
"sms_recipients": notification.octopushDMPhoneNumber,
"sms_sender": notification.octopushDMSenderName,
"sms_type": (notification.octopushDMSMSType == "sms_premium") ? "FR" : "XXX",
"sms_type": (notification.octopushDMSMSType === "sms_premium") ? "FR" : "XXX",
"transactional": "1",
//octopush not supporting non ascii char
"sms_text": msg.replace(/[^\x00-\x7F]/g, ""),
@ -49,7 +49,7 @@ class Octopush extends NotificationProvider {
},
params: data
};
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config)
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
} else {
throw new Error("Unknown Octopush version!");
}

View File

@ -0,0 +1,45 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class OneBot extends NotificationProvider {
name = "OneBot";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let httpAddr = notification.httpAddr;
if (!httpAddr.startsWith("http")) {
httpAddr = "http://" + httpAddr;
}
if (!httpAddr.endsWith("/")) {
httpAddr += "/";
}
let onebotAPIUrl = httpAddr + "send_msg";
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.accessToken,
}
};
let pushText = "UptimeKuma Alert: " + msg;
let data = {
"auto_escape": true,
"message": pushText,
};
if (notification.msgType === "group") {
data["message_type"] = "group";
data["group_id"] = notification.recieverId;
} else {
data["message_type"] = "private";
data["user_id"] = notification.recieverId;
}
await axios.post(onebotAPIUrl, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = OneBot;

View File

@ -12,7 +12,7 @@ class PromoSMS extends NotificationProvider {
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString('base64'),
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString("base64"),
"Accept": "text/json",
}
};

View File

@ -23,26 +23,26 @@ class Pushbullet extends NotificationProvider {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(pushbulletUrl, testdata, config)
} else if (heartbeatJSON["status"] == DOWN) {
};
await axios.post(pushbulletUrl, testdata, config);
} else if (heartbeatJSON["status"] === DOWN) {
let downdata = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, downdata, config)
} else if (heartbeatJSON["status"] == UP) {
};
await axios.post(pushbulletUrl, downdata, config);
} else if (heartbeatJSON["status"] === UP) {
let updata = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, updata, config)
};
await axios.post(pushbulletUrl, updata, config);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}
}

View File

@ -0,0 +1,52 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class PushDeer extends NotificationProvider {
name = "PushDeer";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let pushdeerlink = "https://api2.pushdeer.com/message/push";
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
let title;
if (valid && heartbeatJSON.status === UP) {
title = "## Uptime Kuma: " + monitorJSON.name + " up";
} else if (valid && heartbeatJSON.status === DOWN) {
title = "## Uptime Kuma: " + monitorJSON.name + " down";
} else {
title = "## Uptime Kuma Message";
}
let data = {
"pushkey": notification.pushdeerKey,
"text": title,
"desp": msg.replace(/\n/g, "\n\n"),
"type": "markdown",
};
try {
let res = await axios.post(pushdeerlink, data);
if ("error" in res.data) {
let error = res.data.error;
this.throwGeneralAxiosError(error);
}
if (res.data.content.result.length === 0) {
let error = "Invalid PushDeer key";
this.throwGeneralAxiosError(error);
} else if (JSON.parse(res.data.content.result[0]).success !== "ok") {
let error = "Unknown error";
this.throwGeneralAxiosError(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = PushDeer;

View File

@ -19,10 +19,10 @@ class Pushy extends NotificationProvider {
"badge": 1,
"sound": "ping.aiff"
}
})
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}
}

View File

@ -2,7 +2,7 @@ const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const Slack = require("./slack");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
class RocketChat extends NotificationProvider {

View File

@ -16,10 +16,10 @@ class Signal extends NotificationProvider {
};
let config = {};
await axios.post(notification.signalURL, data, config)
await axios.post(notification.signalURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}
}

View File

@ -10,6 +10,7 @@ class Slack extends NotificationProvider {
/**
* Deprecated property notification.slackbutton
* Set it as primary base url if this is not yet set.
* @param {string} url The primary base URL to use
*/
static async deprecateURL(url) {
let currentPrimaryBaseURL = await setting("primaryBaseURL");

View File

@ -1,6 +1,6 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { DOWN } = require("../../src/util");
class SMTP extends NotificationProvider {

View File

@ -5,6 +5,12 @@ const { DOWN, UP } = require("../../src/util");
class Teams extends NotificationProvider {
name = "teams";
/**
* Generate the message to send
* @param {const} status The status constant
* @param {string} monitorName Name of monitor
* @returns {string}
*/
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down`;
@ -14,6 +20,11 @@ class Teams extends NotificationProvider {
return "Notification";
};
/**
* Select theme color to use based on status
* @param {const} status The status constant
* @returns {string} Selected color in hex RGB format
*/
_getThemeColor = (status) => {
if (status === DOWN) {
return "ff0000";
@ -24,6 +35,14 @@ class Teams extends NotificationProvider {
return "008cff";
};
/**
* Generate payload for notification
* @param {const} status The status of the monitor
* @param {string} monitorMessage Message to send
* @param {string} monitorName Name of monitor affected
* @param {string} monitorUrl URL of monitor affected
* @returns {Object}
*/
_notificationPayloadFactory = ({
status,
monitorMessage,
@ -74,10 +93,21 @@ class Teams extends NotificationProvider {
};
};
/**
* Send the notification
* @param {string} webhookUrl URL to send the request to
* @param {Object} payload Payload generated by _notificationPayloadFactory
*/
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, payload);
};
/**
* Send a general notification
* @param {string} webhookUrl URL to send request to
* @param {string} msg Message to send
* @returns {Promise<void>}
*/
_handleGeneralNotification = (webhookUrl, msg) => {
const payload = this._notificationPayloadFactory({
monitorMessage: msg

View File

@ -12,10 +12,10 @@ class TechulusPush extends NotificationProvider {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
})
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}
}

View File

@ -14,12 +14,12 @@ class Telegram extends NotificationProvider {
chat_id: notification.telegramChatID,
text: msg,
},
})
});
return okMsg;
} catch (error) {
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
throw new Error(msg)
let msg = (error.response.data.description) ? error.response.data.description : "Error without description";
throw new Error(msg);
}
}
}

View File

@ -24,17 +24,17 @@ class Webhook extends NotificationProvider {
config = {
headers: finalData.getHeaders(),
}
};
} else {
finalData = data;
}
await axios.post(notification.webhookURL, finalData, config)
await axios.post(notification.webhookURL, finalData, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
this.throwGeneralAxiosError(error);
}
}

View File

@ -24,12 +24,18 @@ class WeCom extends NotificationProvider {
}
}
/**
* Generate the message to send
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @param {string} msg General message
* @returns {Object}
*/
composeMessage(heartbeatJSON, msg) {
let title;
if (msg != null && heartbeatJSON != null && heartbeatJSON['status'] == UP) {
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up";
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down";
}
if (msg != null) {

View File

@ -24,19 +24,23 @@ const Feishu = require("./notification-providers/feishu");
const AliyunSms = require("./notification-providers/aliyun-sms");
const DingDing = require("./notification-providers/dingding");
const Bark = require("./notification-providers/bark");
const { log } = require("../src/util");
const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
const OneBot = require("./notification-providers/onebot");
const PushDeer = require("./notification-providers/pushdeer");
class Notification {
providerList = {};
/** Initialize the notification providers */
static init() {
console.log("Prepare Notification Providers");
log.info("notification", "Prepare Notification Providers");
this.providerList = {};
@ -72,6 +76,8 @@ class Notification {
new GoogleChat(),
new Gorush(),
new Alerta(),
new OneBot(),
new PushDeer(),
];
for (let item of list) {
@ -87,13 +93,13 @@ class Notification {
}
/**
*
* @param notification : BeanModel
* @param msg : string General Message
* @param monitorJSON : object Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
* Send a notification
* @param {BeanModel} notification
* @param {string} msg General Message
* @param {Object} monitorJSON Monitor details (For Up/Down only)
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Successful msg
* Throw Error with fail msg
* @throws Error with fail msg
*/
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
if (this.providerList[notification.type]) {
@ -103,28 +109,35 @@ class Notification {
}
}
/**
* Save a notification
* @param {Object} notification Notification to save
* @param {?number} notificationID ID of notification to update
* @param {number} userID ID of user who adds notification
* @returns {Promise<Bean>}
*/
static async save(notification, notificationID, userID) {
let bean
let bean;
if (notificationID) {
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID,
userID,
])
]);
if (! bean) {
throw new Error("notification not found")
throw new Error("notification not found");
}
} else {
bean = R.dispense("notification")
bean = R.dispense("notification");
}
bean.name = notification.name;
bean.user_id = userID;
bean.config = JSON.stringify(notification);
bean.is_default = notification.isDefault || false;
await R.store(bean)
await R.store(bean);
if (notification.applyExisting) {
await applyNotificationEveryMonitor(bean.id, userID);
@ -133,19 +146,29 @@ class Notification {
return bean;
}
/**
* Delete a notification
* @param {number} notificationID ID of notification to delete
* @param {number} userID ID of user who created notification
* @returns {Promise<void>}
*/
static async delete(notificationID, userID) {
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID,
userID,
])
]);
if (! bean) {
throw new Error("notification not found")
throw new Error("notification not found");
}
await R.trash(bean)
await R.trash(bean);
}
/**
* Check if apprise exists
* @returns {boolean} Does the command apprise exist?
*/
static checkApprise() {
let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync("apprise");
@ -155,11 +178,10 @@ class Notification {
}
/**
* Adds a new monitor to the database.
* @param {number} userID The ID of the user that owns this monitor.
* @param {string} name The name of this monitor.
*
* Generated by Trelent
* Apply the notification to every monitor
* @param {number} notificationID ID of notification to apply
* @param {number} userID ID of user who created notification
* @returns {Promise<void>}
*/
async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
@ -170,17 +192,17 @@ async function applyNotificationEveryMonitor(notificationID, userID) {
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
monitors[i].id,
notificationID,
])
]);
if (! checkNotification) {
let relation = R.dispense("monitor_notification");
relation.monitor_id = monitors[i].id;
relation.notification_id = notificationID;
await R.store(relation)
await R.store(relation);
}
}
}
module.exports = {
Notification,
}
};

View File

@ -2,22 +2,42 @@ const passwordHashOld = require("password-hash");
const bcrypt = require("bcryptjs");
const saltRounds = 10;
/**
* Hash a password
* @param {string} password
* @returns {string}
*/
exports.generate = function (password) {
return bcrypt.hashSync(password, saltRounds);
}
};
/**
* Verify a password against a hash
* @param {string} password
* @param {string} hash
* @returns {boolean} Does the password match the hash?
*/
exports.verify = function (password, hash) {
if (isSHA1(hash)) {
return passwordHashOld.verify(password, hash)
return passwordHashOld.verify(password, hash);
}
return bcrypt.compareSync(password, hash);
}
};
/**
* Is the hash a SHA1 hash
* @param {string} hash
* @returns {boolean}
*/
function isSHA1(hash) {
return (typeof hash === "string" && hash.startsWith("sha1"))
return (typeof hash === "string" && hash.startsWith("sha1"));
}
/**
* Does the hash need to be rehashed?
* @returns {boolean}
*/
exports.needRehash = function (hash) {
return isSHA1(hash);
}
};

View File

@ -9,11 +9,10 @@ const util = require("./util-server");
module.exports = Ping;
/**
* @param {string} host - The host to ping
* @param {object} [options] - Options for the ping command
* Constructor for ping class
* @param {string} host Host to ping
* @param {object} [options] Options for the ping command
* @param {array|string} [options.args] - Arguments to pass to the ping command
*
* Generated by Trelent
*/
function Ping(host, options) {
if (!host) {
@ -82,8 +81,17 @@ function Ping(host, options) {
Ping.prototype.__proto__ = events.EventEmitter.prototype;
// SEND A PING
// ===========
/**
* Callback for send
* @callback pingCB
* @param {any} err Any error encountered
* @param {number} ms Ping time in ms
*/
/**
* Send a ping
* @param {pingCB} callback Callback to call with results
*/
Ping.prototype.send = function (callback) {
let self = this;
callback = callback || function (err, ms) {
@ -157,8 +165,10 @@ Ping.prototype.send = function (callback) {
}
};
// CALL Ping#send(callback) ON A TIMER
// ===================================
/**
* Ping every interval
* @param {pingCB} callback Callback to call with results
*/
Ping.prototype.start = function (callback) {
let self = this;
this._i = setInterval(function () {
@ -167,8 +177,7 @@ Ping.prototype.start = function (callback) {
self.send(callback);
};
// STOP SENDING PINGS
// ==================
/** Stop sending pings */
Ping.prototype.stop = function () {
clearInterval(this._i);
};
@ -177,7 +186,7 @@ Ping.prototype.stop = function () {
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
* Thank @pemassi
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
* @param data
* @param {any} data
* @returns {string}
*/
function convertOutput(data) {

View File

@ -1,4 +1,5 @@
const PrometheusClient = require("prom-client");
const { log } = require("../src/util");
const commonLabels = [
"monitor_name",
@ -8,32 +9,35 @@ const commonLabels = [
"monitor_port",
];
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
const monitorCertDaysRemaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining",
help: "The number of days remaining until the certificate expires",
labelNames: commonLabels
});
const monitor_cert_is_valid = new PrometheusClient.Gauge({
const monitorCertIsValid = new PrometheusClient.Gauge({
name: "monitor_cert_is_valid",
help: "Is the certificate still valid? (1 = Yes, 0= No)",
labelNames: commonLabels
});
const monitor_response_time = new PrometheusClient.Gauge({
const monitorResponseTime = new PrometheusClient.Gauge({
name: "monitor_response_time",
help: "Monitor Response Time (ms)",
labelNames: commonLabels
});
const monitor_status = new PrometheusClient.Gauge({
const monitorStatus = new PrometheusClient.Gauge({
name: "monitor_status",
help: "Monitor Status (1 = UP, 0= DOWN)",
labelNames: commonLabels
});
class Prometheus {
monitorLabelValues = {}
monitorLabelValues = {};
/**
* @param {Object} monitor Monitor object to monitor
*/
constructor(monitor) {
this.monitorLabelValues = {
monitor_name: monitor.name,
@ -44,54 +48,63 @@ class Prometheus {
};
}
/**
* Update the metrics page
* @param {Object} heartbeat Heartbeat details
* @param {Object} tlsInfo TLS details
*/
update(heartbeat, tlsInfo) {
if (typeof tlsInfo !== "undefined") {
try {
let is_valid = 0;
if (tlsInfo.valid == true) {
is_valid = 1;
let isValid;
if (tlsInfo.valid === true) {
isValid = 1;
} else {
is_valid = 0;
isValid = 0;
}
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid);
monitorCertIsValid.set(this.monitorLabelValues, isValid);
} catch (e) {
console.error(e);
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
try {
if (tlsInfo.certInfo != null) {
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
}
} catch (e) {
console.error(e);
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
}
try {
monitor_status.set(this.monitorLabelValues, heartbeat.status);
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) {
console.error(e);
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
try {
if (typeof heartbeat.ping === "number") {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping);
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
} else {
// Is it good?
monitor_response_time.set(this.monitorLabelValues, -1);
monitorResponseTime.set(this.monitorLabelValues, -1);
}
} catch (e) {
console.error(e);
log.error("prometheus", "Caught error");
log.error("prometheus", e);
}
}
remove() {
try {
monitor_cert_days_remaining.remove(this.monitorLabelValues);
monitor_cert_is_valid.remove(this.monitorLabelValues);
monitor_response_time.remove(this.monitorLabelValues);
monitor_status.remove(this.monitorLabelValues);
monitorCertDaysRemaining.remove(this.monitorLabelValues);
monitorCertIsValid.remove(this.monitorLabelValues);
monitorResponseTime.remove(this.monitorLabelValues);
monitorStatus.remove(this.monitorLabelValues);
} catch (e) {
console.error(e);
}

View File

@ -3,11 +3,11 @@ const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { debug } = require("../src/util");
const server = require("./server");
const { UptimeKumaServer } = require("./uptime-kuma-server");
class Proxy {
static SUPPORTED_PROXY_PROTOCOLS = ["http", "https", "socks", "socks5", "socks4"]
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
/**
* Saves and updates given proxy entity
@ -151,6 +151,8 @@ class Proxy {
* @returns {Promise<void>}
*/
static async reloadProxy() {
const server = UptimeKumaServer.getInstance();
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
for (let monitorID in server.monitorList) {

View File

@ -1,15 +1,30 @@
const { RateLimiter } = require("limiter");
const { debug } = require("../src/util");
const { log } = require("../src/util");
class KumaRateLimiter {
/**
* @param {Object} config Rate limiter configuration object
*/
constructor(config) {
this.errorMessage = config.errorMessage;
this.rateLimiter = new RateLimiter(config);
}
/**
* Callback for pass
* @callback passCB
* @param {Object} err Too many requests
*/
/**
* Should the request be passed through
* @param {passCB} callback
* @param {number} [num=1] Number of tokens to remove
* @returns {Promise<boolean>}
*/
async pass(callback, num = 1) {
const remainingRequests = await this.removeTokens(num);
debug("Rate Limit (remainingRequests):" + remainingRequests);
log.info("rate-limit", "remaining requests: " + remainingRequests);
if (remainingRequests < 0) {
if (callback) {
callback({
@ -22,6 +37,11 @@ class KumaRateLimiter {
return true;
}
/**
* Remove a given number of tokens
* @param {number} [num=1] Number of tokens to remove
* @returns {Promise<number>}
*/
async removeTokens(num = 1) {
return await this.rateLimiter.removeTokens(num);
}

View File

@ -1,15 +1,19 @@
let express = require("express");
const { allowDevAllOrigin, getSettings, setting } = require("../util-server");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server");
const { R } = require("redbean-node");
const server = require("../server");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, flipStatus, debug } = require("../../src/util");
const { UP, DOWN, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { makeBadge } = require("badge-maker");
const { badgeConstants } = require("../config");
let router = express.Router();
let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
let io = server.io;
router.get("/api/entry-page", async (request, response) => {
@ -33,6 +37,8 @@ router.get("/api/push/:pushToken", async (request, response) => {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = request.query.ping || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken
@ -44,7 +50,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
let status = UP;
if (monitor.isUpsideDown()) {
status = flipStatus(status);
}
@ -62,8 +67,8 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
debug("PreviousStatus: " + previousStatus);
debug("Current Status: " + status);
log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status);
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
bean.monitor_id = monitor.id;
@ -124,7 +129,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
debug("Show Tags???" + showTags);
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
@ -195,14 +200,187 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
}
});
/**
* Default is published
* @returns {Promise<boolean>}
*/
async function isPublished() {
return true;
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response);
const {
label,
upLabel = "Up",
downLabel = "Down",
upColor = badgeConstants.defaultUpColor,
downColor = badgeConstants.defaultDownColor,
style = badgeConstants.defaultStyle,
value, // for demo purpose only
} = request.query;
try {
const requestedMonitorId = parseInt(request.params.id, 10);
const overrideValue = value !== undefined ? parseInt(value) : undefined;
let publicMonitor = await R.getRow(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND monitor_group.monitor_id = ?
AND public = 1
`,
[ requestedMonitorId ]
);
const badgeValues = { style };
if (!publicMonitor) {
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
badgeValues.message = "N/A";
badgeValues.color = badgeConstants.naColor;
} else {
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
badgeValues.color = state ? upColor : downColor;
badgeValues.message = label ?? state ? upLabel : downLabel;
}
// build the svg based on given values
const svg = makeBadge(badgeValues);
response.type("image/svg+xml");
response.send(svg);
} catch (error) {
send403(response, error.message);
}
});
router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response);
const {
label,
labelPrefix,
labelSuffix = badgeConstants.defaultUptimeLabelSuffix,
prefix,
suffix = badgeConstants.defaultUptimeValueSuffix,
color,
labelColor,
style = badgeConstants.defaultStyle,
value, // for demo purpose only
} = request.query;
try {
const requestedMonitorId = parseInt(request.params.id, 10);
// if no duration is given, set value to 24 (h)
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
const overrideValue = value && parseFloat(value);
let publicMonitor = await R.getRow(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND monitor_group.monitor_id = ?
AND public = 1
`,
[ requestedMonitorId ]
);
const badgeValues = { style };
if (!publicMonitor) {
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
badgeValues.message = "N/A";
badgeValues.color = badgeConstants.naColor;
} else {
const uptime = overrideValue ?? await Monitor.calcUptime(
requestedDuration,
requestedMonitorId
);
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
const cleanUptime = parseFloat(uptime.toPrecision(4));
// use a given, custom color or calculate one based on the uptime value
badgeValues.color = color ?? percentageToColor(uptime);
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
badgeValues.labelColor = labelColor ?? "";
// build a lable string. If a custom label is given, override the default one (requestedDuration)
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
}
// build the SVG based on given values
const svg = makeBadge(badgeValues);
response.type("image/svg+xml");
response.send(svg);
} catch (error) {
send403(response, error.message);
}
});
router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response);
const {
label,
labelPrefix,
labelSuffix = badgeConstants.defaultPingLabelSuffix,
prefix,
suffix = badgeConstants.defaultPingValueSuffix,
color = badgeConstants.defaultPingColor,
labelColor,
style = badgeConstants.defaultStyle,
value, // for demo purpose only
} = request.query;
try {
const requestedMonitorId = parseInt(request.params.id, 10);
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
const overrideValue = value && parseFloat(value);
const publicAvgPing = parseInt(await R.getCell(`
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
WHERE monitor_group.group_id = \`group\`.id
AND heartbeat.time > DATETIME('now', ? || ' hours')
AND heartbeat.ping IS NOT NULL
AND public = 1
AND heartbeat.monitor_id = ?
`,
[ -requestedDuration, requestedMonitorId ]
));
const badgeValues = { style };
if (!publicAvgPing) {
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
badgeValues.message = "N/A";
badgeValues.color = badgeConstants.naColor;
} else {
const avgPing = parseInt(overrideValue ?? publicAvgPing);
badgeValues.color = color;
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
badgeValues.labelColor = labelColor ?? "";
// build a lable string. If a custom label is given, override the default one (requestedDuration)
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
}
// build the SVG based on given values
const svg = makeBadge(badgeValues);
response.type("image/svg+xml");
response.send(svg);
} catch (error) {
send403(response, error.message);
}
});
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
function send403(res, msg = "") {
res.status(403).json({
"status": "fail",

View File

@ -1,3 +1,8 @@
/*
* Uptime Kuma Server
* node "server/server.js"
* DO NOT require("./server") in other modules, it likely creates circular dependency!
*/
console.log("Welcome to Uptime Kuma");
// Check Node.js Version
@ -11,81 +16,63 @@ if (nodeVersion < requiredVersion) {
}
const args = require("args-parser")(process.argv);
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
const config = require("./config");
debug(args);
log.info("server", "Welcome to Uptime Kuma");
log.debug("server", "Arguments");
log.debug("server", args);
if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
console.log("Node Env: " + process.env.NODE_ENV);
log.info("server", "Node Env: " + process.env.NODE_ENV);
console.log("Importing Node libraries");
log.info("server", "Importing Node libraries");
const fs = require("fs");
const http = require("http");
const https = require("https");
console.log("Importing 3rd-party libraries");
debug("Importing express");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
debug("Importing socket.io");
const { Server } = require("socket.io");
debug("Importing redbean-node");
log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node");
debug("Importing jsonwebtoken");
log.debug("server", "Importing jsonwebtoken");
const jwt = require("jsonwebtoken");
debug("Importing http-graceful-shutdown");
log.debug("server", "Importing http-graceful-shutdown");
const gracefulShutdown = require("http-graceful-shutdown");
debug("Importing prometheus-api-metrics");
log.debug("server", "Importing prometheus-api-metrics");
const prometheusAPIMetrics = require("prometheus-api-metrics");
debug("Importing compare-versions");
log.debug("server", "Importing compare-versions");
const compareVersions = require("compare-versions");
const { passwordStrength } = require("check-password-strength");
debug("Importing 2FA Modules");
log.debug("server", "Importing 2FA Modules");
const notp = require("notp");
const base32 = require("thirty-two");
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
* @type {UptimeKumaServer}
*/
class UptimeKumaServer {
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
entryPage = "dashboard";
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance(args);
const io = module.exports.io = server.io;
const app = server.app;
async sendMonitorList(socket) {
let list = await getMonitorJSONList(socket.userID);
io.to(socket.userID).emit("monitorList", list);
return list;
}
}
const server = module.exports = new UptimeKumaServer();
console.log("Importing this project modules");
debug("Importing Monitor");
log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
debug("Importing Notification");
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
Notification.init();
debug("Importing Proxy");
log.debug("server", "Importing Proxy");
const { Proxy } = require("./proxy");
debug("Importing Database");
log.debug("server", "Importing Database");
const Database = require("./database");
debug("Importing Background Jobs");
log.debug("server", "Importing Background Jobs");
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
@ -94,7 +81,7 @@ const { login } = require("./auth");
const passwordHash = require("./password-hash");
const checkVersion = require("./check-version");
console.info("Version: " + checkVersion.version);
log.info("server", "Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
@ -103,21 +90,18 @@ let hostEnv = FBSD ? null : process.env.HOST;
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
if (hostname) {
console.log("Custom hostname: " + hostname);
log.info("server", "Custom hostname: " + hostname);
}
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const disableFrameSameOrigin = args["disable-frame-sameorigin"] || !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || false;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
// 2FA / notp verification defaults
const twofa_verification_opts = {
const twoFAVerifyOptions = {
"window": 1,
"time": 30
};
@ -129,28 +113,9 @@ const twofa_verification_opts = {
const testMode = !!args["test"] || false;
if (config.demoMode) {
console.log("==== Demo Mode ====");
log.info("server", "==== Demo Mode ====");
}
console.log("Creating express and socket.io instance");
const app = express();
let httpServer;
if (sslKey && sslCert) {
console.log("Server Type: HTTPS");
httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, app);
} else {
console.log("Server Type: HTTP");
httpServer = http.createServer(app);
}
const io = new Server(httpServer);
module.exports.io = io;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
@ -171,12 +136,6 @@ app.use(function (req, res, next) {
next();
});
/**
* Total WebSocket client connected to server currently, no actual use
* @type {number}
*/
let totalClient = 0;
/**
* Use for decode the auth object
* @type {null}
@ -200,7 +159,7 @@ try {
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
console.error("Error: Cannot find 'dist/index.html', did you install correctly?");
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
@ -212,7 +171,7 @@ try {
exports.entryPage = await setting("entryPage");
await StatusPage.loadDomainMappingList();
console.log("Adding route");
log.info("server", "Adding route");
// ***************************
// Normal Router here
@ -232,6 +191,13 @@ try {
}
});
if (isDev) {
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.body);
response.send("OK");
});
}
// Robots.txt
app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
@ -270,54 +236,55 @@ try {
}
});
console.log("Adding socket handler");
log.info("server", "Adding socket handler");
io.on("connection", async (socket) => {
sendInfo(socket);
totalClient++;
if (needSetup) {
console.log("Redirect to setup page");
log.info("server", "Redirect to setup page");
socket.emit("setup");
}
socket.on("disconnect", () => {
totalClient--;
});
// ***************************
// Public Socket API
// ***************************
socket.on("loginByToken", async (token, callback) => {
log.info("auth", `Login by token. IP=${getClientIp(socket)}`);
try {
let decoded = jwt.verify(token, jwtSecret);
console.log("Username from JWT: " + decoded.username);
log.info("auth", "Username from JWT: " + decoded.username);
let user = await R.findOne("user", " username = ? AND active = 1 ", [
decoded.username,
]);
if (user) {
debug("afterLogin");
log.debug("auth", "afterLogin");
afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
debug("afterLogin ok");
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`);
callback({
ok: true,
});
} else {
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`);
callback({
ok: false,
msg: "The user is inactive or deleted.",
});
}
} catch (error) {
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`);
callback({
ok: false,
msg: "Invalid token.",
@ -327,7 +294,7 @@ try {
});
socket.on("login", async (data, callback) => {
console.log("Login");
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`);
// Checking
if (typeof callback !== "function") {
@ -340,14 +307,18 @@ try {
// Login Rate Limit
if (! await loginRateLimiter.pass(callback)) {
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`);
return;
}
let user = await login(data.username, data.password);
if (user) {
if (user.twofa_status == 0) {
if (user.twofa_status === 0) {
afterLogin(socket, user);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
callback({
ok: true,
token: jwt.sign({
@ -356,14 +327,17 @@ try {
});
}
if (user.twofa_status == 1 && !data.token) {
if (user.twofa_status === 1 && !data.token) {
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
callback({
tokenRequired: true,
});
}
if (data.token) {
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
let verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== data.token && verify) {
afterLogin(socket, user);
@ -373,6 +347,8 @@ try {
socket.userID,
]);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
callback({
ok: true,
token: jwt.sign({
@ -380,6 +356,9 @@ try {
}, jwtSecret),
});
} else {
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`);
callback({
ok: false,
msg: "Invalid Token!",
@ -387,6 +366,9 @@ try {
}
}
} else {
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`);
callback({
ok: false,
msg: "Incorrect username or password.",
@ -422,7 +404,7 @@ try {
socket.userID,
]);
if (user.twofa_status == 0) {
if (user.twofa_status === 0) {
let newSecret = genSecret();
let encodedSecret = base32.encode(newSecret);
@ -469,11 +451,16 @@ try {
socket.userID,
]);
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`);
callback({
ok: true,
msg: "2FA Enabled.",
});
} catch (error) {
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`);
callback({
ok: false,
msg: error.message,
@ -491,11 +478,16 @@ try {
await doubleCheckPassword(socket, currentPassword);
await TwoFA.disable2FA(socket.userID);
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`);
callback({
ok: true,
msg: "2FA Disabled.",
});
} catch (error) {
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`);
callback({
ok: false,
msg: error.message,
@ -512,7 +504,7 @@ try {
socket.userID,
]);
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
let verify = notp.totp.verify(token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== token && verify) {
callback({
@ -543,7 +535,7 @@ try {
socket.userID,
]);
if (user.twofa_status == 1) {
if (user.twofa_status === 1) {
callback({
ok: true,
status: true,
@ -622,6 +614,8 @@ try {
await server.sendMonitorList(socket);
await startMonitor(socket.userID, bean.id);
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
callback({
ok: true,
msg: "Added Successfully.",
@ -629,6 +623,9 @@ try {
});
} catch (e) {
log.error("monitor", `Error adding Monitor: ${monitor.id} User ID: ${socket.userID}`);
callback({
ok: false,
msg: e.message,
@ -676,6 +673,10 @@ try {
bean.docker_daemon = monitor.docker_daemon;
bean.docker_type = monitor.docker_type;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
await R.store(bean);
@ -694,7 +695,7 @@ try {
});
} catch (e) {
console.error(e);
log.error("monitor", e);
callback({
ok: false,
msg: e.message,
@ -710,7 +711,7 @@ try {
ok: true,
});
} catch (e) {
console.error(e);
log.error("monitor", e);
callback({
ok: false,
msg: e.message,
@ -722,7 +723,7 @@ try {
try {
checkLogin(socket);
console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`);
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
monitorID,
@ -746,7 +747,7 @@ try {
try {
checkLogin(socket);
console.log(`Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`);
log.info("monitor", `Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`);
if (period == null) {
throw new Error("Invalid period.");
@ -817,7 +818,7 @@ try {
try {
checkLogin(socket);
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
log.info("manage", `Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
if (monitorID in server.monitorList) {
server.monitorList[monitorID].stop();
@ -1049,7 +1050,13 @@ try {
try {
checkLogin(socket);
if (data.disableAuth) {
// If currently is disabled auth, don't need to check
// Disabled Auth + Want to Disable Auth => No Check
// Disabled Auth + Want to Enable Auth => No Check
// Enabled Auth + Want to Disable Auth => Check!!
// Enabled Auth + Want to Enable Auth => No Check
const currentDisabledAuth = await setting("disableAuth");
if (!currentDisabledAuth && data.disableAuth) {
await doubleCheckPassword(socket, currentPassword);
}
@ -1149,7 +1156,7 @@ try {
let backupData = JSON.parse(uploadedJSON);
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
log.info("manage", `Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
let notificationListData = backupData.notificationList;
let proxyListData = backupData.proxyList;
@ -1158,7 +1165,7 @@ try {
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") {
if (importHandle === "overwrite") {
// Stops every monitor first, so it doesn't execute any heartbeat while importing
for (let id in server.monitorList) {
let monitor = server.monitorList[id];
@ -1182,7 +1189,7 @@ try {
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") {
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);
@ -1192,7 +1199,7 @@ try {
}
// Only starts importing if the backup file contains at least one proxy
if (proxyListData.length >= 1) {
if (proxyListData && proxyListData.length >= 1) {
const proxies = await R.findAll("proxy");
// Loop over proxy list and save proxies
@ -1217,7 +1224,7 @@ try {
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") {
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 ---
@ -1314,7 +1321,7 @@ try {
await updateMonitorNotification(bean.id, notificationIDList);
// If monitor was active start it immediately, otherwise pause it
if (monitorListData[i].active == 1) {
if (monitorListData[i].active === 1) {
await startMonitor(socket.userID, bean.id);
} else {
await pauseMonitor(socket.userID, bean.id);
@ -1344,7 +1351,7 @@ try {
try {
checkLogin(socket);
console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`);
log.info("manage", `Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`);
await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [
"",
@ -1370,7 +1377,7 @@ try {
try {
checkLogin(socket);
console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`);
log.info("manage", `Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`);
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
monitorID
@ -1394,7 +1401,7 @@ try {
try {
checkLogin(socket);
console.log(`Clear Statistics User ID: ${socket.userID}`);
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
await R.exec("DELETE FROM heartbeat");
@ -1416,35 +1423,35 @@ try {
databaseSocketHandler(socket);
proxySocketHandler(socket);
debug("added all socket handlers");
log.debug("server", "added all socket handlers");
// ***************************
// Better do anything after added all socket handlers here
// ***************************
debug("check auto login");
log.debug("auth", "check auto login");
if (await setting("disableAuth")) {
console.log("Disabled Auth: auto login to admin");
log.info("auth", "Disabled Auth: auto login to admin");
afterLogin(socket, await R.findOne("user"));
socket.emit("autoLogin");
} else {
debug("need auth");
log.debug("auth", "need auth");
}
});
console.log("Init the server");
log.info("server", "Init the server");
httpServer.once("error", async (err) => {
server.httpServer.once("error", async (err) => {
console.error("Cannot listen: " + err.message);
await shutdownFunction();
});
httpServer.listen(port, hostname, () => {
server.httpServer.listen(port, hostname, () => {
if (hostname) {
console.log(`Listening on ${hostname}:${port}`);
log.info("server", `Listening on ${hostname}:${port}`);
} else {
console.log(`Listening on ${port}`);
log.info("server", `Listening on ${port}`);
}
startMonitors();
checkVersion.startInterval();
@ -1462,11 +1469,11 @@ try {
})();
/**
* Adds or removes notifications from a monitor.
* @param {number} monitorID The ID of the monitor to add/remove notifications from.
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove.
*
* Generated by Trelent
* Update notifications for a given monitor
* @param {number} monitorID ID of monitor to update
* @param {number[]} notificationIDList List of new notification
* providers to add
* @returns {Promise<void>}
*/
async function updateMonitorNotification(monitorID, notificationIDList) {
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
@ -1484,11 +1491,11 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
}
/**
* This function checks if the user owns a monitor with the given ID.
* @param {number} monitorID - The ID of the monitor to check ownership for.
* @param {number} userID - The ID of the user who is trying to access this data.
*
* Generated by Trelent
* Check if a given user owns a specific monitor
* @param {number} userID
* @param {number} monitorID
* @returns {Promise<void>}
* @throws {Error} The specified user does not own the monitor
*/
async function checkOwner(userID, monitorID) {
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
@ -1502,8 +1509,11 @@ async function checkOwner(userID, monitorID) {
}
/**
* Function called after user login
* This function is used to send the heartbeat list of a monitor.
* @param {Socket} socket - The socket object that will be used to send the data.
* @param {Socket} socket Socket.io instance
* @param {Object} user User object
* @returns {Promise<void>}
*/
async function afterLogin(socket, user) {
socket.userID = user.id;
@ -1531,40 +1541,20 @@ async function afterLogin(socket, user) {
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async function getMonitorJSONList(userID) {
let result = {};
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();
}
return result;
}
/**
* Connect to the database and patch it if necessary.
*
* Generated by Trelent
* Initialize the database
* @param {boolean} [testMode=false] Should the connection be
* started in test mode?
* @returns {Promise<void>}
*/
async function initDatabase(testMode = false) {
if (! fs.existsSync(Database.path)) {
console.log("Copying Database");
log.info("server", "Copying Database");
fs.copyFileSync(Database.templatePath, Database.path);
}
console.log("Connecting to the Database");
log.info("server", "Connecting to the Database");
await Database.connect(testMode);
console.log("Connected");
log.info("server", "Connected");
// Patch the database
await Database.patch();
@ -1574,16 +1564,16 @@ async function initDatabase(testMode = false) {
]);
if (! jwtSecretBean) {
console.log("JWT secret is not found, generate one.");
log.info("server", "JWT secret is not found, generate one.");
jwtSecretBean = await initJWTSecret();
console.log("Stored JWT secret into database");
log.info("server", "Stored JWT secret into database");
} else {
console.log("Load JWT secret from database.");
log.info("server", "Load JWT secret from database.");
}
// 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");
log.info("server", "No user, need setup");
needSetup = true;
}
@ -1591,16 +1581,15 @@ async function initDatabase(testMode = false) {
}
/**
* Resume a monitor.
* @param {string} userID - The ID of the user who owns the monitor.
* @param {string} monitorID - The ID of the monitor to resume.
*
* Generated by Trelent
* Start the specified monitor
* @param {number} userID ID of user who owns monitor
* @param {number} monitorID ID of monitor to start
* @returns {Promise<void>}
*/
async function startMonitor(userID, monitorID) {
await checkOwner(userID, monitorID);
console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`);
log.info("manage", `Resume Monitor: ${monitorID} User ID: ${userID}`);
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
monitorID,
@ -1619,21 +1608,26 @@ async function startMonitor(userID, monitorID) {
monitor.start(io);
}
/**
* Restart a given monitor
* @param {number} userID ID of user who owns monitor
* @param {number} monitorID ID of monitor to start
* @returns {Promise<void>}
*/
async function restartMonitor(userID, monitorID) {
return await startMonitor(userID, monitorID);
}
/**
* Pause a monitor.
* @param {string} userID - The ID of the user who owns the monitor.
* @param {string} monitorID - The ID of the monitor to pause.
*
* Generated by Trelent
* Pause a given monitor
* @param {number} userID ID of user who owns monitor
* @param {number} monitorID ID of monitor to start
* @returns {Promise<void>}
*/
async function pauseMonitor(userID, monitorID) {
await checkOwner(userID, monitorID);
console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`);
log.info("manage", `Pause Monitor: ${monitorID} User ID: ${userID}`);
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
monitorID,
@ -1645,9 +1639,7 @@ async function pauseMonitor(userID, monitorID) {
}
}
/**
* Resume active monitors
*/
/** Resume active monitors */
async function startMonitors() {
let list = await R.find("monitor", " active = 1 ");
@ -1663,16 +1655,16 @@ async function startMonitors() {
}
/**
* Shutdown the application
* Stops all monitors and closes the database connection.
* @param {string} signal The signal that triggered this function to be called.
*
* Generated by Trelent
* @returns {Promise<void>}
*/
async function shutdownFunction(signal) {
console.log("Shutdown requested");
console.log("Called signal: " + signal);
log.info("server", "Shutdown requested");
log.info("server", "Called signal: " + signal);
console.log("Stopping all monitors");
log.info("server", "Stopping all monitors");
for (let id in server.monitorList) {
let monitor = server.monitorList[id];
monitor.stop();
@ -1684,11 +1676,16 @@ async function shutdownFunction(signal) {
await cloudflaredStop();
}
function finalFunction() {
console.log("Graceful shutdown successful!");
function getClientIp(socket) {
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
}
gracefulShutdown(httpServer, {
/** Final function called before application exits */
function finalFunction() {
log.info("server", "Graceful shutdown successful!");
}
gracefulShutdown(server.httpServer, {
signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode
@ -1700,6 +1697,6 @@ gracefulShutdown(httpServer, {
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
console.trace(error);
errorLog(error, false);
UptimeKumaServer.errorLog(error, false);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});

View File

@ -1,19 +1,33 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { io } = require("../server");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
const cloudflared = new CloudflaredTunnel();
/**
* Change running state
* @param {string} running Is it running?
* @param {string} message Message to pass
*/
cloudflared.change = (running, message) => {
io.to("cloudflared").emit(prefix + "running", running);
io.to("cloudflared").emit(prefix + "message", message);
};
/**
* Emit an error message
* @param {string} errorMessage
*/
cloudflared.error = (errorMessage) => {
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
};
/**
* Handler for cloudflared
* @param {Socket} socket Socket.io instance
*/
module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "join", async () => {
@ -68,6 +82,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
};
/**
* Automatically start cloudflared
* @param {string} token Cloudflared tunnel token
*/
module.exports.autoStart = async (token) => {
if (!token) {
token = await setting("cloudflaredTunnelToken");
@ -84,7 +102,10 @@ module.exports.autoStart = async (token) => {
}
};
/** Stop cloudflared */
module.exports.stop = async () => {
console.log("Stop cloudflared");
if (cloudflared) {
cloudflared.stop();
}
};

View File

@ -1,6 +1,10 @@
const { checkLogin } = require("../util-server");
const Database = require("../database");
/**
* Handlers for database
* @param {Socket} socket Socket.io instance
*/
module.exports = (socket) => {
// Post or edit incident

View File

@ -1,8 +1,13 @@
const { checkLogin } = require("../util-server");
const { Proxy } = require("../proxy");
const { sendProxyList } = require("../client");
const server = require("../server");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const server = UptimeKumaServer.getInstance();
/**
* Handlers for proxy
* @param {Socket} socket Socket.io instance
*/
module.exports.proxySocketHandler = (socket) => {
socket.on("addProxy", async (proxy, proxyID, callback) => {
try {

View File

@ -1,13 +1,17 @@
const { R } = require("redbean-node");
const { checkLogin, setSettings, setSetting } = require("../util-server");
const { checkLogin, setSetting } = require("../util-server");
const dayjs = require("dayjs");
const { debug } = require("../../src/util");
const { log } = require("../../src/util");
const ImageDataURI = require("../image-data-uri");
const Database = require("../database");
const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const server = require("../server");
const { UptimeKumaServer } = require("../uptime-kuma-server");
/**
* Socket handlers for status page
* @param {Socket} socket Socket.io instance to add listeners on
*/
module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident
@ -155,6 +159,9 @@ module.exports.statusPageSocketHandler = (socket) => {
//statusPage.search_engine_index = ;
statusPage.show_tags = config.showTags;
//statusPage.password = null;
statusPage.footer_text = config.footerText;
statusPage.custom_css = config.customCSS;
statusPage.show_powered_by = config.showPoweredBy;
statusPage.modified_date = R.isoDateTime();
await R.store(statusPage);
@ -202,8 +209,8 @@ module.exports.statusPageSocketHandler = (socket) => {
group.id = groupBean.id;
}
// Delete groups that not in the list
debug("Delete groups that not in the list");
// Delete groups that are not in the list
log.debug("socket", "Delete groups that are not in the list");
const slots = groupIDList.map(() => "?").join(",");
const data = [
@ -212,6 +219,8 @@ module.exports.statusPageSocketHandler = (socket) => {
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
const server = UptimeKumaServer.getInstance();
// Also change entry page to new slug if it is the default one, and slug is changed.
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
server.entryPage = "statusPage-" + statusPage.slug;
@ -226,7 +235,7 @@ module.exports.statusPageSocketHandler = (socket) => {
});
} catch (error) {
console.error(error);
log.error("socket", error);
callback({
ok: false,
@ -281,6 +290,8 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete a status page
socket.on("deleteStatusPage", async (slug, callback) => {
const server = UptimeKumaServer.getInstance();
try {
checkLogin(socket);
@ -331,6 +342,7 @@ module.exports.statusPageSocketHandler = (socket) => {
/**
* Check slug a-z, 0-9, - only
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
* @param {string} slug Slug to test
*/
function checkSlug(slug) {
if (typeof slug !== "string") {

View File

@ -0,0 +1,117 @@
const express = require("express");
const https = require("https");
const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log } = require("../src/util");
const Database = require("./database");
const util = require("util");
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
* @type {UptimeKumaServer}
*/
class UptimeKumaServer {
/**
*
* @type {UptimeKumaServer}
*/
static instance = null;
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
entryPage = "dashboard";
app = undefined;
httpServer = undefined;
io = undefined;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
}
return UptimeKumaServer.instance;
}
constructor(args) {
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, this.app);
} else {
log.info("server", "Server Type: HTTP");
this.httpServer = http.createServer(this.app);
}
this.io = new Server(this.httpServer);
}
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list);
return list;
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async getMonitorJSONList(userID) {
let result = {};
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();
}
return result;
}
/**
* Write error to log file
* @param {any} error The error to write
* @param {boolean} outputToConsole Should the error also be output to console?
*/
static errorLog(error, outputToConsole = true) {
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
flags: "a"
});
errorLogStream.on("error", () => {
log.info("", "Cannot write to error.log");
});
if (errorLogStream) {
const dateTime = R.isoDateTime();
errorLogStream.write(`[${dateTime}] ` + util.format(error) + "\n");
if (outputToConsole) {
console.error(error);
}
}
errorLogStream.end();
}
}
module.exports = {
UptimeKumaServer
};

View File

@ -1,14 +1,15 @@
const tcpp = require("tcp-ping");
const Ping = require("./ping-lite");
const { R } = require("redbean-node");
const { debug, genSecret } = require("../src/util");
const { log, genSecret } = require("../src/util");
const passwordHash = require("./password-hash");
const { Resolver } = require("dns");
const child_process = require("child_process");
const childProcess = require("child_process");
const iconv = require("iconv-lite");
const chardet = require("chardet");
const fs = require("fs");
const nodeJsUtil = require("util");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
@ -36,6 +37,12 @@ exports.initJWTSecret = async () => {
return jwtSecretBean;
};
/**
* Send TCP request to specified hostname and port
* @param {string} hostname Hostname / address of machine
* @param {number} port TCP port to test
* @returns {Promise<number>} Maximum time in ms rounded to nearest integer
*/
exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => {
tcpp.ping({
@ -57,6 +64,11 @@ exports.tcping = function (hostname, port) {
});
};
/**
* Ping the specified machine
* @param {string} hostname Hostname / address of machine
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
*/
exports.ping = async (hostname) => {
try {
return await exports.pingAsync(hostname);
@ -70,6 +82,12 @@ exports.ping = async (hostname) => {
}
};
/**
* Ping the specified machine
* @param {string} hostname Hostname / address of machine to ping
* @param {boolean} ipv6 Should IPv6 be used?
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
*/
exports.pingAsync = function (hostname, ipv6 = false) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname, {
@ -88,11 +106,84 @@ exports.pingAsync = function (hostname, ipv6 = false) {
});
};
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
const resolver = new Resolver();
resolver.setServers([resolver_server]);
/**
* MQTT Monitor
* @param {string} hostname Hostname / address of machine to test
* @param {string} topic MQTT topic
* @param {string} okMessage Expected result
* @param {Object} [options={}] MQTT options. Contains port, username,
* password and interval (interval defaults to 20)
* @returns {Promise<string>}
*/
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
return new Promise((resolve, reject) => {
if (rrtype == "PTR") {
const { port, username, password, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) {
hostname = "mqtt://" + hostname;
}
const timeoutID = setTimeout(() => {
log.debug("mqtt", "MQTT timeout triggered");
client.end();
reject(new Error("Timeout"));
}, interval * 1000 * 0.8);
log.debug("mqtt", "MQTT connecting");
let client = mqtt.connect(hostname, {
port,
username,
password
});
client.on("connect", () => {
log.debug("mqtt", "MQTT connected");
try {
log.debug("mqtt", "MQTT subscribe topic");
client.subscribe(topic);
} catch (e) {
client.end();
clearTimeout(timeoutID);
reject(new Error("Cannot subscribe topic"));
}
});
client.on("error", (error) => {
client.end();
clearTimeout(timeoutID);
reject(error);
});
client.on("message", (messageTopic, message) => {
if (messageTopic === topic) {
client.end();
clearTimeout(timeoutID);
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
reject(new Error(`Message Mismatch - Topic: ${messageTopic}; Message: ${message.toString()}`));
} else {
resolve(`Topic: ${messageTopic}; Message: ${message.toString()}`);
}
}
});
});
};
/**
* Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup
* @param {string} resolverServer The DNS server to use
* @param {string} rrtype The type of record to request
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
const resolver = new Resolver();
resolver.setServers([ resolverServer ]);
return new Promise((resolve, reject) => {
if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => {
if (err) {
reject(err);
@ -112,6 +203,11 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value
*/
exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key,
@ -119,13 +215,20 @@ exports.setting = async function (key) {
try {
const v = JSON.parse(value);
debug(`Get Setting: ${key}: ${v}`);
log.debug("util", `Get Setting: ${key}: ${v}`);
return v;
} catch (e) {
return value;
}
};
/**
* Sets the specified setting to specifed value
* @param {string} key Key of setting to set
* @param {any} value Value to set to
* @param {?string} type Type of setting
* @returns {Promise<void>}
*/
exports.setSetting = async function (key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
@ -139,6 +242,11 @@ exports.setSetting = async function (key, value, type = null) {
await R.store(bean);
};
/**
* Get settings based on type
* @param {?string} type The type of setting
* @returns {Promise<Bean>}
*/
exports.getSettings = async function (type) {
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type,
@ -157,6 +265,12 @@ exports.getSettings = async function (type) {
return result;
};
/**
* Set settings based on type
* @param {?string} type Type of settings to set
* @param {Object} data Values of settings
* @returns {Promise<void>}
*/
exports.setSettings = async function (type, data) {
let keyList = Object.keys(data);
@ -183,12 +297,23 @@ exports.setSettings = async function (type, data) {
};
// ssl-checker by @dyaa
// param: res - response object from axios
// return an object containing the certificate information
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
/**
* Get number of days between two dates
* @param {Date} validFrom Start date
* @param {Date} validTo End date
* @returns {number}
*/
const getDaysBetween = (validFrom, validTo) =>
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
/**
* Get days remaining from a time range
* @param {Date} validFrom Start date
* @param {Date} validTo End date
* @returns {number}
*/
const getDaysRemaining = (validFrom, validTo) => {
const daysRemaining = getDaysBetween(validFrom, validTo);
if (new Date(validTo).getTime() < new Date().getTime()) {
@ -197,8 +322,11 @@ const getDaysRemaining = (validFrom, validTo) => {
return daysRemaining;
};
// Fix certificate Info for display
// param: info - the chain obtained from getPeerCertificate()
/**
* Fix certificate info for display
* @param {Object} info The chain obtained from getPeerCertificate()
* @returns {Object} An object representing certificate information
*/
const parseCertificateInfo = function (info) {
let link = info;
let i = 0;
@ -206,7 +334,7 @@ const parseCertificateInfo = function (info) {
const existingList = {};
while (link) {
debug(`[${i}] ${link.fingerprint}`);
log.debug("cert", `[${i}] ${link.fingerprint}`);
if (!link.valid_from || !link.valid_to) {
break;
@ -221,7 +349,7 @@ const parseCertificateInfo = function (info) {
if (link.issuerCertificate == null) {
break;
} else if (link.issuerCertificate.fingerprint in existingList) {
debug(`[Last] ${link.issuerCertificate.fingerprint}`);
log.debug("cert", `[Last] ${link.issuerCertificate.fingerprint}`);
link.issuerCertificate = null;
break;
} else {
@ -238,11 +366,16 @@ const parseCertificateInfo = function (info) {
return info;
};
/**
* Check if certificate is valid
* @param {Object} res Response object from axios
* @returns {Object} Object containing certificate information
*/
exports.checkCertificate = function (res) {
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
debug("Parsing Certificate Info");
log.debug("cert", "Parsing Certificate Info");
const parsedInfo = parseCertificateInfo(info);
return {
@ -251,25 +384,26 @@ exports.checkCertificate = function (res) {
};
};
// Check if the provided status code is within the accepted ranges
// Param: status - the status code to check
// Param: accepted_codes - an array of accepted status codes
// Return: true if the status code is within the accepted ranges, false otherwise
// Will throw an error if the provided status code is not a valid range string or code string
exports.checkStatusCode = function (status, accepted_codes) {
if (accepted_codes == null || accepted_codes.length === 0) {
/**
* Check if the provided status code is within the accepted ranges
* @param {string} status The status code to check
* @param {string[]} acceptedCodes An array of accepted status codes
* @returns {boolean} True if status code within range, false otherwise
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
*/
exports.checkStatusCode = function (status, acceptedCodes) {
if (acceptedCodes == null || acceptedCodes.length === 0) {
return false;
}
for (const code_range of accepted_codes) {
const code_range_split = code_range.split("-").map(string => parseInt(string));
if (code_range_split.length === 1) {
if (status === code_range_split[0]) {
for (const codeRange of acceptedCodes) {
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
if (codeRangeSplit.length === 1) {
if (status === codeRangeSplit[0]) {
return true;
}
} else if (code_range_split.length === 2) {
if (status >= code_range_split[0] && status <= code_range_split[1]) {
} else if (codeRangeSplit.length === 2) {
if (status >= codeRangeSplit[0] && status <= codeRangeSplit[1]) {
return true;
}
} else {
@ -280,6 +414,12 @@ exports.checkStatusCode = function (status, accepted_codes) {
return false;
};
/**
* Get total number of clients in room
* @param {Server} io Socket server instance
* @param {string} roomName Name of room to check
* @returns {number}
*/
exports.getTotalClientInRoom = (io, roomName) => {
const sockets = io.sockets;
@ -303,17 +443,29 @@ exports.getTotalClientInRoom = (io, roomName) => {
}
};
/**
* Allow CORS all origins if development
* @param {Object} res Response object from axios
*/
exports.allowDevAllOrigin = (res) => {
if (process.env.NODE_ENV === "development") {
exports.allowAllOrigin(res);
}
};
/**
* Allow CORS all origins
* @param {Object} res Response object from axios
*/
exports.allowAllOrigin = (res) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
};
/**
* Check if a user is logged in
* @param {Socket} socket Socket instance
*/
exports.checkLogin = (socket) => {
if (!socket.userID) {
throw new Error("You are not logged in.");
@ -322,8 +474,8 @@ exports.checkLogin = (socket) => {
/**
* For logged-in users, double-check the password
* @param socket
* @param currentPassword
* @param {Socket} socket Socket.io instance
* @param {string} currentPassword
* @returns {Promise<Bean>}
*/
exports.doubleCheckPassword = async (socket, currentPassword) => {
@ -342,10 +494,11 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
return user;
};
/** Start Unit tests */
exports.startUnitTest = async () => {
console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = child_process.spawn(npm, ["run", "jest"]);
const child = childProcess.spawn(npm, [ "run", "jest" ]);
child.stdout.on("data", (data) => {
console.log(data.toString());
@ -362,33 +515,42 @@ exports.startUnitTest = async () => {
};
/**
* @param body : Buffer
* Convert unknown string to UTF8
* @param {Uint8Array} body Buffer
* @returns {string}
*/
exports.convertToUTF8 = (body) => {
const guessEncoding = chardet.detect(body);
//debug("Guess Encoding: " + guessEncoding);
const str = iconv.decode(body, guessEncoding);
return str.toString();
};
let logFile;
/**
* Returns a color code in hex format based on a given percentage:
* 0% => hue = 10 => red
* 100% => hue = 90 => green
*
* @param {number} percentage float, 0 to 1
* @param {number} maxHue
* @param {number} minHue, int
* @returns {string}, hex value
*/
exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
const hue = percentage * (maxHue - minHue) + minHue;
try {
logFile = fs.createWriteStream("./data/error.log", {
flags: "a"
});
} catch (_) { }
exports.errorLog = (error, outputToConsole = true) => {
try {
if (logFile) {
const dateTime = R.isoDateTime();
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
if (outputToConsole) {
console.error(error);
return chroma(`hsl(${hue}, 90%, 40%)`).hex();
} catch (err) {
return badgeConstants.naColor;
}
}
} catch (_) { }
};
/**
* Joins and array of string to one string after filtering out empty values
*
* @param {string[]} parts
* @param {string} connector
* @returns {string}
*/
exports.filterAndJoin = (parts, connector = "") => {
return parts.filter((part) => !!part && part !== "").join(connector);
};

View File

@ -469,6 +469,10 @@ textarea.form-control {
color: $primary;
}
.prism-editor__textarea {
outline: none !important;
}
// Localization
@import "localization.scss";

View File

@ -25,7 +25,7 @@
</template>
<script>
import { Modal } from "bootstrap"
import { Modal } from "bootstrap";
export default {
props: {
@ -42,19 +42,20 @@ export default {
default: "No",
},
},
emits: [ "yes" ],
data: () => ({
modal: null,
}),
mounted() {
this.modal = new Modal(this.$refs.modal)
this.modal = new Modal(this.$refs.modal);
},
methods: {
show() {
this.modal.show()
this.modal.show();
},
yes() {
this.$emit("yes");
},
},
}
};
</script>

View File

@ -57,6 +57,7 @@ export default {
default: undefined,
},
},
emits: [ "update:modelValue" ],
data() {
return {
visibility: "password",

View File

@ -5,12 +5,15 @@
<script lang="ts">
import { sleep } from "../util.ts"
import { sleep } from "../util.ts";
export default {
props: {
value: [String, Number],
value: {
type: [ String, Number ],
default: 0,
},
time: {
type: Number,
default: 0.3,
@ -25,12 +28,12 @@ export default {
return {
output: "",
frameDuration: 30,
}
};
},
computed: {
isNum() {
return typeof this.value === "number"
return typeof this.value === "number";
},
},
@ -45,7 +48,7 @@ export default {
} else {
for (let i = 1; i < frames; i++) {
this.output += step;
await sleep(15)
await sleep(15);
}
}
@ -59,5 +62,5 @@ export default {
methods: {},
}
};
</script>

View File

@ -4,16 +4,19 @@
<script>
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"
import utc from "dayjs/plugin/utc"
import timezone from "dayjs/plugin/timezone" // dependent on utc plugin
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(relativeTime)
import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export default {
props: {
value: String,
value: {
type: String,
default: null,
},
dateOnly: {
type: Boolean,
default: false,
@ -29,5 +32,5 @@ export default {
}
},
},
}
};
</script>

View File

@ -38,7 +38,7 @@ export default {
beatMargin: 4,
move: false,
maxBeat: -1,
}
};
},
computed: {
@ -69,12 +69,12 @@ export default {
if (start < 0) {
// Add empty placeholder
for (let i = start; i < 0; i++) {
placeholders.push(0)
placeholders.push(0);
}
start = 0;
}
return placeholders.concat(this.beatList.slice(start))
return placeholders.concat(this.beatList.slice(start));
},
wrapStyle() {
@ -84,7 +84,7 @@ export default {
return {
padding: `${topBottom}px ${leftRight}px`,
width: "100%",
}
};
},
barStyle() {
@ -94,12 +94,12 @@ export default {
return {
transition: "all ease-in-out 0.25s",
transform: `translateX(${width}px)`,
}
};
}
return {
transform: "translateX(0)",
}
};
},
@ -109,7 +109,7 @@ export default {
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
}
};
},
},
@ -120,7 +120,7 @@ export default {
setTimeout(() => {
this.move = false;
}, 300)
}, 300);
},
deep: true,
},
@ -162,15 +162,16 @@ export default {
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
}
},
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ``);
}
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
},
}
},
};
</script>
<style lang="scss" scoped>

View File

@ -48,18 +48,19 @@ export default {
default: undefined,
},
},
emits: [ "update:modelValue" ],
data() {
return {
visibility: "password",
}
};
},
computed: {
model: {
get() {
return this.modelValue
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value)
this.$emit("update:modelValue", value);
}
}
},
@ -74,5 +75,5 @@ export default {
this.visibility = "password";
},
}
}
};
</script>

View File

@ -21,7 +21,7 @@
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
@ -47,8 +47,8 @@
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default {
@ -105,7 +105,7 @@ export default {
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText != "") {
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
@ -170,12 +170,6 @@ export default {
}
}
.dark {
.footer {
// background-color: $dark-bg;
}
}
@media (max-width: 770px) {
.list-header {
margin: -20px;
@ -198,7 +192,7 @@ export default {
max-width: 15em;
}
.monitorItem {
.monitor-item {
width: 100%;
}

View File

@ -69,7 +69,6 @@
<script lang="ts">
import { Modal } from "bootstrap";
import { ucfirst } from "../util.ts";
import Confirm from "./Confirm.vue";
import NotificationFormList from "./notifications";

View File

@ -18,13 +18,13 @@
<script lang="ts">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import "chartjs-adapter-dayjs";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { LineChart } from "vue-chart-3";
import { useToast } from "vue-toastification";
import { UP, DOWN, PENDING } from "../util.ts";
import { DOWN, log } from "../util.ts";
dayjs.extend(utc);
dayjs.extend(timezone);
@ -217,9 +217,11 @@ export default {
watch: {
// Update chart data when the selected chart period changes
chartPeriodHrs: function (newPeriod) {
// eslint-disable-next-line eqeqeq
if (newPeriod == "0") {
newPeriod = null;
this.heartbeatList = null;
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
} else {
this.loading = true;
@ -228,6 +230,7 @@ export default {
toast.error(res.msg);
} else {
this.heartbeatList = res.data;
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
}
this.loading = false;
});
@ -239,7 +242,11 @@ export default {
// And mirror latest change to this.heartbeatList
this.$watch(() => this.$root.heartbeatList[this.monitorId],
(heartbeatList) => {
if (this.chartPeriodHrs != 0) {
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
// eslint-disable-next-line eqeqeq
if (this.chartPeriodHrs != "0") {
const newBeat = heartbeatList.at(-1);
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
this.heartbeatList.push(heartbeatList.at(-1));
@ -248,6 +255,12 @@ export default {
},
{ deep: true }
);
// Load chart period from storage if saved
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
if (period != null) {
this.chartPeriodHrs = Math.min(period, 6);
}
}
};
</script>
@ -278,7 +291,7 @@ export default {
.dropdown-item {
border-radius: 0.3rem;
padding: 2px 16px 4px 16px;
padding: 2px 16px 4px;
.dark & {
background: $dark-bg;
@ -286,6 +299,7 @@ export default {
.dark &:hover {
background: $dark-font-color;
color: $dark-font-color2;
}
}

View File

@ -25,7 +25,7 @@
<label for="proxy-host" class="form-label">{{ $t("Proxy Server") }}</label>
<div class="d-flex">
<input id="proxy-host" v-model="proxy.host" type="text" class="form-control" required :placeholder="$t('Server Address')">
<input v-model="proxy.port" type="number" class="form-control ms-2" style="width: 100px" required min="1" max="65535" :placeholder="$t('Port')">
<input v-model="proxy.port" type="number" class="form-control ms-2" style="width: 100px;" required min="1" max="65535" :placeholder="$t('Port')">
</div>
</div>

View File

@ -41,7 +41,7 @@
<Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }}
</div>
<div v-if="showTags" class="tags">
<div v-if="showTag" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
@ -145,7 +145,7 @@ export default {
.mobile {
.item {
padding: 13px 0 10px 0;
padding: 13px 0 10px;
}
}

View File

@ -5,7 +5,10 @@
<script>
export default {
props: {
status: Number,
status: {
type: Number,
default: 0,
}
},
computed: {

View File

@ -1,5 +1,6 @@
<template>
<div class="tag-wrapper rounded d-inline-flex"
<div
class="tag-wrapper rounded d-inline-flex"
:class="{ 'px-3': size == 'normal',
'py-1': size == 'normal',
'm-2': size == 'normal',
@ -34,14 +35,14 @@ export default {
},
computed: {
displayText() {
if (this.item.value == "") {
if (this.item.value === "") {
return this.item.name;
} else {
return `${this.item.name}: ${this.item.value}`;
}
}
}
}
};
</script>
<style lang="scss" scoped>

View File

@ -34,7 +34,8 @@
label="name"
>
<template #option="{ option }">
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
<div
class="mx-2 py-1 px-3 rounded d-inline-flex"
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
>
@ -43,7 +44,8 @@
</div>
</template>
<template #singleLabel="{ option }">
<div class="py-1 px-3 rounded d-inline-flex"
<div
class="py-1 px-3 rounded d-inline-flex"
style="height: 24px;"
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
>
@ -53,7 +55,8 @@
</vue-multiselect>
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
<div class="w-50 pe-2">
<input v-model="newDraftTag.name" class="form-control"
<input
v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}"
:placeholder="$t('Name')"
@keydown.enter.prevent="onEnter"
@ -75,7 +78,8 @@
deselect-label=""
>
<template #option="{ option }">
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
<div
class="mx-2 py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
@ -83,7 +87,8 @@
</div>
</template>
<template #singleLabel="{ option }">
<div class="py-1 px-3 rounded d-inline-flex"
<div
class="py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
@ -94,7 +99,8 @@
</div>
</div>
<div class="mb-2">
<input v-model="newDraftTag.value" class="form-control"
<input
v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}"
:placeholder="$t('value (optional)')"
@keydown.enter.prevent="onEnter"
@ -123,8 +129,8 @@
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import Tag from "../components/Tag.vue";
import { useToast } from "vue-toastification";
import Tag from "../components/Tag.vue";
const toast = useToast();
export default {
@ -159,14 +165,14 @@ export default {
tagOptions() {
const tagOptions = this.existingTags;
for (const tag of this.newTags) {
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
tagOptions.push(tag);
}
}
return tagOptions;
},
selectedTags() {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id));
},
colorOptions() {
return [
@ -192,7 +198,7 @@ export default {
let nameInvalid = false;
let valueInvalid = false;
let invalid = true;
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value)) {
// Undo removing a Tag
nameInvalid = false;
valueInvalid = false;
@ -202,9 +208,9 @@ export default {
nameInvalid = true;
invalid = true;
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value
) || (
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
tag.name === this.newDraftTag.name && tag.value === this.newDraftTag.value
)).length > 0) {
// Try to add a tag with existing name and value
valueInvalid = true;
@ -250,7 +256,7 @@ export default {
deleteTag(item) {
if (item.new) {
// Undo Adding a new Tag
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
this.newTags = this.newTags.filter(tag => !(tag.name === item.name && tag.value === item.value));
} else {
// Remove an Existing Tag
this.deleteTags.push(item);
@ -266,9 +272,9 @@ export default {
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value)) {
// Undo removing a tag
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
this.deleteTags = this.deleteTags.filter(tag => !(tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value));
} else {
// Add an existing Tag
this.newTags.push({
@ -345,7 +351,7 @@ export default {
tagId = newTagResult.id;
// Assign the new ID to the tags of the same name & color
this.newTags.map(tag => {
if (tag.name == newTag.name && tag.color == newTag.color) {
if (tag.name === newTag.name && tag.color === newTag.color) {
tag.id = newTagResult.id;
}
});

Some files were not shown because too many files have changed in this diff Show More