Merge branch 'master' into #1059-specify-dns-resolver-port

This commit is contained in:
Matthew Nickson 2022-04-24 01:06:45 +01:00 committed by GitHub
commit d1a3cd047a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 3154 additions and 719 deletions

View file

@ -22,39 +22,47 @@ module.exports = {
requireConfigFile: false, requireConfigFile: false,
}, },
rules: { rules: {
"linebreak-style": ["error", "unix"], "yoda": "error",
"camelcase": ["warn", { eqeqeq: [ "warn", "smart" ],
"linebreak-style": [ "error", "unix" ],
"camelcase": [ "warn", {
"properties": "never", "properties": "never",
"ignoreImports": true "ignoreImports": true
}], }],
// override/add rules settings here, such as: "no-unused-vars": [ "warn", {
// 'vue/no-unused-vars': 'error' "args": "none"
"no-unused-vars": "warn", }],
indent: [ indent: [
"error", "error",
4, 4,
{ {
ignoredNodes: ["TemplateLiteral"], ignoredNodes: [ "TemplateLiteral" ],
SwitchCase: 1, SwitchCase: 1,
}, },
], ],
quotes: ["warn", "double"], quotes: [ "error", "double" ],
semi: "error", semi: "error",
"vue/html-indent": ["warn", 4], // default: 2 "vue/html-indent": [ "error", 4 ], // default: 2
"vue/max-attributes-per-line": "off", "vue/max-attributes-per-line": "off",
"vue/singleline-html-element-content-newline": "off", "vue/singleline-html-element-content-newline": "off",
"vue/html-self-closing": "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/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
"no-multi-spaces": ["error", { "no-multi-spaces": [ "error", {
ignoreEOLComments: true, ignoreEOLComments: true,
}], }],
"space-before-function-paren": ["error", { "array-bracket-spacing": [ "warn", "always", {
"singleValue": true,
"objectsInArrays": false,
"arraysInArrays": false
}],
"space-before-function-paren": [ "error", {
"anonymous": "always", "anonymous": "always",
"named": "never", "named": "never",
"asyncArrow": "always" "asyncArrow": "always"
}], }],
"curly": "error", "curly": "error",
"object-curly-spacing": ["error", "always"], "object-curly-spacing": [ "error", "always" ],
"object-curly-newline": "off", "object-curly-newline": "off",
"object-property-newline": "error", "object-property-newline": "error",
"comma-spacing": "error", "comma-spacing": "error",
@ -64,37 +72,37 @@ module.exports = {
"keyword-spacing": "warn", "keyword-spacing": "warn",
"space-infix-ops": "warn", "space-infix-ops": "warn",
"arrow-spacing": "warn", "arrow-spacing": "warn",
"no-trailing-spaces": "warn", "no-trailing-spaces": "error",
"no-constant-condition": ["error", { "no-constant-condition": [ "error", {
"checkLoops": false, "checkLoops": false,
}], }],
"space-before-blocks": "warn", "space-before-blocks": "warn",
//'no-console': 'warn', //'no-console': 'warn',
"no-extra-boolean-cast": "off", "no-extra-boolean-cast": "off",
"no-multiple-empty-lines": ["warn", { "no-multiple-empty-lines": [ "warn", {
"max": 1, "max": 1,
"maxBOF": 0, "maxBOF": 0,
}], }],
"lines-between-class-members": ["warn", "always", { "lines-between-class-members": [ "warn", "always", {
exceptAfterSingleLine: true, exceptAfterSingleLine: true,
}], }],
"no-unneeded-ternary": "error", "no-unneeded-ternary": "error",
"array-bracket-newline": ["error", "consistent"], "array-bracket-newline": [ "error", "consistent" ],
"eol-last": ["error", "always"], "eol-last": [ "error", "always" ],
//'prefer-template': 'error', //'prefer-template': 'error',
"comma-dangle": ["warn", "only-multiline"], "comma-dangle": [ "warn", "only-multiline" ],
"no-empty": ["error", { "no-empty": [ "error", {
"allowEmptyCatch": true "allowEmptyCatch": true
}], }],
"no-control-regex": "off", "no-control-regex": "off",
"one-var": ["error", "never"], "one-var": [ "error", "never" ],
"max-statements-per-line": ["error", { "max": 1 }] "max-statements-per-line": [ "error", { "max": 1 }]
}, },
"overrides": [ "overrides": [
{ {
"files": [ "src/languages/*.js", "src/icon.js" ], "files": [ "src/languages/*.js", "src/icon.js" ],
"rules": { "rules": {
"comma-dangle": ["error", "always-multiline"], "comma-dangle": [ "error", "always-multiline" ],
} }
}, },

View file

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

View file

@ -21,7 +21,7 @@ jobs:
steps: steps:
- run: git config --global core.autocrlf false # Mainly for Windows - run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v2

View file

@ -79,6 +79,7 @@ I personally do not like something need to learn so much and need to config so m
- 4 spaces indentation - 4 spaces indentation
- Follow `.editorconfig` - Follow `.editorconfig`
- Follow ESLint - Follow ESLint
- Methods and funtions should be documented with JSDoc
## Name convention ## Name convention

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. * Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
* Fancy, Reactive, Fast UI/UX. * 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. * 20 second intervals.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages) * [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 * Ping Chart
* Certificate Info * Certificate Info
* Proxy Support
* 2FA available
## 🔧 How to Install ## 🔧 How to Install
@ -154,10 +157,17 @@ https://www.reddit.com/r/UptimeKuma/
## Contribute ## Contribute
### 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. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues). 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).
### 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 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

@ -1,11 +1,11 @@
const config = {}; const config = {};
if (process.env.TEST_FRONTEND) { if (process.env.TEST_FRONTEND) {
config.presets = ["@babel/preset-env"]; config.presets = [ "@babel/preset-env" ];
} }
if (process.env.TEST_BACKEND) { if (process.env.TEST_BACKEND) {
config.plugins = ["babel-plugin-rewire"]; config.plugins = [ "babel-plugin-rewire" ];
} }
module.exports = config; module.exports = config;

View file

@ -10,15 +10,15 @@ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
legacy({ legacy({
targets: ["ie > 11"], targets: [ "ie > 11" ],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"] additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
}) })
], ],
css: { css: {
postcss: { postcss: {
"parser": postCssScss, "parser": postCssScss,
"map": false, "map": false,
"plugins": [postcssRTLCSS] "plugins": [ postcssRTLCSS ]
} }
}, },
}); });

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 # 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 && \ 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 && \
rm -rf /root/.cache rm -rf /root/.cache

View file

@ -11,7 +11,7 @@ WORKDIR /app
RUN apt update && \ 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 \ 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 && \ 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 && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install cloudflared # Install cloudflared

View file

@ -5,7 +5,6 @@ const util = require("../../src/util");
util.polyfill(); util.polyfill();
const oldVersion = pkg.version;
const version = process.env.VERSION; const version = process.env.VERSION;
console.log("Beta Version: " + version); console.log("Beta Version: " + version);
@ -32,7 +31,7 @@ if (! exists) {
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
let res = childProcess.spawnSync("git", ["commit", "-m", msg, "-a"]); let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
let stdout = res.stdout.toString().trim(); let stdout = res.stdout.toString().trim();
console.log(stdout); console.log(stdout);
@ -40,15 +39,15 @@ function commit(version) {
throw new Error("commit error"); throw new Error("commit error");
} }
res = childProcess.spawnSync("git", ["push", "origin", "master"]); res = childProcess.spawnSync("git", [ "push", "origin", "master" ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", ["tag", version]); let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
res = childProcess.spawnSync("git", ["push", "origin", version]); res = childProcess.spawnSync("git", [ "push", "origin", version ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
@ -57,15 +56,7 @@ function tagExists(version) {
throw new Error("invalid version"); throw new Error("invalid version");
} }
let res = childProcess.spawnSync("git", ["tag", "-l", version]); let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
return res.stdout.toString().trim() === version; return res.stdout.toString().trim() === version;
} }
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

View file

@ -29,7 +29,7 @@ const github = require("@actions/github");
owner: issue.owner, owner: issue.owner,
repo: issue.repo, repo: issue.repo,
issue_number: issue.number, issue_number: issue.number,
labels: ["invalid-format"] labels: [ "invalid-format" ]
}); });
// Add the issue closing comment // Add the issue closing comment

View file

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

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 pkg = require("../package.json");
const fs = require("fs"); const fs = require("fs");
const rmSync = require("./fs-rmSync.js"); const childProcess = require("child_process");
const child_process = require("child_process");
const util = require("../src/util"); const util = require("../src/util");
util.polyfill(); util.polyfill();
@ -42,7 +41,7 @@ if (! exists) {
function commit(version) { function commit(version) {
let msg = "Update to " + 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(); let stdout = res.stdout.toString().trim();
console.log(stdout); console.log(stdout);
@ -52,7 +51,7 @@ function commit(version) {
} }
function tag(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()); console.log(res.stdout.toString().trim());
} }
@ -67,7 +66,7 @@ function tagExists(version) {
throw new Error("invalid 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; 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 fs = require("fs");
const newVersion = process.env.VERSION; const newVersion = process.env.VERSION;
@ -16,23 +16,23 @@ function updateWiki(newVersion) {
safeDelete(wikiDir); 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(); let content = fs.readFileSync(howToUpdateFilename).toString();
// Replace the version: https://regex101.com/r/hmj2Bc/1 // Replace the version: https://regex101.com/r/hmj2Bc/1
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`); content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
fs.writeFileSync(howToUpdateFilename, content); fs.writeFileSync(howToUpdateFilename, content);
child_process.spawnSync("git", ["add", "-A"], { childProcess.spawnSync("git", [ "add", "-A" ], {
cwd: wikiDir, cwd: wikiDir,
}); });
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], { childProcess.spawnSync("git", [ "commit", "-m", `Update to ${newVersion}` ], {
cwd: wikiDir, cwd: wikiDir,
}); });
console.log("Pushing to Github"); console.log("Pushing to Github");
child_process.spawnSync("git", ["push"], { childProcess.spawnSync("git", [ "push" ], {
cwd: wikiDir, cwd: wikiDir,
}); });

2068
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.14.0", "version": "1.15.0-beta.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,6 +13,7 @@
"install-legacy": "npm install --legacy-peer-deps", "install-legacy": "npm install --legacy-peer-deps",
"update-legacy": "npm update --legacy-peer-deps", "update-legacy": "npm update --legacy-peer-deps",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", "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:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style", "lint": "npm run lint:js && npm run lint:style",
"dev": "vite --host --config ./config/vite.config.js", "dev": "vite --host --config ./config/vite.config.js",
@ -36,7 +37,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-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", "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", "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.14.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
@ -48,6 +49,7 @@
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", "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 .", "test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js", "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-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", "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
"ncu-patch": "npm-check-updates -u -t patch", "ncu-patch": "npm-check-updates -u -t patch",
@ -60,7 +62,7 @@
"@fortawesome/free-regular-svg-icons": "~5.15.4", "@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4", "@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5", "@fortawesome/vue-fontawesome": "~3.0.0-5",
"@louislam/sqlite3": "~6.0.1", "@louislam/sqlite3": "~15.0.3",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.26.1", "axios": "~0.26.1",
@ -85,12 +87,14 @@
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"limiter": "^2.1.0", "limiter": "^2.1.0",
"mqtt": "^4.2.8",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1", "postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.3", "postcss-scss": "~4.0.3",
"prismjs": "^1.27.0",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
@ -110,6 +114,7 @@
"vue-i18n": "~9.1.9", "vue-i18n": "~9.1.9",
"vue-image-crop-upload": "~3.0.3", "vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2", "vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-qrcode": "~1.0.0", "vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14", "vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5", "vue-toastification": "~2.0.0-rc.5",
@ -123,6 +128,7 @@
"@vitejs/plugin-legacy": "~1.6.4", "@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.4", "@vitejs/plugin-vue": "~1.9.4",
"@vue/compiler-sfc": "~3.2.31", "@vue/compiler-sfc": "~3.2.31",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",

View file

@ -1,4 +1,3 @@
const { checkLogin } = require("./util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
class TwoFA { class TwoFA {

View file

@ -3,7 +3,8 @@
*/ */
const { TimeLogger } = require("../src/util"); const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node"); 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 { setting } = require("./util-server");
const checkVersion = require("./check-version"); const checkVersion = require("./check-version");
@ -98,7 +99,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
async function sendProxyList(socket) { async function sendProxyList(socket) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
const list = await R.find("proxy", " user_id = ? ", [socket.userID]); const list = await R.find("proxy", " user_id = ? ", [ socket.userID ]);
io.to(socket.userID).emit("proxyList", list.map(bean => bean.export())); io.to(socket.userID).emit("proxyList", list.map(bean => bean.export()));
timeLogger.print("Send Proxy List"); timeLogger.print("Send Proxy List");

View file

@ -56,6 +56,8 @@ class Database {
"patch-status-page.sql": true, "patch-status-page.sql": true,
"patch-proxy.sql": true, "patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true, "patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true,
} }
/** /**

View file

@ -30,7 +30,7 @@ const DEFAULT_KEEP_PERIOD = 180;
try { try {
await R.exec( await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ", "DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[parsedPeriod] [ parsedPeriod ]
); );
} catch (e) { } catch (e) {
log(`Failed to clear old data: ${e.message}`); log(`Failed to clear old data: ${e.message}`);

View file

@ -9,7 +9,7 @@ const log = function (any) {
}; };
const exit = function (error) { const exit = function (error) {
if (error && error != 0) { if (error && error !== 0) {
process.exit(error); process.exit(error);
} else { } else {
if (parentPort) { if (parentPort) {

View file

View file

@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog, mqttAsync } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
@ -42,7 +42,7 @@ class Monitor extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
*/ */
async toJSON() { async toJSON(includeSensitiveData = true) {
let notificationIDList = {}; let notificationIDList = {};
@ -56,15 +56,11 @@ class Monitor extends BeanModel {
const tags = await this.getTags(); const tags = await this.getTags();
return { let data = {
id: this.id, id: this.id,
name: this.name, name: this.name,
url: this.url, url: this.url,
method: this.method, 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, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries, maxretries: this.maxretries,
@ -82,15 +78,31 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type, dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server, dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result, dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
proxyId: this.proxy_id, proxyId: this.proxy_id,
notificationIDList, notificationIDList,
tags: tags, 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;
} }
async getTags() { 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]); 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 ]);
} }
/** /**
@ -151,7 +163,7 @@ class Monitor extends BeanModel {
// undefined if not https // undefined if not https
let tlsInfo = undefined; let tlsInfo = undefined;
if (! previousBeat) { if (!previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id, this.id,
]); ]);
@ -169,7 +181,7 @@ class Monitor extends BeanModel {
} }
// Duration // Duration
if (! isFirstBeat) { if (!isFirstBeat) {
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second"); bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
} else { } else {
bean.duration = 0; bean.duration = 0;
@ -262,7 +274,7 @@ class Monitor extends BeanModel {
log.debug("monitor", "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) { if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
log.info("monitor", res.data); log.info("monitor", res.data);
} }
@ -302,24 +314,24 @@ class Monitor extends BeanModel {
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type); let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime; 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 += "Records: ";
dnsMessage += dnsRes.join(" | "); 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]; dnsMessage = dnsRes[0];
} else if (this.dns_resolve_type == "CAA") { } else if (this.dns_resolve_type === "CAA") {
dnsMessage = dnsRes[0].issue; dnsMessage = dnsRes[0].issue;
} else if (this.dns_resolve_type == "MX") { } else if (this.dns_resolve_type === "MX") {
dnsRes.forEach(record => { dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `; dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
}); });
dnsMessage = dnsMessage.slice(0, -2); dnsMessage = dnsMessage.slice(0, -2);
} else if (this.dns_resolve_type == "NS") { } else if (this.dns_resolve_type === "NS") {
dnsMessage += "Servers: "; dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | "); 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}`; 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 => { dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `; dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
}); });
@ -374,7 +386,7 @@ class Monitor extends BeanModel {
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(), rejectUnauthorized: !this.getIgnoreTls(),
}), }),
maxRedirects: this.maxredirects, maxRedirects: this.maxredirects,
validateStatus: (status) => { validateStatus: (status) => {
@ -396,7 +408,14 @@ class Monitor extends BeanModel {
} else { } else {
throw new Error("Server not found on Steam"); throw new Error("Server not found on Steam");
} }
} 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 { } else {
bean.msg = "Unknown Monitor Type"; bean.msg = "Unknown Monitor Type";
bean.status = PENDING; bean.status = PENDING;
@ -609,11 +628,11 @@ class Monitor extends BeanModel {
} }
static async sendCertInfo(io, monitorID, userID) { 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, monitorID,
]); ]);
if (tls_info != null) { if (tlsInfo != null) {
io.to(userID).emit("certInfo", monitorID, tls_info.info_json); io.to(userID).emit("certInfo", monitorID, tlsInfo.info_json);
} }
} }
@ -675,7 +694,7 @@ class Monitor extends BeanModel {
} else { } else {
// Handle new monitor with only one beat, because the beat's duration = 0 // Handle new monitor with only one beat, because the beat's duration = 0
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ])); let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [monitorID]));
if (status === UP) { if (status === UP) {
uptime = 1; uptime = 1;
@ -727,7 +746,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) { for (let notification of notificationList) {
try { 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) { } catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name); log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e); log.error("monitor", e);

View file

@ -92,6 +92,9 @@ class StatusPage extends BeanModel {
published: !!this.published, published: !!this.published,
showTags: !!this.show_tags, showTags: !!this.show_tags,
domainNameList: this.getDomainNameList(), domainNameList: this.getDomainNameList(),
customCSS: this.custom_css,
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
}; };
} }
@ -104,6 +107,9 @@ class StatusPage extends BeanModel {
theme: this.theme, theme: this.theme,
published: !!this.published, published: !!this.published,
showTags: !!this.show_tags, showTags: !!this.show_tags,
customCSS: this.custom_css,
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
}; };
} }

View file

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

View file

@ -40,17 +40,17 @@ class Alerta extends NotificationProvider {
await axios.post(alertaUrl, postData, config); await axios.post(alertaUrl, postData, config);
} else { } else {
let datadup = Object.assign( { let datadup = Object.assign( {
correlate: ["service_up", "service_down"], correlate: [ "service_up", "service_down" ],
event: monitorJSON["type"], event: monitorJSON["type"],
group: "uptimekuma-" + monitorJSON["type"], group: "uptimekuma-" + monitorJSON["type"],
resource: monitorJSON["name"], resource: monitorJSON["name"],
}, data ); }, data );
if (heartbeatJSON["status"] == DOWN) { if (heartbeatJSON["status"] === DOWN) {
datadup.severity = notification.alertaAlertState; // critical datadup.severity = notification.alertaAlertState; // critical
datadup.text = "Service " + monitorJSON["type"] + " is down."; datadup.text = "Service " + monitorJSON["type"] + " is down.";
await axios.post(alertaUrl, datadup, config); await axios.post(alertaUrl, datadup, config);
} else if (heartbeatJSON["status"] == UP) { } else if (heartbeatJSON["status"] === UP) {
datadup.severity = notification.alertaRecoverState; // cleaned datadup.severity = notification.alertaRecoverState; // cleaned
datadup.text = "Service " + monitorJSON["type"] + " is up."; datadup.text = "Service " + monitorJSON["type"] + " is up.";
await axios.post(alertaUrl, datadup, config); await axios.post(alertaUrl, datadup, config);

View file

@ -64,7 +64,7 @@ class AliyunSMS extends NotificationProvider {
}; };
let result = await axios(config); let result = await axios(config);
if (result.data.Message == "OK") { if (result.data.Message === "OK") {
return true; return true;
} }
return false; return false;

View file

@ -1,12 +1,12 @@
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const child_process = require("child_process"); const childProcess = require("child_process");
class Apprise extends NotificationProvider { class Apprise extends NotificationProvider {
name = "apprise"; name = "apprise";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]); let s = childProcess.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL ]);
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";

View file

@ -28,12 +28,12 @@ class Bark extends NotificationProvider {
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1); 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"; let title = "UptimeKuma Monitor Up";
return await this.postNotification(title, msg, barkEndpoint); 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"; let title = "UptimeKuma Monitor Down";
return await this.postNotification(title, msg, barkEndpoint); return await this.postNotification(title, msg, barkEndpoint);
} }

View file

@ -50,7 +50,7 @@ class DingDing extends NotificationProvider {
}; };
let result = await axios(config); let result = await axios(config);
if (result.data.errmsg == "ok") { if (result.data.errmsg === "ok") {
return true; return true;
} }
return false; return false;

View file

@ -35,7 +35,7 @@ class Discord extends NotificationProvider {
} }
// If heartbeatJSON is not null, we go into the normal alerting loop. // If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == DOWN) { if (heartbeatJSON["status"] === DOWN) {
let discorddowndata = { let discorddowndata = {
username: discordDisplayName, username: discordDisplayName,
embeds: [{ embeds: [{
@ -70,7 +70,7 @@ class Discord extends NotificationProvider {
await axios.post(notification.discordWebhookUrl, discorddowndata); await axios.post(notification.discordWebhookUrl, discorddowndata);
return okMsg; return okMsg;
} else if (heartbeatJSON["status"] == UP) { } else if (heartbeatJSON["status"] === UP) {
let discordupdata = { let discordupdata = {
username: discordDisplayName, username: discordDisplayName,
embeds: [{ embeds: [{

View file

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

View file

@ -18,7 +18,7 @@ class Gorush extends NotificationProvider {
let data = { let data = {
"notifications": [ "notifications": [
{ {
"tokens": [notification.gorushDeviceToken], "tokens": [ notification.gorushDeviceToken ],
"platform": platformMapping[notification.gorushPlatform], "platform": platformMapping[notification.gorushPlatform],
"message": msg, "message": msg,
// Optional // Optional

View file

@ -27,7 +27,7 @@ class Line extends NotificationProvider {
] ]
}; };
await axios.post(lineAPIUrl, testMessage, config); await axios.post(lineAPIUrl, testMessage, config);
} else if (heartbeatJSON["status"] == DOWN) { } else if (heartbeatJSON["status"] === DOWN) {
let downMessage = { let downMessage = {
"to": notification.lineUserID, "to": notification.lineUserID,
"messages": [ "messages": [
@ -38,7 +38,7 @@ class Line extends NotificationProvider {
] ]
}; };
await axios.post(lineAPIUrl, downMessage, config); await axios.post(lineAPIUrl, downMessage, config);
} else if (heartbeatJSON["status"] == UP) { } else if (heartbeatJSON["status"] === UP) {
let upMessage = { let upMessage = {
"to": notification.lineUserID, "to": notification.lineUserID,
"messages": [ "messages": [

View file

@ -20,7 +20,7 @@ class LunaSea extends NotificationProvider {
return okMsg; return okMsg;
} }
if (heartbeatJSON["status"] == DOWN) { if (heartbeatJSON["status"] === DOWN) {
let downdata = { let downdata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"], "title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
@ -29,7 +29,7 @@ class LunaSea extends NotificationProvider {
return okMsg; return okMsg;
} }
if (heartbeatJSON["status"] == UP) { if (heartbeatJSON["status"] === UP) {
let updata = { let updata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"], "title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],

View file

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

View file

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

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

@ -25,14 +25,14 @@ class Pushbullet extends NotificationProvider {
"body": "Testing Successful.", "body": "Testing Successful.",
}; };
await axios.post(pushbulletUrl, testdata, config); await axios.post(pushbulletUrl, testdata, config);
} else if (heartbeatJSON["status"] == DOWN) { } else if (heartbeatJSON["status"] === DOWN) {
let downdata = { let downdata = {
"type": "note", "type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"], "title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}; };
await axios.post(pushbulletUrl, downdata, config); await axios.post(pushbulletUrl, downdata, config);
} else if (heartbeatJSON["status"] == UP) { } else if (heartbeatJSON["status"] === UP) {
let updata = { let updata = {
"type": "note", "type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"], "title": "UptimeKuma Alert: " + monitorJSON["name"],

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

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

View file

@ -26,10 +26,10 @@ class WeCom extends NotificationProvider {
composeMessage(heartbeatJSON, msg) { composeMessage(heartbeatJSON, msg) {
let title; let title;
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) { if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor 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"; title = "UptimeKuma Monitor Down";
} }
if (msg != null) { if (msg != null) {

View file

@ -31,6 +31,8 @@ const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat"); const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush"); const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta"); const Alerta = require("./notification-providers/alerta");
const OneBot = require("./notification-providers/onebot");
const PushDeer = require("./notification-providers/pushdeer");
class Notification { class Notification {
@ -73,6 +75,8 @@ class Notification {
new GoogleChat(), new GoogleChat(),
new Gorush(), new Gorush(),
new Alerta(), new Alerta(),
new OneBot(),
new PushDeer(),
]; ];
for (let item of list) { for (let item of list) {

View file

@ -9,24 +9,24 @@ const commonLabels = [
"monitor_port", "monitor_port",
]; ];
const monitor_cert_days_remaining = new PrometheusClient.Gauge({ const monitorCertDaysRemaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining", name: "monitor_cert_days_remaining",
help: "The number of days remaining until the certificate expires", help: "The number of days remaining until the certificate expires",
labelNames: commonLabels labelNames: commonLabels
}); });
const monitor_cert_is_valid = new PrometheusClient.Gauge({ const monitorCertIsValid = new PrometheusClient.Gauge({
name: "monitor_cert_is_valid", name: "monitor_cert_is_valid",
help: "Is the certificate still valid? (1 = Yes, 0= No)", help: "Is the certificate still valid? (1 = Yes, 0= No)",
labelNames: commonLabels labelNames: commonLabels
}); });
const monitor_response_time = new PrometheusClient.Gauge({ const monitorResponseTime = new PrometheusClient.Gauge({
name: "monitor_response_time", name: "monitor_response_time",
help: "Monitor Response Time (ms)", help: "Monitor Response Time (ms)",
labelNames: commonLabels labelNames: commonLabels
}); });
const monitor_status = new PrometheusClient.Gauge({ const monitorStatus = new PrometheusClient.Gauge({
name: "monitor_status", name: "monitor_status",
help: "Monitor Status (1 = UP, 0= DOWN)", help: "Monitor Status (1 = UP, 0= DOWN)",
labelNames: commonLabels labelNames: commonLabels
@ -49,13 +49,13 @@ class Prometheus {
if (typeof tlsInfo !== "undefined") { if (typeof tlsInfo !== "undefined") {
try { try {
let isValid = 0; let isValid;
if (tlsInfo.valid == true) { if (tlsInfo.valid === true) {
isValid = 1; isValid = 1;
} else { } else {
isValid = 0; isValid = 0;
} }
monitor_cert_is_valid.set(this.monitorLabelValues, isValid); monitorCertIsValid.set(this.monitorLabelValues, isValid);
} catch (e) { } catch (e) {
log.error("prometheus", "Caught error"); log.error("prometheus", "Caught error");
log.error("prometheus", e); log.error("prometheus", e);
@ -63,7 +63,7 @@ class Prometheus {
try { try {
if (tlsInfo.certInfo != null) { if (tlsInfo.certInfo != null) {
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining); monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
} }
} catch (e) { } catch (e) {
log.error("prometheus", "Caught error"); log.error("prometheus", "Caught error");
@ -72,7 +72,7 @@ class Prometheus {
} }
try { try {
monitor_status.set(this.monitorLabelValues, heartbeat.status); monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) { } catch (e) {
log.error("prometheus", "Caught error"); log.error("prometheus", "Caught error");
log.error("prometheus", e); log.error("prometheus", e);
@ -80,10 +80,10 @@ class Prometheus {
try { try {
if (typeof heartbeat.ping === "number") { if (typeof heartbeat.ping === "number") {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping); monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
} else { } else {
// Is it good? // Is it good?
monitor_response_time.set(this.monitorLabelValues, -1); monitorResponseTime.set(this.monitorLabelValues, -1);
} }
} catch (e) { } catch (e) {
log.error("prometheus", "Caught error"); log.error("prometheus", "Caught error");
@ -93,10 +93,10 @@ class Prometheus {
remove() { remove() {
try { try {
monitor_cert_days_remaining.remove(this.monitorLabelValues); monitorCertDaysRemaining.remove(this.monitorLabelValues);
monitor_cert_is_valid.remove(this.monitorLabelValues); monitorCertIsValid.remove(this.monitorLabelValues);
monitor_response_time.remove(this.monitorLabelValues); monitorResponseTime.remove(this.monitorLabelValues);
monitor_status.remove(this.monitorLabelValues); monitorStatus.remove(this.monitorLabelValues);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View file

@ -3,11 +3,11 @@ const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent"); const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent"); const SocksProxyAgent = require("socks-proxy-agent");
const { debug } = require("../src/util"); const { debug } = require("../src/util");
const server = require("./server"); const { UptimeKumaServer } = require("./uptime-kuma-server");
class Proxy { 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 * Saves and updates given proxy entity
@ -21,7 +21,7 @@ class Proxy {
let bean; let bean;
if (proxyID) { if (proxyID) {
bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]); bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [ proxyID, userID ]);
if (!bean) { if (!bean) {
throw new Error("proxy not found"); throw new Error("proxy not found");
@ -71,14 +71,14 @@ class Proxy {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
static async delete(proxyID, userID) { static async delete(proxyID, userID) {
const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]); const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [ proxyID, userID ]);
if (!bean) { if (!bean) {
throw new Error("proxy not found"); throw new Error("proxy not found");
} }
// Delete removed proxy from monitors if exists // Delete removed proxy from monitors if exists
await R.exec("UPDATE monitor SET proxy_id = null WHERE proxy_id = ?", [proxyID]); await R.exec("UPDATE monitor SET proxy_id = null WHERE proxy_id = ?", [ proxyID ]);
// Delete proxy from list // Delete proxy from list
await R.trash(bean); await R.trash(bean);
@ -151,6 +151,8 @@ class Proxy {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async reloadProxy() { static async reloadProxy() {
const server = UptimeKumaServer.getInstance();
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor"); let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
for (let monitorID in server.monitorList) { for (let monitorID in server.monitorList) {
@ -172,12 +174,12 @@ class Proxy {
*/ */
async function applyProxyEveryMonitor(proxyID, userID) { async function applyProxyEveryMonitor(proxyID, userID) {
// Find all monitors with id and proxy id // Find all monitors with id and proxy id
const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [userID]); const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [ userID ]);
// Update proxy id not match with given proxy id // Update proxy id not match with given proxy id
for (const monitor of monitors) { for (const monitor of monitors) {
if (monitor.proxy_id !== proxyID) { if (monitor.proxy_id !== proxyID) {
await R.exec("UPDATE monitor SET proxy_id = ? WHERE id = ?", [proxyID, monitor.id]); await R.exec("UPDATE monitor SET proxy_id = ? WHERE id = ?", [ proxyID, monitor.id ]);
} }
} }
} }

View file

@ -1,15 +1,16 @@
let express = require("express"); let express = require("express");
const { allowDevAllOrigin, getSettings, setting } = require("../util-server"); const { allowDevAllOrigin } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const server = require("../server");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { UP, flipStatus, log } = require("../../src/util"); const { UP, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page"); const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
let router = express.Router(); let router = express.Router();
let cache = apicache.middleware; let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
let io = server.io; let io = server.io;
router.get("/api/entry-page", async (request, response) => { router.get("/api/entry-page", async (request, response) => {
@ -195,14 +196,6 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
} }
}); });
/**
* Default is published
* @returns {Promise<boolean>}
*/
async function isPublished() {
return true;
}
function send403(res, msg = "") { function send403(res, msg = "") {
res.status(403).json({ res.status(403).json({
"status": "fail", "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"); console.log("Welcome to Uptime Kuma");
// Check Node.js Version // Check Node.js Version
@ -11,7 +16,7 @@ if (nodeVersion < requiredVersion) {
} }
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, debug } = require("../src/util"); const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
const config = require("./config"); const config = require("./config");
log.info("server", "Welcome to Uptime Kuma"); log.info("server", "Welcome to Uptime Kuma");
@ -26,14 +31,10 @@ log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Importing Node libraries"); log.info("server", "Importing Node libraries");
const fs = require("fs"); const fs = require("fs");
const http = require("http");
const https = require("https");
log.info("server", "Importing 3rd-party libraries"); log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express"); log.debug("server", "Importing express");
const express = require("express"); const express = require("express");
log.debug("server", "Importing socket.io");
const { Server } = require("socket.io");
log.debug("server", "Importing redbean-node"); log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken"); log.debug("server", "Importing jsonwebtoken");
@ -50,26 +51,10 @@ log.debug("server", "Importing 2FA Modules");
const notp = require("notp"); const notp = require("notp");
const base32 = require("thirty-two"); const base32 = require("thirty-two");
/** const { UptimeKumaServer } = require("./uptime-kuma-server");
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue. const server = UptimeKumaServer.getInstance(args);
* @type {UptimeKumaServer} const io = module.exports.io = server.io;
*/ const app = server.app;
class UptimeKumaServer {
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
entryPage = "dashboard";
async sendMonitorList(socket) {
let list = await getMonitorJSONList(socket.userID);
io.to(socket.userID).emit("monitorList", list);
return list;
}
}
const server = module.exports = new UptimeKumaServer();
log.info("server", "Importing this project modules"); log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor"); log.debug("server", "Importing Monitor");
@ -108,18 +93,15 @@ if (hostname) {
log.info("server", "Custom hostname: " + hostname); log.info("server", "Custom hostname: " + hostname);
} }
const port = [args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001] const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue)) .map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue)); .find(portValue => !isNaN(portValue));
// SSL const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
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 cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined; const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
// 2FA / notp verification defaults // 2FA / notp verification defaults
const twofa_verification_opts = { const twoFAVerifyOptions = {
"window": 1, "window": 1,
"time": 30 "time": 30
}; };
@ -134,25 +116,6 @@ if (config.demoMode) {
log.info("server", "==== Demo Mode ===="); log.info("server", "==== Demo Mode ====");
} }
log.info("server", "Creating express and socket.io instance");
const app = express();
let httpServer;
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, app);
} else {
log.info("server", "Server Type: HTTP");
httpServer = http.createServer(app);
}
const io = new Server(httpServer);
module.exports.io = io;
// Must be after io instantiation // Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client"); const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
@ -175,6 +138,7 @@ app.use(function (req, res, next) {
/** /**
* Total WebSocket client connected to server currently, no actual use * Total WebSocket client connected to server currently, no actual use
*
* @type {number} * @type {number}
*/ */
let totalClient = 0; let totalClient = 0;
@ -234,6 +198,13 @@ try {
} }
}); });
if (isDev) {
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.body);
response.send("OK");
});
}
// Robots.txt // Robots.txt
app.get("/robots.txt", async (_request, response) => { app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:"; let txt = "User-agent: *\nDisallow:";
@ -379,7 +350,7 @@ try {
} }
if (data.token) { 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) { if (user.twofa_last_token !== data.token && verify) {
afterLogin(socket, user); afterLogin(socket, user);
@ -546,7 +517,7 @@ try {
socket.userID, 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) { if (user.twofa_last_token !== token && verify) {
callback({ callback({
@ -712,6 +683,10 @@ try {
bean.dns_resolve_server = monitor.dns_resolve_server; bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken; bean.pushToken = monitor.pushToken;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null; 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); await R.store(bean);
@ -1228,7 +1203,7 @@ try {
} }
// Only starts importing if the backup file contains at least one proxy // 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"); const proxies = await R.findAll("proxy");
// Loop over proxy list and save proxies // Loop over proxy list and save proxies
@ -1236,7 +1211,7 @@ try {
const exists = proxies.find(item => item.id === proxy.id); const exists = proxies.find(item => item.id === proxy.id);
// Do not process when proxy already exists in import handle is skip and keep // Do not process when proxy already exists in import handle is skip and keep
if (["skip", "keep"].includes(importHandle) && !exists) { if ([ "skip", "keep" ].includes(importHandle) && !exists) {
return; return;
} }
@ -1471,12 +1446,12 @@ try {
log.info("server", "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); console.error("Cannot listen: " + err.message);
await shutdownFunction(); await shutdownFunction();
}); });
httpServer.listen(port, hostname, () => { server.httpServer.listen(port, hostname, () => {
if (hostname) { if (hostname) {
log.info("server", `Listening on ${hostname}:${port}`); log.info("server", `Listening on ${hostname}:${port}`);
} else { } else {
@ -1566,27 +1541,6 @@ 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. * Connect to the database and patch it if necessary.
* *
@ -1728,7 +1682,7 @@ function finalFunction() {
log.info("server", "Graceful shutdown successful!"); log.info("server", "Graceful shutdown successful!");
} }
gracefulShutdown(httpServer, { gracefulShutdown(server.httpServer, {
signals: "SIGINT SIGTERM", signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode development: false, // not in dev mode

View file

@ -1,6 +1,7 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server"); const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel"); 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 prefix = "cloudflared_";
const cloudflared = new CloudflaredTunnel(); const cloudflared = new CloudflaredTunnel();
@ -86,5 +87,7 @@ module.exports.autoStart = async (token) => {
module.exports.stop = async () => { module.exports.stop = async () => {
console.log("Stop cloudflared"); console.log("Stop cloudflared");
cloudflared.stop(); if (cloudflared) {
cloudflared.stop();
}
}; };

View file

@ -1,7 +1,8 @@
const { checkLogin } = require("../util-server"); const { checkLogin } = require("../util-server");
const { Proxy } = require("../proxy"); const { Proxy } = require("../proxy");
const { sendProxyList } = require("../client"); const { sendProxyList } = require("../client");
const server = require("../server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const server = UptimeKumaServer.getInstance();
module.exports.proxySocketHandler = (socket) => { module.exports.proxySocketHandler = (socket) => {
socket.on("addProxy", async (proxy, proxyID, callback) => { socket.on("addProxy", async (proxy, proxyID, callback) => {

View file

@ -6,7 +6,7 @@ const ImageDataURI = require("../image-data-uri");
const Database = require("../database"); const Database = require("../database");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page"); const StatusPage = require("../model/status_page");
const server = require("../server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
module.exports.statusPageSocketHandler = (socket) => { module.exports.statusPageSocketHandler = (socket) => {
@ -155,6 +155,9 @@ module.exports.statusPageSocketHandler = (socket) => {
//statusPage.search_engine_index = ; //statusPage.search_engine_index = ;
statusPage.show_tags = config.showTags; statusPage.show_tags = config.showTags;
//statusPage.password = null; //statusPage.password = null;
statusPage.footer_text = config.footerText;
statusPage.custom_css = config.customCSS;
statusPage.show_powered_by = config.showPoweredBy;
statusPage.modified_date = R.isoDateTime(); statusPage.modified_date = R.isoDateTime();
await R.store(statusPage); await R.store(statusPage);
@ -212,6 +215,8 @@ module.exports.statusPageSocketHandler = (socket) => {
]; ];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data); 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. // 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) { if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
server.entryPage = "statusPage-" + statusPage.slug; server.entryPage = "statusPage-" + statusPage.slug;
@ -281,6 +286,8 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete a status page // Delete a status page
socket.on("deleteStatusPage", async (slug, callback) => { socket.on("deleteStatusPage", async (slug, callback) => {
const server = UptimeKumaServer.getInstance();
try { try {
checkLogin(socket); checkLogin(socket);

View file

@ -0,0 +1,90 @@
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");
/**
* `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;
}
}
module.exports = {
UptimeKumaServer
};

View file

@ -9,6 +9,7 @@ const iconv = require("iconv-lite");
const chardet = require("chardet"); const chardet = require("chardet");
const fs = require("fs"); const fs = require("fs");
const nodeJsUtil = require("util"); const nodeJsUtil = require("util");
const mqtt = require("mqtt");
// From ping-lite // From ping-lite
exports.WIN = /^win/.test(process.platform); exports.WIN = /^win/.test(process.platform);
@ -26,7 +27,7 @@ exports.initJWTSecret = async () => {
"jwtSecret", "jwtSecret",
]); ]);
if (! jwtSecretBean) { if (!jwtSecretBean) {
jwtSecretBean = R.dispense("setting"); jwtSecretBean = R.dispense("setting");
jwtSecretBean.key = "jwtSecret"; jwtSecretBean.key = "jwtSecret";
} }
@ -88,14 +89,71 @@ exports.pingAsync = function (hostname, ipv6 = false) {
}); });
}; };
exports.dnsResolve = function (hostname, resolver_server, resolver_port, rrtype) { exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
return new Promise((resolve, reject) => {
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()}`);
}
}
});
});
};
exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
const resolver = new Resolver(); const resolver = new Resolver();
// Remove brackets from IPv6 addresses so we can re-add them to // Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300) // prevent issues with ::1:5300 (::1 port 5300)
resolver_server = resolver_server.replace("[", "").replace("]", ""); resolverServer = resolverServer.replace("[", "").replace("]", "");
resolver.setServers([`[${resolver_server}]:${resolver_port}`]); resolver.setServers([`[${resolverServer}]:${resolverPort}`]);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (rrtype == "PTR") { if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => { resolver.reverse(hostname, (err, records) => {
if (err) { if (err) {
reject(err); reject(err);
@ -209,7 +267,7 @@ const parseCertificateInfo = function (info) {
const existingList = {}; const existingList = {};
while (link) { while (link) {
log.debug("util", `[${i}] ${link.fingerprint}`); log.debug("cert", `[${i}] ${link.fingerprint}`);
if (!link.valid_from || !link.valid_to) { if (!link.valid_from || !link.valid_to) {
break; break;
@ -224,7 +282,7 @@ const parseCertificateInfo = function (info) {
if (link.issuerCertificate == null) { if (link.issuerCertificate == null) {
break; break;
} else if (link.issuerCertificate.fingerprint in existingList) { } else if (link.issuerCertificate.fingerprint in existingList) {
log.debug("util", `[Last] ${link.issuerCertificate.fingerprint}`); log.debug("cert", `[Last] ${link.issuerCertificate.fingerprint}`);
link.issuerCertificate = null; link.issuerCertificate = null;
break; break;
} else { } else {
@ -245,7 +303,7 @@ exports.checkCertificate = function (res) {
const info = res.request.res.socket.getPeerCertificate(true); const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false; const valid = res.request.res.socket.authorized || false;
log.debug("util", "Parsing Certificate Info"); log.debug("cert", "Parsing Certificate Info");
const parsedInfo = parseCertificateInfo(info); const parsedInfo = parseCertificateInfo(info);
return { return {
@ -260,19 +318,19 @@ exports.checkCertificate = function (res) {
// Return: true if the status code is within the accepted ranges, false otherwise // 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 // Will throw an error if the provided status code is not a valid range string or code string
exports.checkStatusCode = function (status, accepted_codes) { exports.checkStatusCode = function (status, acceptedCodes) {
if (accepted_codes == null || accepted_codes.length === 0) { if (acceptedCodes == null || acceptedCodes.length === 0) {
return false; return false;
} }
for (const code_range of accepted_codes) { for (const codeRange of acceptedCodes) {
const code_range_split = code_range.split("-").map(string => parseInt(string)); const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
if (code_range_split.length === 1) { if (codeRangeSplit.length === 1) {
if (status === code_range_split[0]) { if (status === codeRangeSplit[0]) {
return true; return true;
} }
} else if (code_range_split.length === 2) { } else if (codeRangeSplit.length === 2) {
if (status >= code_range_split[0] && status <= code_range_split[1]) { if (status >= codeRangeSplit[0] && status <= codeRangeSplit[1]) {
return true; return true;
} }
} else { } else {
@ -287,13 +345,13 @@ exports.getTotalClientInRoom = (io, roomName) => {
const sockets = io.sockets; const sockets = io.sockets;
if (! sockets) { if (!sockets) {
return 0; return 0;
} }
const adapter = sockets.adapter; const adapter = sockets.adapter;
if (! adapter) { if (!adapter) {
return 0; return 0;
} }
@ -318,7 +376,7 @@ exports.allowAllOrigin = (res) => {
}; };
exports.checkLogin = (socket) => { exports.checkLogin = (socket) => {
if (! socket.userID) { if (!socket.userID) {
throw new Error("You are not logged in."); throw new Error("You are not logged in.");
} }
}; };
@ -348,7 +406,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
exports.startUnitTest = async () => { exports.startUnitTest = async () => {
console.log("Starting unit test..."); console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = childProcess.spawn(npm, ["run", "jest"]); const child = childProcess.spawn(npm, [ "run", "jest" ]);
child.stdout.on("data", (data) => { child.stdout.on("data", (data) => {
console.log(data.toString()); console.log(data.toString());

View file

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

View file

@ -42,6 +42,7 @@ export default {
default: "No", default: "No",
}, },
}, },
emits: [ "yes" ],
data: () => ({ data: () => ({
modal: null, modal: null,
}), }),

View file

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

View file

@ -10,7 +10,7 @@ import { sleep } from "../util.ts";
export default { export default {
props: { props: {
value: [String, Number], value: [ String, Number ],
time: { time: {
type: Number, type: Number,
default: 0.3, default: 0.3,

View file

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

View file

@ -78,7 +78,7 @@ export default {
Confirm, Confirm,
}, },
props: {}, props: {},
emits: ["added"], emits: [ "added" ],
data() { data() {
return { return {
model: null, model: null,

View file

@ -220,6 +220,7 @@ export default {
if (newPeriod == "0") { if (newPeriod == "0") {
newPeriod = null; newPeriod = null;
this.heartbeatList = null; this.heartbeatList = null;
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
} else { } else {
this.loading = true; this.loading = true;
@ -228,6 +229,7 @@ export default {
toast.error(res.msg); toast.error(res.msg);
} else { } else {
this.heartbeatList = res.data; this.heartbeatList = res.data;
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
} }
this.loading = false; this.loading = false;
}); });
@ -248,6 +250,12 @@ export default {
}, },
{ deep: true } { 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> </script>
@ -286,6 +294,7 @@ export default {
.dark &:hover { .dark &:hover {
background: $dark-font-color; background: $dark-font-color;
color: $dark-font-color2;
} }
} }

View file

@ -105,7 +105,7 @@ export default {
Confirm, Confirm,
}, },
props: {}, props: {},
emits: ["added"], emits: [ "added" ],
data() { data() {
return { return {
model: null, model: null,

View file

@ -0,0 +1,34 @@
<template>
<div class="mb-3">
<div class="mb-3">
<label for="onebot-http-addr" class="form-label">{{ $t("onebotHttpAddress") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="HttpUrl" v-model="$parent.notification.httpAddr" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="onebot-access-token" class="form-label">AccessToken<span style="color: red;"><sup>*</sup></span></label>
<input id="HttpUrl" v-model="$parent.notification.accessToken" type="text" class="form-control" required>
<div class="form-text">
<p>{{ $t("onebotSafetyTips") }}</p>
</div>
</div>
<div class="mb-3">
<label for="onebot-msg-type" class="form-label">{{ $t("onebotMessageType") }}</label>
<select id="onebot-msg-type" v-model="$parent.notification.msgType" class="form-select">
<option value="group">{{ $t("onebotGroupMessage") }}</option>
<option value="private">{{ $t("onebotPrivateMessage") }}</option>
</select>
</div>
<div class="mb-3">
<label for="onebot-reciever-id" class="form-label">{{ $t("onebotUserOrGroupId") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretKey" v-model="$parent.notification.recieverId" type="text" class="form-control" required>
</div>
<div class="form-text">
<i18n-t tag="p" keypath="Read more:">
<a href="https://github.com/botuniverse/onebot-11" target="_blank">https://github.com/botuniverse/onebot-11</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -0,0 +1,19 @@
<template>
<div class="mb-3">
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="http://www.pushdeer.com/" rel="noopener noreferrer" target="_blank">http://www.pushdeer.com/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -29,6 +29,8 @@ import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.vue"; import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue"; import Gorush from "./Gorush.vue";
import Alerta from "./Alerta.vue"; import Alerta from "./Alerta.vue";
import OneBot from "./OneBot.vue";
import PushDeer from "./PushDeer.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -67,6 +69,8 @@ const NotificationFormList = {
"GoogleChat": GoogleChat, "GoogleChat": GoogleChat,
"gorush": Gorush, "gorush": Gorush,
"alerta": Alerta, "alerta": Alerta,
"OneBot": OneBot,
"PushDeer": PushDeer,
}; };
export default NotificationFormList; export default NotificationFormList;

View file

@ -4,7 +4,7 @@
<!-- Change Password --> <!-- Change Password -->
<template v-if="!settings.disableAuth"> <template v-if="!settings.disableAuth">
<p> <p>
{{ $t("Current User") }}: <strong>{{ username }}</strong> {{ $t("Current User") }}: <strong>{{ $root.username }}</strong>
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button> <button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
</p> </p>
@ -269,7 +269,6 @@ export default {
data() { data() {
return { return {
username: "",
invalidPassword: false, invalidPassword: false,
password: { password: {
currentPassword: "", currentPassword: "",
@ -297,10 +296,6 @@ export default {
}, },
}, },
mounted() {
this.loadUsername();
},
methods: { methods: {
savePassword() { savePassword() {
if (this.password.newPassword !== this.password.repeatNewPassword) { if (this.password.newPassword !== this.password.repeatNewPassword) {
@ -319,14 +314,6 @@ export default {
} }
}, },
loadUsername() {
const jwtPayload = this.$root.getJWTPayload();
if (jwtPayload) {
this.username = jwtPayload.username;
}
},
disableAuth() { disableAuth() {
this.settings.disableAuth = true; this.settings.disableAuth = true;
@ -334,6 +321,8 @@ export default {
// Set it to empty if done // Set it to empty if done
this.saveSettings(() => { this.saveSettings(() => {
this.password.currentPassword = ""; this.password.currentPassword = "";
this.$root.username = null;
this.$root.socket.token = "autoLogin";
}, this.password.currentPassword); }, this.password.currentPassword);
}, },

View file

@ -43,7 +43,7 @@ for (let lang in languageList) {
}; };
} }
const rtlLangs = ["fa"]; const rtlLangs = [ "fa" ];
export const currentLocale = () => localStorage.locale export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language || languageList[navigator.language] && navigator.language

View file

@ -34,11 +34,13 @@ import {
faAward, faAward,
faLink, faLink,
faChevronDown, faChevronDown,
faSignOutAlt,
faPen, faPen,
faExternalLinkSquareAlt, faExternalLinkSquareAlt,
faSpinner, faSpinner,
faUndo, faUndo,
faPlusCircle, faPlusCircle,
faAngleDown,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -72,11 +74,13 @@ library.add(
faAward, faAward,
faLink, faLink,
faChevronDown, faChevronDown,
faSignOutAlt,
faPen, faPen,
faExternalLinkSquareAlt, faExternalLinkSquareAlt,
faSpinner, faSpinner,
faUndo, faUndo,
faPlusCircle, faPlusCircle,
faAngleDown,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

View file

@ -197,7 +197,7 @@ export default {
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Status Page": "Статус страница", "Status Page": "Статус страница",
"Status Pages": "Статус страница", "Status Pages": "Статус страници",
"Primary Base URL": "Основен базов URL адрес", "Primary Base URL": "Основен базов URL адрес",
"Push URL": "Генериран Push URL адрес", "Push URL": "Генериран Push URL адрес",
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди", needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
@ -343,7 +343,7 @@ export default {
"No Monitors": "Няма монитори", "No Monitors": "Няма монитори",
"Untitled Group": "Група без заглавие", "Untitled Group": "Група без заглавие",
Services: "Услуги", Services: "Услуги",
Discard: "Премахни", Discard: "Отмени",
Cancel: "Отмени", Cancel: "Отмени",
"Powered by": "Създадено чрез", "Powered by": "Създадено чрез",
serwersms: "SerwerSMS.pl", serwersms: "SerwerSMS.pl",
@ -371,4 +371,97 @@ export default {
alertaAlertState: "Състояние на тревога", alertaAlertState: "Състояние на тревога",
alertaRecoverState: "Състояние на възстановяване", alertaRecoverState: "Състояние на възстановяване",
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?", deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
Proxies: "Прокси",
default: "По подразбиране",
enabled: "Включено",
setAsDefault: "Зададен по подразбиране",
deleteProxyMsg: "Сигурни ли сте, че желаете да изтриете това прокси за всички монитори?",
proxyDescription: "За да функционират трябва да бъдат зададени към монитор.",
enableProxyDescription: "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Може да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
setAsDefaultProxyDescription: "Това проки ще бъде включено по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
"Certificate Chain": "Верига на сертификата",
Valid: "Валиден",
Invalid: "Невалиден",
AccessKeyId: "ID на ключ за достъп",
SecretAccessKey: "Тайна на ключа за достъп",
PhoneNumbers: "Телефонни номера",
TemplateCode: "Шаблон Код",
SignName: "Знак име",
"Sms template must contain parameters: ": "SMS шаблонът трябва да съдържа следните параметри: ",
"Bark Endpoint": "Bark крайна точка",
WebHookUrl: "URL адрес на уеб кука",
SecretKey: "Таен ключ",
"For safety, must use secret key": "За сигурност, трябва да се използва таен ключ",
"Device Token": "Токен за устройство",
Platform: "Платформа",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Висок",
Retry: "Повтори",
Topic: "Тема",
"WeCom Bot Key": "WeCom бот ключ",
"Setup Proxy": "Настройка за прокси",
"Proxy Protocol": "Прокси протокол",
"Proxy Server": "Прокси сървър",
"Proxy server has authentication": "Прокси сървърът е с удостоверяване",
User: "Потребител",
Installed: "Инсталиран",
"Not installed": "Не е инсталиран",
Running: "Работи",
"Not running": "Не работи",
"Remove Token": "Премахни токен",
Start: "Старт",
Stop: "Стоп",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Добави нова статус страница",
Slug: "Слъг",
"Accept characters:": "Приеми символи:",
startOrEndWithOnly: "Започва или завършва само с {0}",
"No consecutive dashes": "Без последователни тирета",
Next: "Следващ",
"The slug is already taken. Please choose another slug.": "Този слъг вече се използва. Моля изберете друг.",
"No Proxy": "Без прокси",
"HTTP Basic Auth": "HTTP основно удостоверяване",
"New Status Page": "Нова статус страница",
"Page Not Found": "Страницата не е открита",
"Reverse Proxy": "Ревърс прокси",
Backup: "Архивиране",
About: "Относно",
wayToGetCloudflaredURL: "(Свалете \"cloudflared\" от {0})",
cloudflareWebsite: "Cloudflare уебсайт",
"Message:": "Съобщение:",
"Don't know how to get the token? Please read the guide:": "Не знаете как да вземете токен? Моля, прочетете ръководството:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Текущата връзка може да прекъсне ако в момента сте свързани чрез \"Cloudflare Tunnel\". Сигурни ли сте, че желаете да го спрете? Въведете Вашата текуща парола за да потвърдите.",
"Other Software": "Друг софтуер",
"For example: nginx, Apache and Traefik.": "Например: Nginx, Apache и Traefik.",
"Please read": "Моля, прочетете",
"Subject:": "Тема:",
"Valid To:": "Валиден до:",
"Days Remaining:": "Оставащи дни:",
"Issuer:": "Издател:",
"Fingerprint:": "Пръстов отпечатък:",
"No status pages": "Няма статус страници",
topic: "Тема",
topicExplanation: "MQTT тема за мониториране",
successMessage: "Съобщение при успех",
successMessageExplanation: "MQTT съобщение, което ще бъде считано за успех",
Customize: "Персонализирай",
"Custom Footer": "Персонализиран долен колонтитул",
"Custom CSS": "Потребителски CSS",
"Domain Name Expiry Notification": "Известяване при изтичащ домейн",
Proxy: "Прокси",
"Date Created": "Дата на създаване",
onebotHttpAddress: "OneBot HTTP адрес",
onebotMessageType: "OneBot тип съобщение",
onebotGroupMessage: "Група",
onebotPrivateMessage: "Лично",
onebotUserOrGroupId: "Група/Потребител ID",
onebotSafetyTips: "С цел безопасност трябва да зададете токен код за достъп",
"PushDeer Key": "PushDeer ключ",
"Footer Text": "Текст долен колонтитул",
"Show Powered By": "Покажи \"Създадено чрез\"",
"Domain Names": "Домейни",
signedInDisp: "Вписан като {0}",
signedInDispDisabled: "Удостоверяването е изключено.",
}; };

View file

@ -337,7 +337,7 @@ export default {
"Hide Tags": "Tags ausblenden", "Hide Tags": "Tags ausblenden",
Description: "Beschreibung", Description: "Beschreibung",
"No monitors available.": "Keine Monitore verfügbar.", "No monitors available.": "Keine Monitore verfügbar.",
"Add one": "Füge eins hinzu", "Add one": "Hinzufügen",
"No Monitors": "Keine Monitore", "No Monitors": "Keine Monitore",
"Untitled Group": "Gruppe ohne Titel", "Untitled Group": "Gruppe ohne Titel",
Services: "Dienste", Services: "Dienste",
@ -442,4 +442,7 @@ export default {
"Issuer:": "Aussteller:", "Issuer:": "Aussteller:",
"Fingerprint:": "Fingerabdruck:", "Fingerprint:": "Fingerabdruck:",
"No status pages": "Keine Status-Seiten", "No status pages": "Keine Status-Seiten",
Customize: "Anpassen",
"Custom Footer": "Eigener Footer",
"Custom CSS": "Eigenes CSS",
}; };

View file

@ -310,6 +310,10 @@ export default {
"One record": "One record", "One record": "One record",
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ", steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
"Current User": "Current User", "Current User": "Current User",
topic: "Topic",
topicExplanation: "MQTT topic to monitor",
successMessage: "Success Message",
successMessageExplanation: "MQTT message that will be considered as success",
recent: "Recent", recent: "Recent",
Done: "Done", Done: "Done",
Info: "Info", Info: "Info",
@ -355,6 +359,9 @@ export default {
serwersmsPhoneNumber: "Phone number", serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)", serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
stackfield: "Stackfield", stackfield: "Stackfield",
Customize: "Customize",
"Custom Footer": "Custom Footer",
"Custom CSS": "Custom CSS",
smtpDkimSettings: "DKIM Settings", smtpDkimSettings: "DKIM Settings",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.", smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "documentation", documentation: "documentation",
@ -418,7 +425,7 @@ export default {
"Add New Status Page": "Add New Status Page", "Add New Status Page": "Add New Status Page",
Slug: "Slug", Slug: "Slug",
"Accept characters:": "Accept characters:", "Accept characters:": "Accept characters:",
"startOrEndWithOnly": "Start or end with {0} only", startOrEndWithOnly: "Start or end with {0} only",
"No consecutive dashes": "No consecutive dashes", "No consecutive dashes": "No consecutive dashes",
Next: "Next", Next: "Next",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.", "The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
@ -444,6 +451,18 @@ export default {
"Fingerprint:": "Fingerprint:", "Fingerprint:": "Fingerprint:",
"No status pages": "No status pages", "No status pages": "No status pages",
"Domain Name Expiry Notification": "Domain Name Expiry Notification", "Domain Name Expiry Notification": "Domain Name Expiry Notification",
"Proxy": "Proxy", Proxy: "Proxy",
"Date Created": "Date Created", "Date Created": "Date Created",
onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group",
onebotPrivateMessage: "Private",
onebotUserOrGroupId: "Group/User ID",
onebotSafetyTips: "For safety, must set access token",
"PushDeer Key": "PushDeer Key",
"Footer Text": "Footer Text",
"Show Powered By": "Show Powered By",
"Domain Names": "Domain Names",
signedInDisp: "Signed in as {0}",
signedInDispDisabled: "Auth Disabled.",
}; };

View file

@ -171,7 +171,7 @@ export default {
"Avg. Response": "Gemiddelde Response", "Avg. Response": "Gemiddelde Response",
"Entry Page": "Entry Page", "Entry Page": "Entry Page",
statusPageNothing: "Niets hier, voeg een groep of monitor toe.", statusPageNothing: "Niets hier, voeg een groep of monitor toe.",
"No Services": "No Services", "No Services": "Geen diensten",
"All Systems Operational": "Alle systemen operationeel", "All Systems Operational": "Alle systemen operationeel",
"Partially Degraded Service": "Gedeeltelijk verminderde prestaties", "Partially Degraded Service": "Gedeeltelijk verminderde prestaties",
"Degraded Service": "Verminderde prestaties", "Degraded Service": "Verminderde prestaties",
@ -205,4 +205,262 @@ export default {
PushUrl: "Push URL", PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers is geen geldige JSON: ", HeadersInvalidFormat: "The request headers is geen geldige JSON: ",
BodyInvalidFormat: "De request body is geen geldige JSON: ", BodyInvalidFormat: "De request body is geen geldige JSON: ",
"Primary Base URL": "Hoofd Basis URL",
"Push URL": "Push URL",
needPushEvery: "Je moet deze URL elke {0} seconden aanroepen.",
pushOptionalParams: "Optionele parameters: {0}",
defaultNotificationName: "Mijn {notification} Alert ({number})",
here: "hier",
Required: "Verplicht",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "Je kunt een token krijgen van {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Ondersteuning Directe Chat / Groep / Kanaal Chat ID",
wayToGetTelegramChatID: "Je kunt je CHAT ID krijgen door een bericht te sturen naar de bot en naar deze URL te gaan om het chat_id te bekijken:",
"YOUR BOT TOKEN HERE": "DE BOT TOKEN HIER",
chatIDNotFound: "Chat ID is niet gevonden; stuur eerst een bericht naar de bot",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} is goed voor een moderne HTTP server zoals Express.js",
webhookFormDataDesc: "{multipart} is goed voor PHP. De JSON moet worden ontleed met {decodeFunction}",
secureOptionNone: "Geen / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Negeer TLS Error",
"From Email": "Van Email",
emailCustomSubject: "Aangepast Onderwerp",
"To Email": "Naar Email",
smtpCC: "CC",
smtpBCC: "BCC",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Je kunt dit krijgen door te gaan naar Server Instellingen -> Integraties -> Creëer Webhook",
"Bot Display Name": "Bot Weergave Naam",
"Prefix Custom Message": "Prefix Aangepast Bericht",
"Hello @everyone is...": "Hallo {'@'}iedereen is...",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Je kunt hier leren hoe je een webhook URL kunt maken {0}.",
Number: "Nummer",
Recipients: "Ontvangers",
needSignalAPI: "Je moet een signal client met REST API hebben.",
wayToCheckSignalURL: "Je kunt op deze URL zien hoe je een kunt instellen:",
signalImportant: "BELANGRIJK: Je kunt groepen en nummers niet mengen in ontvangers!",
"Application Token": "Applicatie Token",
"Server URL": "Server URL",
Priority: "Prioriteit",
"Icon Emoji": "Icoon Emoji",
"Channel Name": "Kanaal Naam",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Meer info over Webhooks op: {0}",
aboutChannelName: "Voer de kanaal naam in op {0} Kannaal Naam veld als je het Webhook kanaal wilt omzeilen. Bv: #other-channel",
aboutKumaURL: "Als je de Uptime Kuma URL veld leeg laat, wordt standaard het GitHub project pagina weergegeven.",
emojiCheatSheet: "Emoji cheat sheet: {0}",
PushByTechulus: "Push door Techulus",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (Google Workspace alleen)",
"User Key": "Gebruikers sleutel",
Device: "Apparaat",
"Message Title": "Bericht Titel",
"Notification Sound": "Notificatie Geluid",
"More info on:": "Meer info op: {0}",
pushoverDesc1: "Nood prioriteit (2) heeft standaard een 30 seconden timeout tussen pogingen en verloopt na 1 uur.",
pushoverDesc2: "Vul het appraat veld in als je notificaties naar andere apparaten wilt versturen.",
"SMS Type": "SMS Type",
octopushTypePremium: "Premium (Snel - aangeraden voor te alarmeren)",
octopushTypeLowCost: "Low Cost (Langzaam - wordt soms geblokkeerd door operator)",
checkPrice: "Controleer {0} prijzen:",
apiCredentials: "API referenties",
octopushLegacyHint: "Wil je de legacy versie van Octopush (2011-2020) gebruiken of de nieuwe versie?",
"Check octopush prices": "Controleer Octopush prijzen {0}.",
octopushPhoneNumber: "Telefoon nummer (Int. formaat, eg : +33612345678) ",
octopushSMSSender: "SMS zender naam : 3-11 alfanumerieke karakters en spatie (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Apparaat ID",
"Apprise URL": "Apprise URL",
"Example:": "Voorbeeld: {0}",
"Read more:": "Lees meer: {0}",
"Status:": "Status: {0}",
"Read more": "Lees meer",
appriseInstalled: "Apprise is geïnstalleerd.",
appriseNotInstalled: "Apprise is niet geïnstalleerd. {0}",
"Access Token": "Access Token",
"Channel access token": "Kanaal access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basis Instellingen",
"User ID": "Gebruiker ID",
"Messaging API": "Berichten API",
wayToGetLineChannelToken: "Begin met {0} te openen, creëer een provider en kanaal (Messaging API), dan kun je de kanaal access token en gebruikers ID van de hierboven genoemde menu items krijgen.",
"Icon URL": "Icoon URL",
aboutIconURL: "Je kunt een link om de standaard profiel afbeelding te overschrijving in \"Icoon URL\" meegeven. Dit wordt niet gebruikt als Icon Emoji is ingesteld.",
aboutMattermostChannelName: "Je kunt het standaard kanaal dat de Webhook plaatst overschijven door de kanaal naam in te vullen in het \"Channel Name\" veld. Dit moet worden ingeschakeld in de Mattermost Webhook instellingen. Bv. #ander-kanaal",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - Goedkoop maar langzaam en vaak overbelast. Gelimiteerd tot Poolse ontvangers.",
promosmsTypeFlash: "SMS FLASH - Berichten worden automatisch weergegeven op het apparaat van de ontvanger. Gelimiteerd tot Poolse ontvangers.",
promosmsTypeFull: "SMS FULL - Premium tier van SMS, je kunt de ontvanger naam gebruiken (Je moet eerst de naam registreren). Betrouwbaar voor alarmeringen.",
promosmsTypeSpeed: "SMS SPEED - Hoogste prioriteit in systeem. Is veel sneller en betrouwbaarder maar kost meer (ongeveer twee keer zoveel als volle SMS prijs).",
promosmsPhoneNumber: "Telefoon nummer (voor Poolse ontvangers. Je kunt gebieds codes overslaan)",
promosmsSMSSender: "SMS Ontvanger naam : Voor geregistreerde naam of een van de standaarden: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (met http(s):// en optioneel poort)",
"Internal Room Id": "Interne Room ID",
matrixDesc1: "Je kunt de interne room ID vinden door in de geavanceerde sectie van de room instellingen in je Matrix client te kijken. Het zou moeten uitzien als !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Het wordt ten zeerste aanbevolen om een nieuwe gebruiker aan te maken en niet de access token van je account te gebruiken, aangezien dit volledige toegang geeft tot je account en alle kamers waar je lid van bent. Maak in plaats daarvan een nieuwe gebruiker aan en nodig deze alleen uit voor de ruimte waarin je de melding wilt ontvangen. Je kunt de access token krijgen door het volgende uit te voeren {0}",
"Monitor History": "Monitor Geschiedenis",
clearDataOlderThan: "Bewaar monitor geschiedenis voor {0} dagen.",
PasswordsDoNotMatch: "Wachtwoorden komen niet overeen",
records: "records",
"One record": "Een record",
steamApiKeyDescription: "Om een Steam Game Server te monitoren heb je een Steam Web-API key nodig. Je kunt hier je API key registreren: ",
"Current User": "Huidge Gebruiker",
topic: "Onderwerp",
topicExplanation: "MQTT onderwerp om te monitoren",
successMessage: "Succesbericht",
successMessageExplanation: "MQTT bericht dat als succes wordt beschouwd.",
recent: "Recent",
Done: "Klaar",
Info: "Info",
Security: "Beveiliging",
"Steam API Key": "Steam API Sleutel",
"Shrink Database": "Verklein Database",
"Pick a RR-Type...": "Kies een RR-Type...",
"Pick Accepted Status Codes...": "Kies geaccepteerde Status Codes...",
Default: "Standaard",
"HTTP Options": "HTTP Opties",
"Create Incident": "Creëer Incident",
Title: "Titel",
Content: "Content",
Style: "Stijl",
info: "info",
warning: "waarschuwing",
danger: "gevaar",
primary: "primair",
light: "licht",
dark: "donker",
Post: "Post",
"Please input title and content": "Voer alstublieft titel en content in",
Created: "Gemaakt",
"Last Updated": "Laatst Bijgewerkt",
Unpin: "Losmaken",
"Switch to Light Theme": "Wissel naar Licht Thema",
"Switch to Dark Theme": "Wissel naar Donker Thema",
"Show Tags": "Toon Labels",
"Hide Tags": "Verberg Labels",
Description: "Beschrijving",
"No monitors available.": "Geen monitors beschikbaar.",
"Add one": "Voeg een toe",
"No Monitors": "Geen Monitors",
"Untitled Group": "Naamloze Groep",
Services: "Diensten",
Discard: "Weggooien",
Cancel: "Annuleren",
"Powered by": "Mogelijk gemaakt door",
shrinkDatabaseDescription: "Trigger database VACUUM voor SQLite. Als de database na 1.10.0 gemaakt is, dan is AUTO_VACUUM al aangezet en deze actie niet nodig.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Gebruikersnaam (incl. webapi_ prefix)",
serwersmsAPIPassword: "API Wachtwoord",
serwersmsPhoneNumber: "Telefoon nummer",
serwersmsSenderName: "SMS Zender Naam (geregistreerd via klant portaal)",
stackfield: "Stackfield",
Customize: "Aanpassen",
"Custom Footer": "Aangepaste Footer",
"Custom CSS": "Aangepaste CSS",
smtpDkimSettings: "DKIM Instellingen",
smtpDkimDesc: "Refereer alsjeblieft naar Nodemailer DKIM {0} voor gebruik.",
documentation: "documentatie",
smtpDkimDomain: "Domein Naam",
smtpDkimKeySelector: "Sleutel Kiezer",
smtpDkimPrivateKey: "Prive Sleutel",
smtpDkimHashAlgo: "Hash Algoritme (Optioneel)",
smtpDkimheaderFieldNames: "Header sleutels om te ondertekenen (Optioneel)",
smtpDkimskipFields: "Header sleutels niet om te ondertekenen (Optioneel)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Eindpunt",
alertaEnvironment: "Omgeving",
alertaApiKey: "API Sleutel",
alertaAlertState: "Alert Staat",
alertaRecoverState: "Herstel Staat",
deleteStatusPageMsg: "Weet je zeker je deze status pagina wilt verwijderen?",
Proxies: "Proxies",
default: "Standaard",
enabled: "Ingeschakeld",
setAsDefault: "Stel in als standaard",
deleteProxyMsg: "Weet je zeker dat je deze proxy wilt verwijderen voor alle monitors?",
proxyDescription: "Proxies moeten worden toegewezen aan een monitor om te functioneren.",
enableProxyDescription: "Deze proxy heeft geen effect op monitor verzoeken totdat het is geactiveerd. Je kunt tijdelijk de proxy uitschakelen voor alle monitors voor activatie status.",
setAsDefaultProxyDescription: "Deze proxy wordt standaard aangezet voor alle nieuwe monitors. Je kunt nog steeds de proxy apart uitschakelen voor elke monitor.",
"Certificate Chain": "Certificaat Chain",
Valid: "Geldig",
Invalid: "Ongeldig",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "TelefoonNummers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms sjabloon moet de volgende parameters bevatten: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "Voor de veiligheid moet je de secret key gebruiken",
"Device Token": "Apparaat Token",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Hoog",
Retry: "Opnieuw",
Topic: "Onderwerp",
"WeCom Bot Key": "WeCom Bot Sleutel",
"Setup Proxy": "Proxy instellen",
"Proxy Protocol": "Proxy Protocol",
"Proxy Server": "Proxy Server",
"Proxy server has authentication": "Proxy server heeft authenticatie",
User: "Gebruiker",
Installed: "Geïnstalleerd",
"Not installed": "Niet geïnstalleerd",
Running: "Actief",
"Not running": "Niet actief",
"Remove Token": "Verwijder Token",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Voeg nieuwe status pagina toe",
Slug: "Slug",
"Accept characters:": "Geaccepteerde tekens:",
startOrEndWithOnly: "Start of eindig alleen met {0}",
"No consecutive dashes": "Geen opeenvolgende streepjes",
Next: "Volgende",
"The slug is already taken. Please choose another slug.": "De slug is al in gebruik. Kies een andere slug.",
"No Proxy": "Geen Proxy",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "Nieuwe Status Pagina",
"Page Not Found": "Pagina Niet gevonden",
"Reverse Proxy": "Reverse Proxy",
Backup: "Backup",
About: "Over",
wayToGetCloudflaredURL: "(Download cloudflared van {0})",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Bericht:",
"Don't know how to get the token? Please read the guide:": "Lees de uitleg als je niet weet hoe je een token krijgt:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "De huidge verbinding kan worden verbroken als je momenteel bent verbonden met Cloudflare Tunnel. Weet je zeker dat je het wilt stoppen? Typ je huidige wachtwoord om het te bevestigen.",
"Other Software": "Andere Software",
"For example: nginx, Apache and Traefik.": "Bijvoorbeeld: nginx, Apache and Traefik.",
"Please read": "Lees alstublieft",
"Subject:": "Onderwerp:",
"Valid To:": "Geldig Tot:",
"Days Remaining:": "Dagen Resterend:",
"Issuer:": "Uitgever:",
"Fingerprint:": "Vingerafruk:",
"No status pages": "Geen status pagina's",
"Domain Name Expiry Notification": "Domein Naam Verloop Notificatie",
Proxy: "Proxy",
"Date Created": "Datum Aangemaakt",
onebotHttpAddress: "OneBot HTTP Adres",
onebotMessageType: "OneBot Bericht Type",
onebotGroupMessage: "Groep",
onebotPrivateMessage: "Privé",
onebotUserOrGroupId: "Groep/Gebruiker ID",
onebotSafetyTips: "Voor de veiligheid moet een toegangssleutel worden ingesteld",
"PushDeer Key": "PushDeer Key",
"Footer Text": "Footer Tekst",
"Show Powered By": "Laat 'Mogeljik gemaakt door' zien",
"Domain Names": "Domein Namen",
}; };

View file

@ -374,8 +374,8 @@ export default {
serwersmsSenderName: "SMS Имя Отправителя (регистрированный через пользовательский портал)", serwersmsSenderName: "SMS Имя Отправителя (регистрированный через пользовательский портал)",
stackfield: "Stackfield", stackfield: "Stackfield",
smtpDkimSettings: "DKIM Настройки", smtpDkimSettings: "DKIM Настройки",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.", smtpDkimDesc: "Пожалуйста ознакомьтесь с {0} Nodemailer DKIM для использования.",
documentation: "документация", documentation: "документацией",
smtpDkimDomain: "Имя Домена", smtpDkimDomain: "Имя Домена",
smtpDkimKeySelector: "Ключ", smtpDkimKeySelector: "Ключ",
smtpDkimPrivateKey: "Приватный ключ", smtpDkimPrivateKey: "Приватный ключ",
@ -389,4 +389,12 @@ export default {
alertaApiKey: "Ключ API", alertaApiKey: "Ключ API",
alertaAlertState: "Состояние алерта", alertaAlertState: "Состояние алерта",
alertaRecoverState: "Состояние восстановления", alertaRecoverState: "Состояние восстановления",
Proxies: "Прокси",
default: "По умолчанию",
enabled: "Включено",
setAsDefault: "Установлено по умолчанию",
deleteProxyMsg: "Вы действительно хотите удалить этот прокси для всех мониторов?",
proxyDescription: "Прокси должны быть привязаны к монитору, чтобы работать.",
enableProxyDescription: "Этот прокси не будет влиять на запросы монитора, пока не будет активирован. Вы можете контролировать временное отключение прокси для всех мониторов через статус активации.",
setAsDefaultProxyDescription: "Этот прокси будет по умолчанию включен для новых мониторов. Вы всё ещё можете отдельно отключать прокси в каждом мониторе.",
}; };

View file

@ -434,7 +434,7 @@ export default {
"Add New Status Page": "添加新的状态页", "Add New Status Page": "添加新的状态页",
Slug: "路径", Slug: "路径",
"Accept characters:": "可接受的字符:", "Accept characters:": "可接受的字符:",
"startOrEndWithOnly": "开头和结尾必须为 {0}", startOrEndWithOnly: "开头和结尾必须为 {0}",
"No consecutive dashes": "不能有连续的破折号", "No consecutive dashes": "不能有连续的破折号",
Next: "下一步", Next: "下一步",
"The slug is already taken. Please choose another slug.": "该路径已被使用。请选择其他路径。", "The slug is already taken. Please choose another slug.": "该路径已被使用。请选择其他路径。",
@ -450,6 +450,23 @@ export default {
"Fingerprint:": "指纹:", "Fingerprint:": "指纹:",
"No status pages": "无状态页", "No status pages": "无状态页",
"Domain Name Expiry Notification": "域名到期时通知", "Domain Name Expiry Notification": "域名到期时通知",
"Proxy": "代理", Proxy: "代理",
"Date Created": "创建于", "Date Created": "创建于",
onebotHttpAddress: "OneBot HTTP 地址",
onebotMessageType: "OneBot 消息类型",
onebotGroupMessage: "群聊",
onebotPrivateMessage: "私聊",
onebotUserOrGroupId: "群组/用户ID",
onebotSafetyTips: "出于安全原因,请务必设置 AccessToken",
topic: "Topic",
topicExplanation: "MQTT 传递给监控的 Topic",
successMessage: "成功时消息",
successMessageExplanation: "MQTT 成功时所传递的消息",
Customize: "自定义",
"Custom Footer": "自定义底部",
"Custom CSS": "自定义 CSS",
"PushDeer Key": "PushDeer Key",
"Footer Text": "底部自定义文本",
"Show Powered By": "显示 Powered By",
"Domain Names": "域名",
}; };

View file

@ -33,7 +33,7 @@ export default {
Appearance: "外觀", Appearance: "外觀",
Theme: "主題", Theme: "主題",
General: "一般", General: "一般",
"Primary Base URL": "主要基底 URL", "Primary Base URL": "主要基底網址",
Version: "版本", Version: "版本",
"Check Update On GitHub": "在 GitHub 檢查更新", "Check Update On GitHub": "在 GitHub 檢查更新",
List: "清單", List: "清單",
@ -307,9 +307,12 @@ export default {
PasswordsDoNotMatch: "密碼不相符。", PasswordsDoNotMatch: "密碼不相符。",
records: "記錄", records: "記錄",
"One record": "一項記錄", "One record": "一項記錄",
"Showing {from} to {to} of {count} records": "正在顯示 {count} 項記錄中的 {from} 至 {to} 項",
steamApiKeyDescription: "若要監測 Steam 遊戲伺服器,您將需要 Steam Web-API 金鑰。您可以在此註冊您的 API 金鑰:", steamApiKeyDescription: "若要監測 Steam 遊戲伺服器,您將需要 Steam Web-API 金鑰。您可以在此註冊您的 API 金鑰:",
"Current User": "目前使用者", "Current User": "目前使用者",
topic: "Topic",
topicExplanation: "要監測的 MQTT Topic",
successMessage: "成功訊息",
successMessageExplanation: "視為成功的 MQTT 訊息",
recent: "最近", recent: "最近",
Done: "完成", Done: "完成",
Info: "資訊", Info: "資訊",
@ -355,6 +358,9 @@ export default {
serwersmsPhoneNumber: "電話號碼", serwersmsPhoneNumber: "電話號碼",
serwersmsSenderName: "SMS 寄件人名稱 (由客戶入口網站註冊)", serwersmsSenderName: "SMS 寄件人名稱 (由客戶入口網站註冊)",
stackfield: "Stackfield", stackfield: "Stackfield",
Customize: "自訂",
"Custom Footer": "自訂頁尾",
"Custom CSS": "自訂 CSS",
smtpDkimSettings: "DKIM 設定", smtpDkimSettings: "DKIM 設定",
smtpDkimDesc: "請參考 Nodemailer DKIM {0} 使用方式。", smtpDkimDesc: "請參考 Nodemailer DKIM {0} 使用方式。",
documentation: "文件", documentation: "文件",
@ -366,7 +372,7 @@ export default {
smtpDkimskipFields: "不簽署的郵件標頭 (選填)", smtpDkimskipFields: "不簽署的郵件標頭 (選填)",
gorush: "Gorush", gorush: "Gorush",
alerta: "Alerta", alerta: "Alerta",
alertaApiEndpoint: "API Endpoint", alertaApiEndpoint: "API 端點",
alertaEnvironment: "環境", alertaEnvironment: "環境",
alertaApiKey: "API 金鑰", alertaApiKey: "API 金鑰",
alertaAlertState: "警示狀態", alertaAlertState: "警示狀態",
@ -380,4 +386,80 @@ export default {
proxyDescription: "必須將代理伺服器指派給監測器才能運作。", proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。", enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。", setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
"Certificate Chain": "憑證鏈結",
Valid: "有效",
Invalid: "無效",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey 密碼",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms 範本必須包含參數:",
"Bark Endpoint": "Bark 端點",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "為了安全起見,必須使用秘密金鑰",
"Device Token": "裝置權杖",
Platform: "平台",
iOS: "iOS",
Android: "Android",
Huawei: "華為",
High: "高",
Retry: "重試",
Topic: "Topic",
"WeCom Bot Key": "WeCom 機器人金鑰",
"Setup Proxy": "設置 Proxy",
"Proxy Protocol": "Proxy 通訊協定",
"Proxy Server": "Proxy 伺服器",
"Proxy server has authentication": "Proxy 伺服器啟用了驗證功能",
User: "使用者",
Installed: "已安裝",
"Not installed": "未安裝",
Running: "執行中",
"Not running": "未執行",
"Remove Token": "移除權杖",
Start: "開始",
Stop: "停止",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "新增狀態頁",
Slug: "Slug",
"Accept characters:": "可用字元:",
startOrEndWithOnly: "僅能使用 {0} 開頭或結尾",
"No consecutive dashes": "不得連續使用破折號",
Next: "下一步",
"The slug is already taken. Please choose another slug.": "此 slug 已被使用。請選擇其他 slug。",
"No Proxy": "無 Proxy",
"HTTP Basic Auth": "HTTP 基本驗證",
"New Status Page": "新狀態頁",
"Page Not Found": "找不到頁面",
"Reverse Proxy": "反向代理",
Backup: "備份",
About: "關於",
wayToGetCloudflaredURL: "(從 {0} 下載 cloudflared)",
cloudflareWebsite: "Cloudflare 網站",
"Message:": "訊息:",
"Don't know how to get the token? Please read the guide:": "不知道如何取得權杖嗎?請閱讀指南:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您目前正透過 Cloudflare Tunnel 連線,可能會導致連線中斷。您確定要停止嗎?請輸入密碼以確認。",
"Other Software": "其他軟體",
"For example: nginx, Apache and Traefik.": "例如 nginx、Apache 和 Traefik。",
"Please read": "請閱覽",
"Subject:": "簽發給:",
"Valid To:": "有效期限:",
"Days Remaining:": "剩餘天數:",
"Issuer:": "簽發者:",
"Fingerprint:": "指紋:",
"No status pages": "無狀態頁",
"Domain Name Expiry Notification": "網域名稱到期通知",
Proxy: "Proxy",
"Date Created": "建立日期",
onebotHttpAddress: "OneBot HTTP 位址",
onebotMessageType: "OneBot 訊息類型",
onebotGroupMessage: "群組",
onebotPrivateMessage: "私人",
onebotUserOrGroupId: "群組/使用者 ID",
onebotSafetyTips: "為了安全起見,必須設置存取權杖",
"PushDeer Key": "PushDeer 金鑰",
"Footer Text": "頁尾文字",
"Show Powered By": "顯示技術支援文字",
"Domain Names": "網域名稱",
}; };

View file

@ -32,9 +32,32 @@
</router-link> </router-link>
</li> </li>
<li v-if="$root.loggedIn" class="nav-item"> <li v-if="$root.loggedIn" class="nav-item">
<router-link to="/settings" class="nav-link" :class="{ active: $route.path.includes('settings') }"> <div class="dropdown dropdown-profile-pic">
<font-awesome-icon icon="cog" /> {{ $t("Settings") }} <div type="button" class="nav-link" data-bs-toggle="dropdown">
</router-link> <div class="profile-pic">{{ $root.usernameFirstChar }}</div>
<font-awesome-icon icon="angle-down" />
</div>
<ul class="dropdown-menu">
<li>
<i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
<strong>{{ $root.username }}</strong>
</i18n-t>
<span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<router-link to="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
</router-link>
</li>
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
<button class="dropdown-item" @click="$root.logout">
<font-awesome-icon icon="sign-out-alt" />
{{ $t("Logout") }}
</button>
</li>
</ul>
</div>
</li> </li>
</ul> </ul>
</header> </header>
@ -192,6 +215,81 @@ main {
z-index: 99999; z-index: 99999;
} }
// Profile Pic Button with Dropdown
.dropdown-profile-pic {
user-select: none;
.nav-link {
cursor: pointer;
display: flex;
gap: 6px;
align-items: center;
background-color: rgba(200, 200, 200, 0.2);
padding: 0.5rem 0.8rem;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
}
.dropdown-menu {
transition: all 0.2s;
padding-left: 0;
padding-bottom: 0;
margin-top: 8px !important;
border-radius: 16px;
overflow: hidden;
.dropdown-divider {
margin: 0;
border-top: 1px solid rgba(0, 0, 0, 0.4);
background-color: transparent;
}
.dropdown-item-text {
font-size: 14px;
padding-bottom: 0.7rem;
}
.dropdown-item {
padding: 0.7rem 1rem;
}
.dark & {
background-color: $dark-bg;
color: $dark-font-color;
border-color: $dark-border-color;
.dropdown-item {
color: $dark-font-color;
&.active {
color: $dark-font-color2;
background-color: $highlight !important;
}
&:hover {
background-color: $dark-bg2;
}
}
}
}
.profile-pic {
display: flex;
align-items: center;
justify-content: center;
color: white;
background-color: $primary;
width: 24px;
height: 24px;
margin-right: 5px;
border-radius: 50rem;
font-weight: bold;
font-size: 10px;
}
}
.dark { .dark {
header { header {
background-color: $dark-header-bg; background-color: $dark-header-bg;

View file

@ -1,6 +1,6 @@
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import jwt_decode from "jwt-decode"; import jwtDecode from "jwt-decode";
import Favico from "favico.js"; import Favico from "favico.js";
const toast = useToast(); const toast = useToast();
@ -28,6 +28,7 @@ export default {
connectCount: 0, connectCount: 0,
initedSocketIO: false, initedSocketIO: false,
}, },
username: null,
remember: (localStorage.remember !== "0"), remember: (localStorage.remember !== "0"),
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed. allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
loggedIn: false, loggedIn: false,
@ -89,7 +90,7 @@ export default {
} }
socket = io(wsHost, { socket = io(wsHost, {
transports: ["websocket"], transports: [ "websocket" ],
}); });
socket.on("info", (info) => { socket.on("info", (info) => {
@ -103,12 +104,13 @@ export default {
socket.on("autoLogin", (monitorID, data) => { socket.on("autoLogin", (monitorID, data) => {
this.loggedIn = true; this.loggedIn = true;
this.storage().token = "autoLogin"; this.storage().token = "autoLogin";
this.socket.token = "autoLogin";
this.allowLoginDialog = false; this.allowLoginDialog = false;
}); });
socket.on("monitorList", (data) => { socket.on("monitorList", (data) => {
// Add Helper function // Add Helper function
Object.entries(data).forEach(([monitorID, monitor]) => { Object.entries(data).forEach(([ monitorID, monitor ]) => {
monitor.getUrl = () => { monitor.getUrl = () => {
try { try {
return new URL(monitor.url); return new URL(monitor.url);
@ -233,7 +235,6 @@ export default {
if (token !== "autoLogin") { if (token !== "autoLogin") {
this.loginByToken(token); this.loginByToken(token);
} else { } else {
// Timeout if it is not actually auto login // Timeout if it is not actually auto login
setTimeout(() => { setTimeout(() => {
if (! this.loggedIn) { if (! this.loggedIn) {
@ -241,7 +242,6 @@ export default {
this.$root.storage().removeItem("token"); this.$root.storage().removeItem("token");
} }
}, 5000); }, 5000);
} }
} else { } else {
this.allowLoginDialog = true; this.allowLoginDialog = true;
@ -266,7 +266,7 @@ export default {
const jwtToken = this.$root.storage().token; const jwtToken = this.$root.storage().token;
if (jwtToken && jwtToken !== "autoLogin") { if (jwtToken && jwtToken !== "autoLogin") {
return jwt_decode(jwtToken); return jwtDecode(jwtToken);
} }
return undefined; return undefined;
}, },
@ -305,6 +305,7 @@ export default {
this.storage().token = res.token; this.storage().token = res.token;
this.socket.token = res.token; this.socket.token = res.token;
this.loggedIn = true; this.loggedIn = true;
this.username = this.getJWTPayload()?.username;
// Trigger Chrome Save Password // Trigger Chrome Save Password
history.pushState({}, ""); history.pushState({}, "");
@ -322,6 +323,7 @@ export default {
this.logout(); this.logout();
} else { } else {
this.loggedIn = true; this.loggedIn = true;
this.username = this.getJWTPayload()?.username;
} }
}); });
}, },
@ -331,6 +333,7 @@ export default {
this.storage().removeItem("token"); this.storage().removeItem("token");
this.socket.token = null; this.socket.token = null;
this.loggedIn = false; this.loggedIn = false;
this.username = null;
this.clearData(); this.clearData();
}, },
@ -398,6 +401,14 @@ export default {
computed: { computed: {
usernameFirstChar() {
if (typeof this.username == "string" && this.username.length >= 1) {
return this.username.charAt(0).toUpperCase();
} else {
return "🐻";
}
},
lastHeartbeatList() { lastHeartbeatList() {
let result = {}; let result = {};

View file

@ -32,6 +32,9 @@
<option value="steam"> <option value="steam">
Steam Game Server Steam Game Server
</option> </option>
<option value="mqtt">
MQTT
</option>
</select> </select>
</div> </div>
@ -67,15 +70,15 @@
</div> </div>
<!-- Hostname --> <!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam only --> <!-- TCP Port / Ping / DNS / Steam / MQTT only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
</div> </div>
<!-- Port --> <!-- Port -->
<!-- For TCP Port / Steam Type --> <!-- For TCP Port / Steam / MQTT Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label> <label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
@ -124,6 +127,36 @@
</div> </div>
</template> </template>
<!-- MQTT -->
<!-- For MQTT Type -->
<template v-if="monitor.type === 'mqtt'">
<div class="my-3">
<label for="mqttUsername" class="form-label">MQTT {{ $t("Username") }}</label>
<input id="mqttUsername" v-model="monitor.mqttUsername" type="text" class="form-control">
</div>
<div class="my-3">
<label for="mqttPassword" class="form-label">MQTT {{ $t("Password") }}</label>
<input id="mqttPassword" v-model="monitor.mqttPassword" type="password" class="form-control">
</div>
<div class="my-3">
<label for="mqttTopic" class="form-label">MQTT {{ $t("Topic") }}</label>
<input id="mqttTopic" v-model="monitor.mqttTopic" type="text" class="form-control" required>
<div class="form-text">
{{ $t("topicExplanation") }}
</div>
</div>
<div class="my-3">
<label for="mqttSuccessMessage" class="form-label">MQTT {{ $t("successMessage") }}</label>
<input id="mqttSuccessMessage" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
<div class="form-text">
{{ $t("successMessageExplanation") }}
</div>
</div>
</template>
<!-- Interval --> <!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@ -148,7 +181,7 @@
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div class="my-3 form-check"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox"> <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="expiry-notification"> <label class="form-check-label" for="expiry-notification">
{{ $t("Domain Name Expiry Notification") }} {{ $t("Domain Name Expiry Notification") }}
@ -402,17 +435,17 @@ export default {
}, },
bodyPlaceholder() { bodyPlaceholder() {
return this.$t("Example:", [` return this.$t("Example:", [ `
{ {
"key": "value" "key": "value"
}`]); }` ]);
}, },
headersPlaceholder() { headersPlaceholder() {
return this.$t("Example:", [` return this.$t("Example:", [ `
{ {
"HeaderName": "HeaderValue" "HeaderName": "HeaderValue"
}`]); }` ]);
} }
}, },
@ -506,10 +539,14 @@ export default {
upsideDown: false, upsideDown: false,
expiryNotification: false, expiryNotification: false,
maxredirects: 10, maxredirects: 10,
accepted_statuscodes: ["200-299"], accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A", dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1", dns_resolve_server: "1.1.1.1",
proxyId: null, proxyId: null,
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
}; };
if (this.$root.proxyList && !this.monitor.proxyId) { if (this.$root.proxyList && !this.monitor.proxyId) {

View file

@ -16,6 +16,14 @@
{{ item.title }} {{ item.title }}
</div> </div>
</router-link> </router-link>
<!-- Logout Button -->
<a v-if="$root.isMobile && $root.loggedIn && $root.socket.token !== 'autoLogin'" class="logout" @click.prevent="$root.logout">
<div class="menu-item">
<font-awesome-icon icon="sign-out-alt" />
{{ $t("Logout") }}
</div>
</a>
</div> </div>
<div class="settings-content col-lg-9 col-md-7"> <div class="settings-content col-lg-9 col-md-7">
<div v-if="currentPage" class="settings-content-header"> <div v-if="currentPage" class="settings-content-header">
@ -233,4 +241,8 @@ footer {
} }
} }
} }
.logout {
color: $danger !important;
}
</style> </style>

View file

@ -16,11 +16,18 @@
<input id="title" v-model="config.title" type="text" class="form-control"> <input id="title" v-model="config.title" type="text" class="form-control">
</div> </div>
<!-- Description -->
<div class="my-3"> <div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label> <label for="description" class="form-label">{{ $t("Description") }}</label>
<textarea id="description" v-model="config.description" class="form-control"></textarea> <textarea id="description" v-model="config.description" class="form-control"></textarea>
</div> </div>
<!-- Footer Text -->
<div class="my-3">
<label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label>
<textarea id="footer-text" v-model="config.footerText" class="form-control"></textarea>
</div>
<div class="my-3 form-check form-switch"> <div class="my-3 form-check form-switch">
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light"> <input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label> <label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
@ -31,6 +38,12 @@
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label> <label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
</div> </div>
<!-- Show Powered By -->
<div class="my-3 form-check form-switch">
<input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox">
<label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label>
</div>
<div v-if="false" class="my-3"> <div v-if="false" class="my-3">
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label> <label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control"> <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
@ -39,7 +52,7 @@
<!-- Domain Name List --> <!-- Domain Name List -->
<div class="my-3"> <div class="my-3">
<label class="form-label"> <label class="form-label">
Domain Names {{ $t("Domain Names") }}
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" /> <font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
</label> </label>
@ -51,6 +64,12 @@
</ul> </ul>
</div> </div>
<!-- Custom CSS -->
<div class="my-3">
<div class="mb-1">{{ $t("Custom CSS") }}</div>
<prism-editor v-model="config.customCSS" class="css-editor" :highlight="highlighter" line-numbers></prism-editor>
</div>
<div class="danger-zone"> <div class="danger-zone">
<button class="btn btn-danger me-2" @click="deleteDialog"> <button class="btn btn-danger me-2" @click="deleteDialog">
<font-awesome-icon icon="trash" /> <font-awesome-icon icon="trash" />
@ -239,13 +258,24 @@
</div> </div>
<footer class="mt-5 mb-4"> <footer class="mt-5 mb-4">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a> <div class="custom-footer-text text-start">
<strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
</div>
<Editable v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
<p v-if="config.showPoweredBy">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</p>
</footer> </footer>
</div> </div>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage"> <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
{{ $t("deleteStatusPageMsg") }} {{ $t("deleteStatusPageMsg") }}
</Confirm> </Confirm>
<component is="style" v-if="config.customCSS" type="text/css">
{{ config.customCSS }}
</component>
</div> </div>
</template> </template>
@ -259,11 +289,20 @@ import dayjs from "dayjs";
import Favico from "favico.js"; import Favico from "favico.js";
import { getResBaseURL } from "../util-frontend"; import { getResBaseURL } from "../util-frontend";
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
// import Prism Editor
import { PrismEditor } from "vue-prism-editor";
import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere
// import highlighting library (you can use any library you want just return html string)
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-css";
import "prismjs/themes/prism-tomorrow.css"; // import syntax highlighting styles
const toast = useToast(); const toast = useToast();
const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
// eslint-disable-next-line no-unused-vars
let feedInterval; let feedInterval;
const favicon = new Favico({ const favicon = new Favico({
@ -276,6 +315,7 @@ export default {
PublicGroupList, PublicGroupList,
ImageCropUpload, ImageCropUpload,
Confirm, Confirm,
PrismEditor,
}, },
// Leave Page for vue route change // Leave Page for vue route change
@ -418,6 +458,13 @@ export default {
this.$root.getSocket().emit("getStatusPage", this.slug, (res) => { this.$root.getSocket().emit("getStatusPage", this.slug, (res) => {
if (res.ok) { if (res.ok) {
this.config = res.config; this.config = res.config;
if (!this.config.customCSS) {
this.config.customCSS = "body {\n" +
" \n" +
"}\n";
}
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
@ -520,6 +567,10 @@ export default {
}, },
methods: { methods: {
highlighter(code) {
return highlight(code, languages.css);
},
updateHeartbeatList() { updateHeartbeatList() {
// If editMode, it will use the data from websocket. // If editMode, it will use the data from websocket.
if (! this.editMode) { if (! this.editMode) {
@ -656,7 +707,7 @@ export default {
}, },
postIncident() { postIncident() {
if (this.incident.title == "" || this.incident.content == "") { if (this.incident.title === "" || this.incident.content === "") {
toast.error(this.$t("Please input title and content")); toast.error(this.$t("Please input title and content"));
return; return;
} }
@ -893,4 +944,18 @@ footer {
} }
} }
/* required class */
.css-editor {
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
border-radius: 1rem;
padding: 10px 5px;
border: 1px solid #ced4da;
.dark & {
background: $dark-bg;
border: 1px solid $dark-border-color;
}
}
</style> </style>

View file

@ -54,7 +54,39 @@ function debug(msg) {
} }
exports.debug = debug; exports.debug = debug;
class Logger { class Logger {
constructor() {
/**
* UPTIME_KUMA_HIDE_LOG=debug_monitor,info_monitor
*
* Example:
* [
* "debug_monitor", // Hide all logs that level is debug and the module is monitor
* "info_monitor",
* ]
*/
this.hideLog = {
info: [],
warn: [],
error: [],
debug: [],
};
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
let list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (let pair of list) {
// split first "_" only
let values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
}
this.debug("server", "UPTIME_KUMA_HIDE_LOG is set");
this.debug("server", this.hideLog);
}
}
log(module, msg, level) { log(module, msg, level) {
if (this.hideLog[level] && this.hideLog[level].includes(module)) {
return;
}
module = module.toUpperCase(); module = module.toUpperCase();
level = level.toUpperCase(); level = level.toUpperCase();
const now = new Date().toISOString(); const now = new Date().toISOString();

View file

@ -59,7 +59,46 @@ export function debug(msg: any) {
} }
class Logger { class Logger {
/**
* UPTIME_KUMA_HIDE_LOG=debug_monitor,info_monitor
*
* Example:
* [
* "debug_monitor", // Hide all logs that level is debug and the module is monitor
* "info_monitor",
* ]
*/
hideLog : any = {
info: [],
warn: [],
error: [],
debug: [],
};
constructor() {
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
let list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (let pair of list) {
// split first "_" only
let values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
}
this.debug("server", "UPTIME_KUMA_HIDE_LOG is set");
this.debug("server", this.hideLog);
}
}
log(module: string, msg: any, level: string) { log(module: string, msg: any, level: string) {
if (this.hideLog[level] && this.hideLog[level].includes(module)) {
return;
}
module = module.toUpperCase(); module = module.toUpperCase();
level = level.toUpperCase(); level = level.toUpperCase();