Merge branch 'master' into fix-1448-discord-service-url

pull/1471/head
Jordan Bertasso 3 years ago committed by GitHub
commit c5faf709b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -22,39 +22,48 @@ 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", { "vue/multi-word-component-names": "off",
"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 +73,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" ],
} }
}, },

@ -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)

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

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

@ -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
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues). ### Beta Version
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
### Bug Reports / Feature Requests
If you want to report a bug or request a new feature, feel free to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
### Translations
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages If you want to 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

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

@ -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;

@ -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 ]
} }
}, },
}); });

@ -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;

@ -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;

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

@ -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.3 && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install cloudflared # Install cloudflared

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

@ -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);
@ -21,6 +20,10 @@ if (! exists) {
// Process package.json // Process package.json
pkg.version = version; pkg.version = version;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Also update package-lock.json
childProcess.spawnSync("npm", [ "install" ]);
commit(version); commit(version);
tag(version); tag(version);
@ -32,7 +35,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 +43,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 +60,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,
});
}
}

@ -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

@ -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();

@ -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();

@ -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();
@ -26,6 +25,9 @@ if (! exists) {
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`); pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Also update package-lock.json
childProcess.spawnSync("npm", [ "install" ]);
commit(newVersion); commit(newVersion);
tag(newVersion); tag(newVersion);
@ -42,7 +44,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 +54,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 +69,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;
} }

@ -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,
}); });

3444
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.14.0", "version": "1.15.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -10,17 +10,20 @@
"node": "14.* || >=16.*" "node": "14.* || >=16.*"
}, },
"scripts": { "scripts": {
"install-legacy": "npm install --legacy-peer-deps", "install-legacy": "npm install",
"update-legacy": "npm update --legacy-peer-deps", "update-legacy": "npm update",
"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-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --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": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start": "npm run start-server", "start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js", "start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"build": "vite build --config ./config/vite.config.js", "build": "vite build --config ./config/vite.config.js",
"test": "npm run lint && node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test", "test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
"test-with-build": "npm run build && npm test", "test-with-build": "npm run build && npm test",
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend", "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js", "jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
@ -36,7 +39,7 @@
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-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.15.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 +51,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,10 +64,11 @@
"@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.6",
"@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",
"badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"bree": "~7.1.5", "bree": "~7.1.5",
@ -71,6 +76,7 @@
"chart.js": "~3.6.2", "chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"chroma-js": "^2.1.2",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"dayjs": "~1.10.8", "dayjs": "~1.10.8",
@ -85,12 +91,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 +118,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",
@ -117,27 +126,30 @@
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "~5.0.1", "@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.15.8", "@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@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",
"concurrently": "^7.1.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"dns2": "~2.0.1", "dns2": "~2.0.1",
"eslint": "~7.32.0", "eslint": "~8.14.0",
"eslint-plugin-vue": "~7.18.0", "eslint-plugin-vue": "~8.7.1",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.3", "jest-puppeteer": "~6.0.3",
"npm-check-updates": "^12.5.5", "npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1", "postcss-html": "^1.3.1",
"puppeteer": "~13.1.3", "puppeteer": "~13.1.3",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~14.2.0", "stylelint": "~14.7.1",
"stylelint-config-standard": "~24.0.0", "stylelint-config-standard": "~25.0.0",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"vite": "~2.6.14" "vite": "~2.6.14",
"wait-on": "^6.0.1"
} }
} }

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

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

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

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

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

@ -56,7 +56,9 @@ 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,
};
/** /**
* The final version should be 10 after merged tag feature * The final version should be 10 after merged tag feature
@ -66,6 +68,10 @@ class Database {
static noReject = true; static noReject = true;
/**
* Initialize the database
* @param {Object} args Arguments to initialize DB with
*/
static init(args) { static init(args) {
// Data Directory (must be end with "/") // Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
@ -83,6 +89,15 @@ class Database {
log.info("db", `Data Dir: ${Database.dataDir}`); log.info("db", `Data Dir: ${Database.dataDir}`);
} }
/**
* Connect to the database
* @param {boolean} [testMode=false] Should the connection be
* started in test mode?
* @param {boolean} [autoloadModels=true] Should models be
* automatically loaded?
* @param {boolean} [noLog=false] Should logs not be output?
* @returns {Promise<void>}
*/
static async connect(testMode = false, autoloadModels = true, noLog = false) { static async connect(testMode = false, autoloadModels = true, noLog = false) {
const acquireConnectionTimeout = 120 * 1000; const acquireConnectionTimeout = 120 * 1000;
@ -142,6 +157,7 @@ class Database {
} }
} }
/** Patch the database */
static async patch() { static async patch() {
let version = parseInt(await setting("database_version")); let version = parseInt(await setting("database_version"));
@ -187,7 +203,9 @@ class Database {
} }
/** /**
* Patch DB using new process
* Call it from patch() only * Call it from patch() only
* @private
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async patch2() { static async patch2() {
@ -294,9 +312,12 @@ class Database {
} }
/** /**
* Patch database using new patching process
* Used it patch2() only * Used it patch2() only
* @private
* @param sqlFilename * @param sqlFilename
* @param databasePatchedFiles * @param databasePatchedFiles
* @returns {Promise<void>}
*/ */
static async patch2Recursion(sqlFilename, databasePatchedFiles) { static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename]; let value = this.patchList[sqlFilename];
@ -331,12 +352,12 @@ class Database {
} }
/** /**
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself * Load an SQL file and execute it
* @param filename * @param filename Filename of SQL file to import
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async importSQLFile(filename) { static async importSQLFile(filename) {
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
await R.getCell("SELECT 1"); await R.getCell("SELECT 1");
let text = fs.readFileSync(filename).toString(); let text = fs.readFileSync(filename).toString();
@ -364,6 +385,10 @@ class Database {
} }
} }
/**
* Aquire a direct connection to database
* @returns {any}
*/
static getBetterSQLite3Database() { static getBetterSQLite3Database() {
return R.knex.client.acquireConnection(); return R.knex.client.acquireConnection();
} }
@ -399,7 +424,7 @@ class Database {
/** /**
* One backup one time in this process. * One backup one time in this process.
* Reset this.backupPath if you want to backup again * Reset this.backupPath if you want to backup again
* @param version * @param {string} version Version code of backup
*/ */
static backup(version) { static backup(version) {
if (! this.backupPath) { if (! this.backupPath) {
@ -421,9 +446,7 @@ class Database {
} }
} }
/** /** Restore from most recent backup */
*
*/
static restore() { static restore() {
if (this.backupPath) { if (this.backupPath) {
log.error("db", "Patching the database failed!!! Restoring the backup"); log.error("db", "Patching the database failed!!! Restoring the backup");
@ -465,6 +488,7 @@ class Database {
} }
} }
/** Get the size of the database */
static getSize() { static getSize() {
log.debug("db", "Database.getSize()"); log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.path); let stats = fs.statSync(Database.path);
@ -472,6 +496,10 @@ class Database {
return stats.size; return stats.size;
} }
/**
* Shrink the database
* @returns {Promise<void>}
*/
static async shrink() { static async shrink() {
await R.exec("VACUUM"); await R.exec("VACUUM");
} }

@ -8,10 +8,12 @@ const { log } = require("../src/util");
let ImageDataURI = (() => { let ImageDataURI = (() => {
/** /**
* @param {string} dataURI - A string that is a valid Data URI. * Decode the data:image/ URI
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object. * @param {string} dataURI data:image/ URI to decode
* * @returns {?Object} An object with properties "imageType" and "dataBase64".
* Generated by Trelent * The former is the image type, e.g., "png", and the latter is a base64
* encoded string of the image's binary data. If it fails to parse, returns
* null instead of an object.
*/ */
function decode(dataURI) { function decode(dataURI) {
if (!/data:image\//.test(dataURI)) { if (!/data:image\//.test(dataURI)) {
@ -28,11 +30,11 @@ let ImageDataURI = (() => {
} }
/** /**
* @param {Buffer} data - The image data to be encoded. * Endcode an image into data:image/ URI
* @param {String} mediaType - The type of the image, e.g., "image/png". * @param {(Buffer|string)} data Data to encode
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred. * @param {string} mediaType Media type of data
* * @returns {(string|null)} A string representing the base64-encoded
* Generated by Trelent * version of the given Buffer object or null if an error occurred.
*/ */
function encode(data, mediaType) { function encode(data, mediaType) {
if (!data || !mediaType) { if (!data || !mediaType) {
@ -48,11 +50,10 @@ let ImageDataURI = (() => {
} }
/** /**
* Converts a data URI to a file path. * Write data URI to file
* @param {string} dataURI The Data URI of the image. * @param {string} dataURI data:image/ URI
* @param {string} [filePath] The path where the image will be saved, defaults to "./". * @param {string} [filePath] Path to write file to
* * @returns {Promise<string>}
* Generated by Trelent
*/ */
function outputFile(dataURI, filePath) { function outputFile(dataURI, filePath) {
filePath = filePath || "./"; filePath = filePath || "./";

@ -10,6 +10,11 @@ const jobs = [
}, },
]; ];
/**
* Initialize background jobs
* @param {Object} args Arguments to pass to workers
* @returns {Bree}
*/
const initBackgroundJobs = function (args) { const initBackgroundJobs = function (args) {
bree = new Bree({ bree = new Bree({
root: path.resolve("server", "jobs"), root: path.resolve("server", "jobs"),

@ -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}`);

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

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

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

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

@ -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, 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");
@ -15,6 +15,7 @@ const { Proxy } = require("../proxy");
const { demoMode } = require("../config"); const { demoMode } = require("../config");
const version = require("../../package.json").version; const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
/** /**
* status: * status:
@ -27,6 +28,7 @@ class Monitor extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
* @returns {Object}
*/ */
async toPublicJSON(showTags = false) { async toPublicJSON(showTags = false) {
let obj = { let obj = {
@ -41,8 +43,9 @@ class Monitor extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @returns {Object}
*/ */
async toJSON() { async toJSON(includeSensitiveData = true) {
let notificationIDList = {}; let notificationIDList = {};
@ -56,15 +59,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 +81,35 @@ 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;
} }
/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>}
*/
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 ]);
} }
/** /**
@ -102,6 +121,10 @@ class Monitor extends BeanModel {
return Buffer.from(user + ":" + pass).toString("base64"); return Buffer.from(user + ":" + pass).toString("base64");
} }
/**
* Is the TLS expiry notification enabled?
* @returns {boolean}
*/
isEnabledExpiryNotification() { isEnabledExpiryNotification() {
return Boolean(this.expiryNotification); return Boolean(this.expiryNotification);
} }
@ -122,10 +145,18 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown); return Boolean(this.upsideDown);
} }
/**
* Get accepted status codes
* @returns {Object}
*/
getAcceptedStatuscodes() { getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json); return JSON.parse(this.accepted_statuscodes_json);
} }
/**
* Start monitor
* @param {Server} io Socket server instance
*/
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
let retries = 0; let retries = 0;
@ -151,7 +182,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 +200,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 +293,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 +333,24 @@ class Monitor extends BeanModel {
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type); let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, 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 +405,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 +427,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;
@ -478,12 +516,13 @@ class Monitor extends BeanModel {
}; };
/** Get a heartbeat and handle errors */
const safeBeat = async () => { const safeBeat = async () => {
try { try {
await beat(); await beat();
} catch (e) { } catch (e) {
console.trace(e); console.trace(e);
errorLog(e, false); UptimeKumaServer.errorLog(e, false);
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues"); log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
if (! this.isStop) { if (! this.isStop) {
@ -503,6 +542,7 @@ class Monitor extends BeanModel {
} }
} }
/** Stop monitor */
stop() { stop() {
clearTimeout(this.heartbeatInterval); clearTimeout(this.heartbeatInterval);
this.isStop = true; this.isStop = true;
@ -510,6 +550,10 @@ class Monitor extends BeanModel {
this.prometheus().remove(); this.prometheus().remove();
} }
/**
* Get a new prometheus instance
* @returns {Prometheus}
*/
prometheus() { prometheus() {
return new Prometheus(this); return new Prometheus(this);
} }
@ -518,7 +562,7 @@ class Monitor extends BeanModel {
* Helper Method: * Helper Method:
* returns URL object for further usage * returns URL object for further usage
* returns null if url is invalid * returns null if url is invalid
* @returns {null|URL} * @returns {(null|URL)}
*/ */
getUrl() { getUrl() {
try { try {
@ -531,7 +575,7 @@ class Monitor extends BeanModel {
/** /**
* Store TLS info to database * Store TLS info to database
* @param checkCertificateResult * @param checkCertificateResult
* @returns {Promise<object>} * @returns {Promise<Object>}
*/ */
async updateTlsInfo(checkCertificateResult) { async updateTlsInfo(checkCertificateResult) {
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@ -573,6 +617,12 @@ class Monitor extends BeanModel {
return checkCertificateResult; return checkCertificateResult;
} }
/**
* Send statistics to clients
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
*/
static async sendStats(io, monitorID, userID) { static async sendStats(io, monitorID, userID) {
const hasClients = getTotalClientInRoom(io, userID) > 0; const hasClients = getTotalClientInRoom(io, userID) > 0;
@ -587,8 +637,8 @@ class Monitor extends BeanModel {
} }
/** /**
* * Send the average ping to user
* @param duration : int Hours * @param {number} duration Hours
*/ */
static async sendAvgPing(duration, io, monitorID, userID) { static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
@ -608,12 +658,18 @@ class Monitor extends BeanModel {
io.to(userID).emit("avgPing", monitorID, avgPing); io.to(userID).emit("avgPing", monitorID, avgPing);
} }
/**
* Send certificate information to client
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
*/
static async sendCertInfo(io, monitorID, userID) { 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);
} }
} }
@ -621,7 +677,8 @@ class Monitor extends BeanModel {
* Uptime with calculation * Uptime with calculation
* Calculation based on: * Calculation based on:
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime * https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
* @param duration : int Hours * @param {number} duration Hours
* @param {number} monitorID ID of monitor to calculate
*/ */
static async calcUptime(duration, monitorID) { static async calcUptime(duration, monitorID) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
@ -687,13 +744,23 @@ class Monitor extends BeanModel {
/** /**
* Send Uptime * Send Uptime
* @param duration : int Hours * @param {number} duration Hours
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
*/ */
static async sendUptime(duration, io, monitorID, userID) { static async sendUptime(duration, io, monitorID, userID) {
const uptime = await this.calcUptime(duration, monitorID); const uptime = await this.calcUptime(duration, monitorID);
io.to(userID).emit("uptime", monitorID, duration, uptime); io.to(userID).emit("uptime", monitorID, duration, uptime);
} }
/**
* Has status of monitor changed since last beat?
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
* @param {const} previousBeatStatus Status of the previous beat
* @param {const} currentBeatStatus Status of the current beat
* @returns {boolean} True if is an important beat else false
*/
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) { static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat] // * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important // UP -> PENDING = not important
@ -712,6 +779,12 @@ class Monitor extends BeanModel {
return isImportant; return isImportant;
} }
/**
* Send a notification about a monitor
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
* @param {Monitor} monitor The monitor to send a notificaton about
* @param {Bean} bean Status information about monitor
*/
static async sendNotification(isFirstBeat, monitor, bean) { static async sendNotification(isFirstBeat, monitor, bean) {
if (!isFirstBeat || bean.status === DOWN) { if (!isFirstBeat || bean.status === DOWN) {
const notificationList = await Monitor.getNotificationList(monitor); const notificationList = await Monitor.getNotificationList(monitor);
@ -727,7 +800,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);
@ -736,6 +809,11 @@ class Monitor extends BeanModel {
} }
} }
/**
* Get list of notification providers for a given monitor
* @param {Monitor} monitor Monitor to get notification providers for
* @returns {Promise<LooseObject<any>[]>}
*/
static async getNotificationList(monitor) { static async getNotificationList(monitor) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
monitor.id, monitor.id,
@ -743,6 +821,10 @@ class Monitor extends BeanModel {
return notificationList; return notificationList;
} }
/**
* Send notification about a certificate
* @param {Object} tlsInfoObject Information about certificate
*/
async sendCertNotification(tlsInfoObject) { async sendCertNotification(tlsInfoObject) {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this); const notificationList = await Monitor.getNotificationList(this);
@ -754,6 +836,14 @@ class Monitor extends BeanModel {
} }
} }
/**
* Send a certificate notification when certificate expires in less
* than target days
* @param {number} daysRemaining Number of days remaining on certifcate
* @param {number} targetDays Number of days to alert after
* @param {LooseObject<any>[]} notificationList List of notification providers
* @returns {Promise<void>}
*/
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) { async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
if (daysRemaining > targetDays) { if (daysRemaining > targetDays) {
@ -801,6 +891,11 @@ class Monitor extends BeanModel {
} }
} }
/**
* Get the status of the previous heartbeat
* @param {number} monitorID ID of monitor to check
* @returns {Promise<LooseObject<any>>}
*/
static async getPreviousHeartbeat(monitorID) { static async getPreviousHeartbeat(monitorID) {
return await R.getRow(` return await R.getRow(`
SELECT status, time FROM heartbeat SELECT status, time FROM heartbeat

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

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

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

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

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

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

@ -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);

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

@ -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";

@ -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);
} }
@ -44,7 +44,12 @@ class Bark extends NotificationProvider {
} }
} }
// add additional parameter for better on device styles (iOS 15 optimized) /**
* Add additional parameter for better on device styles (iOS 15
* optimized)
* @param {string} postUrl URL to append parameters to
* @returns {string}
*/
appendAdditionalParameters(postUrl) { appendAdditionalParameters(postUrl) {
// grouping all our notifications // grouping all our notifications
postUrl += "?group=" + barkNotificationGroup; postUrl += "?group=" + barkNotificationGroup;
@ -55,7 +60,11 @@ class Bark extends NotificationProvider {
return postUrl; return postUrl;
} }
// thrown if failed to check result, result code should be in range 2xx /**
* Check if result is successful
* @param {Object} result Axios response object
* @throws {Error} The status code is not in range 2xx
*/
checkResult(result) { checkResult(result) {
if (result.status == null) { if (result.status == null) {
throw new Error("Bark notification failed with invalid response!"); throw new Error("Bark notification failed with invalid response!");
@ -65,6 +74,13 @@ class Bark extends NotificationProvider {
} }
} }
/**
* Send the message
* @param {string} title Message title
* @param {string} subtitle Message
* @param {string} endpoint Endpoint to send request to
* @returns {string}
*/
async postNotification(title, subtitle, endpoint) { async postNotification(title, subtitle, endpoint) {
// url encode title and subtitle // url encode title and subtitle
title = encodeURIComponent(title); title = encodeURIComponent(title);

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

@ -40,7 +40,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: [{
@ -75,7 +75,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: [{

@ -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: {

@ -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

@ -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": [

@ -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"],

@ -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",

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

@ -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, ""),

@ -27,7 +27,7 @@ class OneBot extends NotificationProvider {
"auto_escape": true, "auto_escape": true,
"message": pushText, "message": pushText,
}; };
if (notification.msgType == "group") { if (notification.msgType === "group") {
data["message_type"] = "group"; data["message_type"] = "group";
data["group_id"] = notification.recieverId; data["group_id"] = notification.recieverId;
} else { } else {

@ -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"],

@ -13,9 +13,9 @@ class PushDeer extends NotificationProvider {
let valid = msg != null && monitorJSON != null && heartbeatJSON != null; let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
let title; let title;
if (valid && heartbeatJSON.status == UP) { if (valid && heartbeatJSON.status === UP) {
title = "## Uptime Kuma: " + monitorJSON.name + " up"; title = "## Uptime Kuma: " + monitorJSON.name + " up";
} else if (valid && heartbeatJSON.status == DOWN) { } else if (valid && heartbeatJSON.status === DOWN) {
title = "## Uptime Kuma: " + monitorJSON.name + " down"; title = "## Uptime Kuma: " + monitorJSON.name + " down";
} else { } else {
title = "## Uptime Kuma Message"; title = "## Uptime Kuma Message";
@ -38,7 +38,7 @@ class PushDeer extends NotificationProvider {
if (res.data.content.result.length === 0) { if (res.data.content.result.length === 0) {
let error = "Invalid PushDeer key"; let error = "Invalid PushDeer key";
this.throwGeneralAxiosError(error); this.throwGeneralAxiosError(error);
} else if (JSON.parse(res.data.content.result[0]).success != "ok") { } else if (JSON.parse(res.data.content.result[0]).success !== "ok") {
let error = "Unknown error"; let error = "Unknown error";
this.throwGeneralAxiosError(error); this.throwGeneralAxiosError(error);
} }

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

@ -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 {

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

@ -24,12 +24,18 @@ class WeCom extends NotificationProvider {
} }
} }
/**
* Generate the message to send
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @param {string} msg General message
* @returns {Object}
*/
composeMessage(heartbeatJSON, msg) { 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) {

@ -38,6 +38,7 @@ class Notification {
providerList = {}; providerList = {};
/** Initialize the notification providers */
static init() { static init() {
log.info("notification", "Prepare Notification Providers"); log.info("notification", "Prepare Notification Providers");
@ -92,13 +93,13 @@ class Notification {
} }
/** /**
* * Send a notification
* @param notification : BeanModel * @param {BeanModel} notification
* @param msg : string General Message * @param {string} msg General Message
* @param monitorJSON : object Monitor details (For Up/Down only) * @param {Object} monitorJSON Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only) * @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Successful msg * @returns {Promise<string>} Successful msg
* Throw Error with fail msg * @throws Error with fail msg
*/ */
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
if (this.providerList[notification.type]) { if (this.providerList[notification.type]) {
@ -108,6 +109,13 @@ class Notification {
} }
} }
/**
* Save a notification
* @param {Object} notification Notification to save
* @param {?number} notificationID ID of notification to update
* @param {number} userID ID of user who adds notification
* @returns {Promise<Bean>}
*/
static async save(notification, notificationID, userID) { static async save(notification, notificationID, userID) {
let bean; let bean;
@ -138,6 +146,12 @@ class Notification {
return bean; return bean;
} }
/**
* Delete a notification
* @param {number} notificationID ID of notification to delete
* @param {number} userID ID of user who created notification
* @returns {Promise<void>}
*/
static async delete(notificationID, userID) { static async delete(notificationID, userID) {
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID, notificationID,
@ -151,6 +165,10 @@ class Notification {
await R.trash(bean); await R.trash(bean);
} }
/**
* Check if apprise exists
* @returns {boolean} Does the command apprise exist?
*/
static checkApprise() { static checkApprise() {
let commandExistsSync = require("command-exists").sync; let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync("apprise"); let exists = commandExistsSync("apprise");
@ -160,11 +178,10 @@ class Notification {
} }
/** /**
* Adds a new monitor to the database. * Apply the notification to every monitor
* @param {number} userID The ID of the user that owns this monitor. * @param {number} notificationID ID of notification to apply
* @param {string} name The name of this monitor. * @param {number} userID ID of user who created notification
* * @returns {Promise<void>}
* Generated by Trelent
*/ */
async function applyNotificationEveryMonitor(notificationID, userID) { async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [ let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [

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

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

@ -9,32 +9,35 @@ 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
}); });
class Prometheus { class Prometheus {
monitorLabelValues = {} monitorLabelValues = {};
/**
* @param {Object} monitor Monitor object to monitor
*/
constructor(monitor) { constructor(monitor) {
this.monitorLabelValues = { this.monitorLabelValues = {
monitor_name: monitor.name, monitor_name: monitor.name,
@ -45,17 +48,22 @@ class Prometheus {
}; };
} }
/**
* Update the metrics page
* @param {Object} heartbeat Heartbeat details
* @param {Object} tlsInfo TLS details
*/
update(heartbeat, tlsInfo) { update(heartbeat, tlsInfo) {
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 +71,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 +80,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 +88,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 +101,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);
} }

@ -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 ]);
} }
} }
} }

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

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

@ -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,32 +51,16 @@ 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");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
log.debug("server", "Importing Settings"); log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
log.debug("server", "Importing Notification"); log.debug("server", "Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -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");
@ -173,12 +136,6 @@ app.use(function (req, res, next) {
next(); next();
}); });
/**
* Total WebSocket client connected to server currently, no actual use
* @type {number}
*/
let totalClient = 0;
/** /**
* Use for decode the auth object * Use for decode the auth object
* @type {null} * @type {null}
@ -234,6 +191,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:";
@ -277,17 +241,11 @@ try {
sendInfo(socket); sendInfo(socket);
totalClient++;
if (needSetup) { if (needSetup) {
log.info("server", "Redirect to setup page"); log.info("server", "Redirect to setup page");
socket.emit("setup"); socket.emit("setup");
} }
socket.on("disconnect", () => {
totalClient--;
});
// *************************** // ***************************
// Public Socket API // Public Socket API
// *************************** // ***************************
@ -356,7 +314,7 @@ try {
let user = await login(data.username, data.password); let user = await login(data.username, data.password);
if (user) { if (user) {
if (user.twofa_status == 0) { if (user.twofa_status === 0) {
afterLogin(socket, user); afterLogin(socket, user);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`); log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
@ -369,7 +327,7 @@ try {
}); });
} }
if (user.twofa_status == 1 && !data.token) { if (user.twofa_status === 1 && !data.token) {
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`); log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
@ -379,7 +337,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);
@ -446,7 +404,7 @@ try {
socket.userID, socket.userID,
]); ]);
if (user.twofa_status == 0) { if (user.twofa_status === 0) {
let newSecret = genSecret(); let newSecret = genSecret();
let encodedSecret = base32.encode(newSecret); let encodedSecret = base32.encode(newSecret);
@ -546,7 +504,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({
@ -577,7 +535,7 @@ try {
socket.userID, socket.userID,
]); ]);
if (user.twofa_status == 1) { if (user.twofa_status === 1) {
callback({ callback({
ok: true, ok: true,
status: true, status: true,
@ -712,6 +670,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);
@ -1085,7 +1047,13 @@ try {
try { try {
checkLogin(socket); checkLogin(socket);
if (data.disableAuth) { // If currently is disabled auth, don't need to check
// Disabled Auth + Want to Disable Auth => No Check
// Disabled Auth + Want to Enable Auth => No Check
// Enabled Auth + Want to Disable Auth => Check!!
// Enabled Auth + Want to Enable Auth => No Check
const currentDisabledAuth = await setting("disableAuth");
if (!currentDisabledAuth && data.disableAuth) {
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket, currentPassword);
} }
@ -1194,7 +1162,7 @@ try {
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">="); let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user" // If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
if (importHandle == "overwrite") { if (importHandle === "overwrite") {
// Stops every monitor first, so it doesn't execute any heartbeat while importing // Stops every monitor first, so it doesn't execute any heartbeat while importing
for (let id in server.monitorList) { for (let id in server.monitorList) {
let monitor = server.monitorList[id]; let monitor = server.monitorList[id];
@ -1218,7 +1186,7 @@ try {
for (let i = 0; i < notificationListData.length; i++) { for (let i = 0; i < notificationListData.length; i++) {
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists // Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") { if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
let notification = JSON.parse(notificationListData[i].config); let notification = JSON.parse(notificationListData[i].config);
await Notification.save(notification, null, socket.userID); await Notification.save(notification, null, socket.userID);
@ -1236,7 +1204,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;
} }
@ -1253,7 +1221,7 @@ try {
for (let i = 0; i < monitorListData.length; i++) { for (let i = 0; i < monitorListData.length; i++) {
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists // Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
if ((importHandle == "skip" && monitorNameListString.includes(monitorListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") { if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0) // Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
// --- Start --- // --- Start ---
@ -1350,7 +1318,7 @@ try {
await updateMonitorNotification(bean.id, notificationIDList); await updateMonitorNotification(bean.id, notificationIDList);
// If monitor was active start it immediately, otherwise pause it // If monitor was active start it immediately, otherwise pause it
if (monitorListData[i].active == 1) { if (monitorListData[i].active === 1) {
await startMonitor(socket.userID, bean.id); await startMonitor(socket.userID, bean.id);
} else { } else {
await pauseMonitor(socket.userID, bean.id); await pauseMonitor(socket.userID, bean.id);
@ -1471,12 +1439,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 {
@ -1498,11 +1466,11 @@ try {
})(); })();
/** /**
* Adds or removes notifications from a monitor. * Update notifications for a given monitor
* @param {number} monitorID The ID of the monitor to add/remove notifications from. * @param {number} monitorID ID of monitor to update
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove. * @param {number[]} notificationIDList List of new notification
* * providers to add
* Generated by Trelent * @returns {Promise<void>}
*/ */
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
@ -1520,11 +1488,11 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
} }
/** /**
* This function checks if the user owns a monitor with the given ID. * Check if a given user owns a specific monitor
* @param {number} monitorID - The ID of the monitor to check ownership for. * @param {number} userID
* @param {number} userID - The ID of the user who is trying to access this data. * @param {number} monitorID
* * @returns {Promise<void>}
* Generated by Trelent * @throws {Error} The specified user does not own the monitor
*/ */
async function checkOwner(userID, monitorID) { async function checkOwner(userID, monitorID) {
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [ let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
@ -1538,8 +1506,11 @@ async function checkOwner(userID, monitorID) {
} }
/** /**
* Function called after user login
* This function is used to send the heartbeat list of a monitor. * This function is used to send the heartbeat list of a monitor.
* @param {Socket} socket - The socket object that will be used to send the data. * @param {Socket} socket Socket.io instance
* @param {Object} user User object
* @returns {Promise<void>}
*/ */
async function afterLogin(socket, user) { async function afterLogin(socket, user) {
socket.userID = user.id; socket.userID = user.id;
@ -1567,30 +1538,10 @@ async function afterLogin(socket, user) {
} }
/** /**
* Get a list of monitors for the given user. * Initialize the database
* @param {string} userID - The ID of the user to get monitors for. * @param {boolean} [testMode=false] Should the connection be
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values. * started in test mode?
* * @returns {Promise<void>}
* Generated by Trelent
*/
async function getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
return result;
}
/**
* Connect to the database and patch it if necessary.
*
* Generated by Trelent
*/ */
async function initDatabase(testMode = false) { async function initDatabase(testMode = false) {
if (! fs.existsSync(Database.path)) { if (! fs.existsSync(Database.path)) {
@ -1627,11 +1578,10 @@ async function initDatabase(testMode = false) {
} }
/** /**
* Resume a monitor. * Start the specified monitor
* @param {string} userID - The ID of the user who owns the monitor. * @param {number} userID ID of user who owns monitor
* @param {string} monitorID - The ID of the monitor to resume. * @param {number} monitorID ID of monitor to start
* * @returns {Promise<void>}
* Generated by Trelent
*/ */
async function startMonitor(userID, monitorID) { async function startMonitor(userID, monitorID) {
await checkOwner(userID, monitorID); await checkOwner(userID, monitorID);
@ -1655,16 +1605,21 @@ async function startMonitor(userID, monitorID) {
monitor.start(io); monitor.start(io);
} }
/**
* Restart a given monitor
* @param {number} userID ID of user who owns monitor
* @param {number} monitorID ID of monitor to start
* @returns {Promise<void>}
*/
async function restartMonitor(userID, monitorID) { async function restartMonitor(userID, monitorID) {
return await startMonitor(userID, monitorID); return await startMonitor(userID, monitorID);
} }
/** /**
* Pause a monitor. * Pause a given monitor
* @param {string} userID - The ID of the user who owns the monitor. * @param {number} userID ID of user who owns monitor
* @param {string} monitorID - The ID of the monitor to pause. * @param {number} monitorID ID of monitor to start
* * @returns {Promise<void>}
* Generated by Trelent
*/ */
async function pauseMonitor(userID, monitorID) { async function pauseMonitor(userID, monitorID) {
await checkOwner(userID, monitorID); await checkOwner(userID, monitorID);
@ -1681,9 +1636,7 @@ async function pauseMonitor(userID, monitorID) {
} }
} }
/** /** Resume active monitors */
* Resume active monitors
*/
async function startMonitors() { async function startMonitors() {
let list = await R.find("monitor", " active = 1 "); let list = await R.find("monitor", " active = 1 ");
@ -1699,10 +1652,10 @@ async function startMonitors() {
} }
/** /**
* Shutdown the application
* Stops all monitors and closes the database connection. * Stops all monitors and closes the database connection.
* @param {string} signal The signal that triggered this function to be called. * @param {string} signal The signal that triggered this function to be called.
* * @returns {Promise<void>}
* Generated by Trelent
*/ */
async function shutdownFunction(signal) { async function shutdownFunction(signal) {
log.info("server", "Shutdown requested"); log.info("server", "Shutdown requested");
@ -1724,11 +1677,12 @@ function getClientIp(socket) {
return socket.client.conn.remoteAddress.replace(/^.*:/, ""); return socket.client.conn.remoteAddress.replace(/^.*:/, "");
} }
/** Final function called before application exits */
function finalFunction() { 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
@ -1740,6 +1694,6 @@ gracefulShutdown(httpServer, {
// Catch unexpected errors here // Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => { process.addListener("unhandledRejection", (error, promise) => {
console.trace(error); console.trace(error);
errorLog(error, false); UptimeKumaServer.errorLog(error, false);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues"); console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
}); });

@ -1,19 +1,33 @@
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();
/**
* Change running state
* @param {string} running Is it running?
* @param {string} message Message to pass
*/
cloudflared.change = (running, message) => { cloudflared.change = (running, message) => {
io.to("cloudflared").emit(prefix + "running", running); io.to("cloudflared").emit(prefix + "running", running);
io.to("cloudflared").emit(prefix + "message", message); io.to("cloudflared").emit(prefix + "message", message);
}; };
/**
* Emit an error message
* @param {string} errorMessage
*/
cloudflared.error = (errorMessage) => { cloudflared.error = (errorMessage) => {
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage); io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
}; };
/**
* Handler for cloudflared
* @param {Socket} socket Socket.io instance
*/
module.exports.cloudflaredSocketHandler = (socket) => { module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "join", async () => { socket.on(prefix + "join", async () => {
@ -68,6 +82,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
}; };
/**
* Automatically start cloudflared
* @param {string} token Cloudflared tunnel token
*/
module.exports.autoStart = async (token) => { module.exports.autoStart = async (token) => {
if (!token) { if (!token) {
token = await setting("cloudflaredTunnelToken"); token = await setting("cloudflaredTunnelToken");
@ -84,7 +102,10 @@ module.exports.autoStart = async (token) => {
} }
}; };
/** Stop cloudflared */
module.exports.stop = async () => { module.exports.stop = async () => {
console.log("Stop cloudflared"); console.log("Stop cloudflared");
cloudflared.stop(); if (cloudflared) {
cloudflared.stop();
}
}; };

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

@ -1,8 +1,13 @@
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();
/**
* Handlers for proxy
* @param {Socket} socket Socket.io instance
*/
module.exports.proxySocketHandler = (socket) => { module.exports.proxySocketHandler = (socket) => {
socket.on("addProxy", async (proxy, proxyID, callback) => { socket.on("addProxy", async (proxy, proxyID, callback) => {
try { try {

@ -6,8 +6,12 @@ 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");
/**
* Socket handlers for status page
* @param {Socket} socket Socket.io instance to add listeners on
*/
module.exports.statusPageSocketHandler = (socket) => { module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident // Post or edit incident
@ -155,6 +159,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 +219,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 +290,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);
@ -331,6 +342,7 @@ module.exports.statusPageSocketHandler = (socket) => {
/** /**
* Check slug a-z, 0-9, - only * Check slug a-z, 0-9, - only
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug * Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
* @param {string} slug Slug to test
*/ */
function checkSlug(slug) { function checkSlug(slug) {
if (typeof slug !== "string") { if (typeof slug !== "string") {

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

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

@ -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";

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

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

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

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

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

@ -47,8 +47,8 @@
<script> <script>
import HeartbeatBar from "../components/HeartbeatBar.vue"; import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
import Tag from "../components/Tag.vue"; import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
export default { export default {
@ -105,7 +105,7 @@ export default {
// Simple filter by search text // Simple filter by search text
// finds monitor name, tag name or tag value // finds monitor name, tag name or tag value
if (this.searchText != "") { if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase(); const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => { result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText) return monitor.name.toLowerCase().includes(loweredSearchText)
@ -170,12 +170,6 @@ export default {
} }
} }
.dark {
.footer {
// background-color: $dark-bg;
}
}
@media (max-width: 770px) { @media (max-width: 770px) {
.list-header { .list-header {
margin: -20px; margin: -20px;

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

@ -18,13 +18,13 @@
<script lang="ts"> <script lang="ts">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js"; import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import "chartjs-adapter-dayjs"; import utc from "dayjs/plugin/utc";
import { LineChart } from "vue-chart-3"; import { LineChart } from "vue-chart-3";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import { DOWN } from "../util.ts"; import { DOWN, log } from "../util.ts";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -217,9 +217,11 @@ export default {
watch: { watch: {
// Update chart data when the selected chart period changes // Update chart data when the selected chart period changes
chartPeriodHrs: function (newPeriod) { chartPeriodHrs: function (newPeriod) {
// eslint-disable-next-line eqeqeq
if (newPeriod == "0") { if (newPeriod == "0") {
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 +230,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;
}); });
@ -239,7 +242,11 @@ export default {
// And mirror latest change to this.heartbeatList // And mirror latest change to this.heartbeatList
this.$watch(() => this.$root.heartbeatList[this.monitorId], this.$watch(() => this.$root.heartbeatList[this.monitorId],
(heartbeatList) => { (heartbeatList) => {
if (this.chartPeriodHrs != 0) {
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
// eslint-disable-next-line eqeqeq
if (this.chartPeriodHrs != "0") {
const newBeat = heartbeatList.at(-1); const newBeat = heartbeatList.at(-1);
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) { if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
this.heartbeatList.push(heartbeatList.at(-1)); this.heartbeatList.push(heartbeatList.at(-1));
@ -248,6 +255,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>

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

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

@ -1,13 +1,14 @@
<template> <template>
<div class="tag-wrapper rounded d-inline-flex" <div
:class="{ 'px-3': size == 'normal', class="tag-wrapper rounded d-inline-flex"
'py-1': size == 'normal', :class="{ 'px-3': size == 'normal',
'm-2': size == 'normal', 'py-1': size == 'normal',
'px-2': size == 'sm', 'm-2': size == 'normal',
'py-0': size == 'sm', 'px-2': size == 'sm',
'm-1': size == 'sm', 'py-0': size == 'sm',
}" 'm-1': size == 'sm',
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }" }"
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
> >
<span class="tag-text">{{ displayText }}</span> <span class="tag-text">{{ displayText }}</span>
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)"> <span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
@ -34,7 +35,7 @@ export default {
}, },
computed: { computed: {
displayText() { displayText() {
if (this.item.value == "") { if (this.item.value === "") {
return this.item.name; return this.item.name;
} else { } else {
return `${this.item.name}: ${this.item.value}`; return `${this.item.name}: ${this.item.value}`;

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

@ -5,8 +5,14 @@
<script> <script>
export default { export default {
props: { props: {
monitor: Object, monitor: {
type: String, type: Object,
default: null,
},
type: {
type: String,
default: null,
},
pill: { pill: {
type: Boolean, type: Boolean,
default: false, default: false,

@ -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);
}, },

@ -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

@ -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 };

@ -12,15 +12,15 @@ export default {
keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра", keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра",
pauseDashboardHome: "Пауза", pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?", deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?", deleteNotificationMsg: "Наистина ли желаете да изтриете това известие за всички монитори?",
resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.", resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате", rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?", pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.", enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?", clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?", clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?", confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известяване със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известяване.", importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.",
confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.", confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи", twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.", tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
@ -76,9 +76,9 @@ export default {
"Max. Redirects": "Макс. брой пренасочвания", "Max. Redirects": "Макс. брой пренасочвания",
"Accepted Status Codes": "Допустими статус кодове", "Accepted Status Codes": "Допустими статус кодове",
Save: "Запази", Save: "Запази",
Notifications: "Известявания", Notifications: "Известия",
"Not available, please setup.": "Не са налични. Моля, настройте.", "Not available, please setup.": "Не са налични. Моля, настройте.",
"Setup Notification": "Настройки за известявания", "Setup Notification": "Настрой известие",
Light: "Светла", Light: "Светла",
Dark: "Тъмна", Dark: "Тъмна",
Auto: "Автоматично", Auto: "Автоматично",
@ -109,7 +109,7 @@ export default {
Login: "Вход", Login: "Вход",
"No Monitors, please": "Все още няма монитори. Моля, добавете поне ", "No Monitors, please": "Все още няма монитори. Моля, добавете поне ",
"add one": "един.", "add one": "един.",
"Notification Type": "Тип известяване", "Notification Type": "Тип известие",
Email: "Имейл", Email: "Имейл",
Test: "Тест", Test: "Тест",
"Certificate Info": "Информация за сертификат", "Certificate Info": "Информация за сертификат",
@ -131,9 +131,9 @@ export default {
Events: "Събития", Events: "Събития",
Heartbeats: "Проверки", Heartbeats: "Проверки",
"Auto Get": "Авт. попълване", "Auto Get": "Авт. попълване",
backupDescription: "Можете да архивирате всички монитори и всички известявания в JSON файл.", backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
backupDescription2: "PS: Имайте предвид, че данните за история и събития няма да бъдат включени.", backupDescription2: "PS: Имайте предвид, че данните за история и събития няма да бъдат включени.",
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.", backupDescription3: "Чувствителни данни, като токен кодове за известия, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
alertNoFile: "Моля, изберете файл за импортиране.", alertNoFile: "Моля, изберете файл за импортиране.",
alertWrongFileType: "Моля, изберете JSON файл.", alertWrongFileType: "Моля, изберете JSON файл.",
"Clear all statistics": "Изтрий цялата статистика", "Clear all statistics": "Изтрий цялата статистика",
@ -197,12 +197,12 @@ 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} секунди",
pushOptionalParams: "Допълнителни, но не задължителни параметри: {0}", pushOptionalParams: "Допълнителни, но не задължителни параметри: {0}",
defaultNotificationName: "Моето {notification} известяване ({number})", defaultNotificationName: "Моето {notification} известие ({number})",
here: "тук", here: "тук",
Required: "Задължително поле", Required: "Задължително поле",
"Bot Token": "Бот токен", "Bot Token": "Бот токен",
@ -252,7 +252,7 @@ export default {
"Notification Sound": "Звуков сигнал", "Notification Sound": "Звуков сигнал",
"More info on:": "Повече информация на: {0}", "More info on:": "Повече информация на: {0}",
pushoverDesc1: "Приоритет Спешно (2) по подразбиране изчаква 30 секунди между повторните опити и изтича след 1 час.", pushoverDesc1: "Приоритет Спешно (2) по подразбиране изчаква 30 секунди между повторните опити и изтича след 1 час.",
pushoverDesc2: "Ако желаете да изпратите известявания до различни устройства, попълнете полето Устройство.", pushoverDesc2: "Ако желаете да изпратите известия до различни устройства, попълнете полето Устройство.",
"SMS Type": "SMS тип", "SMS Type": "SMS тип",
octopushTypePremium: "Премиум (Бърз - препоръчителен в случай на тревога)", octopushTypePremium: "Премиум (Бърз - препоръчителен в случай на тревога)",
octopushTypeLowCost: "Евтин (Бавен - понякога бива блокиран от оператора)", octopushTypeLowCost: "Евтин (Бавен - понякога бива блокиран от оператора)",
@ -275,7 +275,7 @@ export default {
lineDevConsoleTo: "Line - Конзола за разработчици - {0}", lineDevConsoleTo: "Line - Конзола за разработчици - {0}",
"Basic Settings": "Основни настройки", "Basic Settings": "Основни настройки",
"User ID": "Потребител ID", "User ID": "Потребител ID",
"Messaging API": "API за известяване", "Messaging API": "API за съобщаване",
wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.", wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
"Icon URL": "URL адрес за иконка", "Icon URL": "URL адрес за иконка",
aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.", aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
@ -291,7 +291,7 @@ export default {
matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)", matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)",
"Internal Room Id": "ID на вътрешна стая", "Internal Room Id": "ID на вътрешна стая",
matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.", matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известяванията. Токен код за достъп ще получите изпълнявайки {0}", matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известията. Токен код за достъп ще получите изпълнявайки {0}",
Method: "Метод", Method: "Метод",
Body: "Съобщение", Body: "Съобщение",
Headers: "Хедъри", Headers: "Хедъри",
@ -353,8 +353,8 @@ export default {
serwersmsSenderName: "SMS Подател име (регистриран през клиентския портал)", serwersmsSenderName: "SMS Подател име (регистриран през клиентския портал)",
stackfield: "Stackfield", stackfield: "Stackfield",
smtpDkimSettings: "DKIM Настройки", smtpDkimSettings: "DKIM Настройки",
smtpDkimDesc: "Моля, вижте Nodemailer DKIM {0} за инструкции.", smtpDkimDesc: "Моля, вижте {0} на Nodemailer DKIM за инструкции.",
documentation: "документация", documentation: "документацията",
smtpDkimDomain: "Домейн", smtpDkimDomain: "Домейн",
smtpDkimKeySelector: "Селектор на ключ", smtpDkimKeySelector: "Селектор на ключ",
smtpDkimPrivateKey: "Частен ключ", smtpDkimPrivateKey: "Частен ключ",
@ -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: "Удостоверяването е изключено.",
}; };

@ -179,7 +179,7 @@ export default {
"Edit Status Page": "Bearbeite Status-Seite", "Edit Status Page": "Bearbeite Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard", "Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite", "Status Page": "Status-Seite",
"Status Pages": "Status-Seite", "Status Pages": "Status-Seiten",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "E-Mail (SMTP)", smtp: "E-Mail (SMTP)",
@ -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",
@ -403,8 +403,8 @@ export default {
"WeCom Bot Key": "WeCom Bot Schlüssel", "WeCom Bot Key": "WeCom Bot Schlüssel",
"Setup Proxy": "Proxy einrichten", "Setup Proxy": "Proxy einrichten",
"Proxy Protocol": "Proxy Protokoll", "Proxy Protocol": "Proxy Protokoll",
"Proxy Server": "Proxy Server", "Proxy Server": "Proxy-Server",
"Proxy server has authentication": "Proxy server hat Authentifizierung", "Proxy server has authentication": "Proxy-Server hat Authentifizierung",
User: "Benutzer", User: "Benutzer",
Installed: "Installiert", Installed: "Installiert",
"Not installed": "Nicht installiert", "Not installed": "Nicht installiert",
@ -442,4 +442,14 @@ export default {
"Issuer:": "Aussteller:", "Issuer:": "Aussteller:",
"Fingerprint:": "Fingerabdruck:", "Fingerprint:": "Fingerabdruck:",
"No status pages": "Keine Status-Seiten", "No status pages": "Keine Status-Seiten",
"Domain Name Expiry Notification": "Benachrichtigung bei Ablauf des Domainnamens",
Customize: "Anpassen",
"Custom Footer": "Eigener Footer",
"Custom CSS": "Eigenes CSS",
"Footer Text": "Fußzeile",
"Show Powered By": "Zeige 'Powered By'",
"Date Created": "Erstellt am",
"Domain Names": "Domainnamen",
signedInDisp: "Angemeldet als {0}",
signedInDispDisabled: "Authentifizierung deaktiviert.",
}; };

@ -309,6 +309,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",
@ -354,6 +358,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",
@ -417,7 +424,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.",
@ -443,7 +450,7 @@ 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", onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type", onebotMessageType: "OneBot Message Type",
@ -452,4 +459,9 @@ export default {
onebotUserOrGroupId: "Group/User ID", onebotUserOrGroupId: "Group/User ID",
onebotSafetyTips: "For safety, must set access token", onebotSafetyTips: "For safety, must set access token",
"PushDeer Key": "PushDeer Key", "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.",
}; };

@ -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",
}; };

@ -66,7 +66,7 @@ export default {
Keyword: "Słowo kluczowe", Keyword: "Słowo kluczowe",
"Friendly Name": "Przyjazna nazwa", "Friendly Name": "Przyjazna nazwa",
URL: "URL", URL: "URL",
Hostname: "Hostname", Hostname: "Nazwa hosta",
Port: "Port", Port: "Port",
"Heartbeat Interval": "Częstotliwość bicia serca", "Heartbeat Interval": "Częstotliwość bicia serca",
Retries: "Prób", Retries: "Prób",
@ -216,7 +216,7 @@ export default {
signal: "Signal", signal: "Signal",
Number: "Numer", Number: "Numer",
Recipients: "Odbiorcy", Recipients: "Odbiorcy",
needSignalAPI: "Musisz posiadać klienta Signal z REST API.", needSignalAPI: "Musisz mieć klienta Signal z REST API.",
wayToCheckSignalURL: "W celu dowiedzenia się, jak go skonfigurować, odwiedź poniższy link:", wayToCheckSignalURL: "W celu dowiedzenia się, jak go skonfigurować, odwiedź poniższy link:",
signalImportant: "UWAGA: Nie można mieszać nazw grup i numerów odbiorców!", signalImportant: "UWAGA: Nie można mieszać nazw grup i numerów odbiorców!",
gotify: "Gotify", gotify: "Gotify",
@ -234,6 +234,7 @@ export default {
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
PushByTechulus: "Push od Techulus",
octopush: "Octopush", octopush: "Octopush",
promosms: "PromoSMS", promosms: "PromoSMS",
lunasea: "LunaSea", lunasea: "LunaSea",
@ -278,7 +279,7 @@ export default {
promosmsTypeEco: "SMS ECO - tanie, lecz wolne. Dostępne tylko w Polsce", promosmsTypeEco: "SMS ECO - tanie, lecz wolne. Dostępne tylko w Polsce",
promosmsTypeFlash: "SMS FLASH - wiadomość automatycznie wyświetli się na urządzeniu. Dostępne tylko w Polsce.", promosmsTypeFlash: "SMS FLASH - wiadomość automatycznie wyświetli się na urządzeniu. Dostępne tylko w Polsce.",
promosmsTypeFull: "SMS FULL - szybkie i dostępne międzynarodowo. Wersja premium usługi, która pozwala min. ustawić własną nazwę nadawcy.", promosmsTypeFull: "SMS FULL - szybkie i dostępne międzynarodowo. Wersja premium usługi, która pozwala min. ustawić własną nazwę nadawcy.",
promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, posiada wszystkie zalety SMS FULL", promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL",
promosmsPhoneNumber: "Numer odbiorcy", promosmsPhoneNumber: "Numer odbiorcy",
promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)", promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)",
"Primary Base URL": "Główny URL", "Primary Base URL": "Główny URL",
@ -306,6 +307,10 @@ export default {
"One record": "Jeden rekord", "One record": "Jeden rekord",
steamApiKeyDescription: "Do monitorowania serwera gier Steam potrzebny jest klucz Steam Web-API. Możesz zarejestrować swój klucz API tutaj: ", steamApiKeyDescription: "Do monitorowania serwera gier Steam potrzebny jest klucz Steam Web-API. Możesz zarejestrować swój klucz API tutaj: ",
"Current User": "Aktualny użytkownik", "Current User": "Aktualny użytkownik",
topic: "Temat",
topicExplanation: "Temat MQTT do monitorowania",
successMessage: "Komunikat o powodzeniu",
successMessageExplanation: "Komunikat MQTT, który zostanie uznany za powodzenie",
recent: "Ostatnie", recent: "Ostatnie",
Done: "Zrobione", Done: "Zrobione",
Info: "Info", Info: "Info",
@ -344,7 +349,7 @@ export default {
Discard: "Odrzuć", Discard: "Odrzuć",
Cancel: "Anuluj", Cancel: "Anuluj",
"Powered by": "Napędzane przez", "Powered by": "Napędzane przez",
shrinkDatabaseDescription: "Uruchom VACUUM na bazie SQLite. Jeżeli twoja baza została stworzona po wersji 1.10.0, to posiada już włączoną opcję AUTO_VACUUM i stosowanie ręcznego oczyszczania nie jest potrzebne.", shrinkDatabaseDescription: "Uruchom VACUUM na bazie SQLite. Jeżeli twoja baza została stworzona po wersji 1.10.0, to ma już włączoną opcję AUTO_VACUUM i stosowanie ręcznego oczyszczania nie jest potrzebne.",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
apiCredentials: "Poświadczenia API", apiCredentials: "Poświadczenia API",
serwersms: "SerwerSMS.pl", serwersms: "SerwerSMS.pl",
@ -352,14 +357,111 @@ export default {
serwersmsAPIPassword: "Hasło API", serwersmsAPIPassword: "Hasło API",
serwersmsPhoneNumber: "Numer telefonu", serwersmsPhoneNumber: "Numer telefonu",
serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)", serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)",
"stackfield": "Stackfield", stackfield: "Stackfield",
Customize: "Dostosuj",
"Custom Footer": "Niestandardowa stopka",
"Custom CSS": "Niestandardowy CSS",
smtpDkimSettings: "Ustawienia DKIM", smtpDkimSettings: "Ustawienia DKIM",
smtpDkimDesc: "Zapoznaj się z Nodemailer DKIM {0}, aby dowiedzieć się więcej", smtpDkimDesc: "Zapoznaj się z Nodemailer DKIM {0}, aby dowiedzieć się więcej",
documentation: "dokumentacja", documentation: "dokumentacja",
smtpDkimDomain: "Nazwa domeny", smtpDkimDomain: "Nazwa domeny",
smtpDkimKeySelector: "Selektor klucza", smtpDkimKeySelector: "Selektor klucza",
smtpDkimPrivateKey: "Klucz prywatny", smtpDkimPrivateKey: "Klucz prywatny",
smtpDkimHashAlgo: "Algorytm Hashowania (opcjonalne)", smtpDkimHashAlgo: "Algorytm haszujący (opcjonalne)",
smtpDkimheaderFieldNames: "Klucze nagłówka do podpisu (opcjonalne)", smtpDkimheaderFieldNames: "Klucze nagłówka do podpisu (opcjonalne)",
smtpDkimskipFields: "Klucze nagłówka do pominięcia (opcjonalne)", smtpDkimskipFields: "Klucze nagłówka do pominięcia (opcjonalne)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Punkt końcowy API",
alertaEnvironment: "Środowisko",
alertaApiKey: "Klucz API",
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Jesteś pewien, że chcesz usunąć tę stronę statusów?",
Proxies: "Proxy",
default: "Domyślny",
enabled: "Włączony",
setAsDefault: "Ustaw jako domyślny",
deleteProxyMsg: "Jesteś pewien, że chcesz usunąć proxy ze wszystkich monitorów?",
proxyDescription: "Proxy muszą być przypisane do monitora, aby działały.",
enableProxyDescription: "Ten serwer proxy nie będzie miał wpływu na żądania monitorów, dopóki nie zostanie aktywowany. Możesz kontrolować tymczasowe wyłączenie serwera proxy ze wszystkich monitorów za pomocą statusu aktywacji.",
setAsDefaultProxyDescription: "Ten serwer proxy będzie domyślnie włączony dla nowych monitorów. Można go jednak wyłączyć osobno dla każdego monitora.",
"Certificate Chain": "Łańcuch certyfikatów",
Valid: "Ważny",
Invalid: "Nieważny",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Sekret",
PhoneNumbers: "Numery telefonów",
TemplateCode: "Kod szablonu",
SignName: "Podpis",
"Sms template must contain parameters: ": "Szablon sms musi posiadać parametry: ",
"Bark Endpoint": "Punkt końcowy Bark",
WebHookUrl: "WebHookUrl",
SecretKey: "Tajny klucz",
"For safety, must use secret key": "Ze względów bezpieczeństwa musisz użyć tajnego klucza",
"Device Token": "Device Token",
Platform: "Platforma",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Wysoki",
Retry: "Ponów",
Topic: "Temat",
"WeCom Bot Key": "Klucz bota WeCom",
"Setup Proxy": "Skonfiguruj proxy",
"Proxy Protocol": "Protokół proxy",
"Proxy Server": "Serwer proxy",
"Proxy server has authentication": "Serwer proxy ma autoryzację",
User: "Użytkownik",
Installed: "Zainstalowany",
"Not installed": "Nie zainstalowany",
Running: "Działa",
"Not running": "Nie działa",
"Remove Token": "Usuń token",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Dodaj nową stronę statusów",
Slug: "Symbol",
"Accept characters:": "Dozwolone znaki:",
startOrEndWithOnly: "Zaczynające się i kończące wyłącznie {0} znakami",
"No consecutive dashes": "Bez powtarzających się myślników",
Next: "Dalej",
"The slug is already taken. Please choose another slug.": "Ten symbol jest już zajęty. Proszę, wybierz inny.",
"No Proxy": "Bez proxy",
"HTTP Basic Auth": "Podstawowa autoryzacja HTTP",
"New Status Page": "Nowa strona statusu",
"Page Not Found": "Strona nie została znaleziona",
"Reverse Proxy": "Odwrotne Proxy",
Backup: "Backup",
About: "O skrypcie",
wayToGetCloudflaredURL: "(Pobierz cloudflared z {0})",
cloudflareWebsite: "Strona Cloudflare",
"Message:": "Wiadomość:",
"Don't know how to get the token? Please read the guide:": "Nie wiesz jak uzyksać token? Przeczytaj proszę poradnik:",
"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.": "Bieżące połączenie może zostać utracone, jeśli aktualnie łączysz się przez tunel Cloudflare. Czy na pewno chcesz to przerwać? Wpisz swoje aktualne hasło, aby je potwierdzić.",
"Other Software": "Inne oprogramowanie",
"For example: nginx, Apache and Traefik.": "Na przykład: nginx, Apache i Traefik.",
"Please read": "Przeczytaj proszę",
"Subject:": "Temat:",
"Valid To:": "Ważdny do:",
"Days Remaining:": "Pozostało dni:",
"Issuer:": "Wydawca:",
"Fingerprint:": "Odcisk palca:",
"No status pages": "Brak stron statusów",
"Domain Name Expiry Notification": "Powiadomienie o wygasaniu domeny",
Proxy: "Proxy",
"Date Created": "Data stworzenia",
onebotHttpAddress: "Adres HTTP OneBot",
onebotMessageType: "Rodzaj wiadomości OneBot",
onebotGroupMessage: "Grupowa",
onebotPrivateMessage: "Prywatna",
onebotUserOrGroupId: "ID Grupy/Użytkownika",
onebotSafetyTips: "Ze względów bezpieczeństwa musisz ustawić token dostępu",
"PushDeer Key": "Klucz PushDeer",
"Footer Text": "Treść stopki",
"Show Powered By": "Pokaż co napędza stronę",
"Domain Names": "Domeny",
signedInDisp: "Zalogowany jako {0}",
signedInDispDisabled: "Autoryzacja wyłączona.",
}; };

@ -381,7 +381,7 @@ export default {
smtpDkimPrivateKey: "Приватный ключ", smtpDkimPrivateKey: "Приватный ключ",
smtpDkimHashAlgo: "Алгоритм хэша (опционально)", smtpDkimHashAlgo: "Алгоритм хэша (опционально)",
smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)", smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)",
smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)", smtpDkimskipFields: "Заголовок ключей не для подписи (опционально)",
gorush: "Gorush", gorush: "Gorush",
alerta: "Alerta", alerta: "Alerta",
alertaApiEndpoint: "Конечная точка API", alertaApiEndpoint: "Конечная точка API",

@ -239,6 +239,7 @@ export default {
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush", octopush: "Octopush",
promosms: "PromoSMS", promosms: "PromoSMS",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
@ -308,6 +309,10 @@ export default {
"One record": "One record", "One record": "One record",
steamApiKeyDescription: "Để theo dõi các Steam Game Server bạn cần một Steam Web-API key. Bạn có thể đăng ký API key tại đây: ", steamApiKeyDescription: "Để theo dõi các Steam Game Server bạn cần một Steam Web-API key. Bạn có thể đăng ký API key tại đây: ",
"Current User": "User hiện tại", "Current User": "User hiện tại",
topic: "Topic",
topicExplanation: "MQTT topic to monitor",
successMessage: "Success Message",
successMessageExplanation: "MQTT message that will be considered as success",
recent: "Gần đây", recent: "Gần đây",
Done: "Hoàn thành", Done: "Hoàn thành",
Info: "Thông tin", Info: "Thông tin",
@ -353,6 +358,9 @@ export default {
serwersmsPhoneNumber: "Số điện thoại", serwersmsPhoneNumber: "Số điện thoại",
serwersmsSenderName: "Tên người gửi SMS (Đã đăng ký qua portal)", serwersmsSenderName: "Tên người gửi SMS (Đã đăng ký qua portal)",
"stackfield": "Stackfield", "stackfield": "Stackfield",
Customize: "Customize",
"Custom Footer": "Custom Footer",
"Custom CSS": "Custom CSS",
smtpDkimSettings: "Cài đặt xác thực Email(DKIM)", smtpDkimSettings: "Cài đặt xác thực Email(DKIM)",
smtpDkimDesc: "Xem hướng dẫn tại {0}.", smtpDkimDesc: "Xem hướng dẫn tại {0}.",
documentation: "Nodemailer DKIM", documentation: "Nodemailer DKIM",
@ -362,4 +370,98 @@ export default {
smtpDkimHashAlgo: "Hash Algorithm (Tuỳ chọn)", smtpDkimHashAlgo: "Hash Algorithm (Tuỳ chọn)",
smtpDkimheaderFieldNames: "Header Keys to sign (Tuỳ chọn)", smtpDkimheaderFieldNames: "Header Keys to sign (Tuỳ chọn)",
smtpDkimskipFields: "Header Keys not to sign (Tuỳ chọn)", smtpDkimskipFields: "Header Keys not to sign (Tuỳ chọn)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "API Key",
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Bạn có chắc chắn muốn xoá trang status này?",
Proxies: "Proxies",
default: "Mặc định",
enabled: "Enabled",
setAsDefault: "Set As Default",
deleteProxyMsg: "Bạn muốn xoá proxy này cho tất cả monitors?",
proxyDescription: "Proxies must be assigned to a monitor to function.",
enableProxyDescription: "Proxy này chưa ảnh hưởng tới monitor requests cho tới khi được activated. Bạn có thể tạm thời tắt proxy cho tất cả monitors bằng trạng thái activation.",
setAsDefaultProxyDescription: "Proxy này sẽ bật mặc định cho tất cả monitors mới. Bạn có thể tắt riêng lẻ proxy trên mỗi monitor.",
"Certificate Chain": "Certificate Chain",
Valid: "Hợp lệ",
Invalid: "Không hợp lệ",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms template must contain parameters: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "Để an toàn, hãy dùng secret key",
"Device Token": "Device Token",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "Retry",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Setup Proxy",
"Proxy Protocol": "Proxy Protocol",
"Proxy Server": "Proxy Server",
"Proxy server has authentication": "Proxy server has authentication",
User: "User",
Installed: "Installed",
"Not installed": "Not installed",
Running: "Running",
"Not running": "Not running",
"Remove Token": "Remove Token",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Thêm mới Status Page",
Slug: "Slug",
"Accept characters:": "Accept characters:",
startOrEndWithOnly: "Start or end with {0} only",
"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.",
"No Proxy": "No Proxy",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "New Status Page",
"Page Not Found": "Page Not Found",
"Reverse Proxy": "Reverse Proxy",
Backup: "Backup",
About: "About",
wayToGetCloudflaredURL: "(Download cloudflared from {0})",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Message:",
"Don't know how to get the token? Please read the guide:": "Chưa biết cách lấy token? Xem hướng dẫn tại:",
"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.": "Nếu bạn đang dùng Cloudflare Tunnel, kết nối hiện tại có thể đang bị mất. Bạn có muốn dừng lại? Nhập lại password để xác nhận.",
"Other Software": "Phần mềm khác",
"For example: nginx, Apache and Traefik.": "Ví dụ: Nginx, Apache hay Traefik.",
"Please read": "Hãy xem qua",
"Subject:": "Subject:",
"Valid To:": "Valid To:",
"Days Remaining:": "Số ngày còn lại:",
"Issuer:": "Issuer:",
"Fingerprint:": "Fingerprint:",
"No status pages": "No status pages",
"Domain Name Expiry Notification": "Cảnh báo hạn hạn Domain Name",
Proxy: "Proxy",
"Date Created": "Ngày khởi tạo",
onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group",
onebotPrivateMessage: "Private",
onebotUserOrGroupId: "Group/User ID",
onebotSafetyTips: "Để đảm bảo an toàn, hãy thiết lập 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.",
}; };

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

Loading…
Cancel
Save