mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 17:06:25 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
a9f3142cee
60
.eslintrc.js
60
.eslintrc.js
@ -1,4 +1,9 @@
|
||||
module.exports = {
|
||||
ignorePatterns: [
|
||||
"test/*",
|
||||
"server/modules/apicache/*",
|
||||
"src/util.js"
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
@ -17,39 +22,48 @@ module.exports = {
|
||||
requireConfigFile: false,
|
||||
},
|
||||
rules: {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"camelcase": ["warn", {
|
||||
"yoda": "error",
|
||||
eqeqeq: [ "warn", "smart" ],
|
||||
"linebreak-style": [ "error", "unix" ],
|
||||
"camelcase": [ "warn", {
|
||||
"properties": "never",
|
||||
"ignoreImports": true
|
||||
}],
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
"no-unused-vars": "warn",
|
||||
"no-unused-vars": [ "warn", {
|
||||
"args": "none"
|
||||
}],
|
||||
indent: [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
ignoredNodes: ["TemplateLiteral"],
|
||||
ignoredNodes: [ "TemplateLiteral" ],
|
||||
SwitchCase: 1,
|
||||
},
|
||||
],
|
||||
quotes: ["warn", "double"],
|
||||
semi: "warn",
|
||||
"vue/html-indent": ["warn", 4], // default: 2
|
||||
quotes: [ "error", "double" ],
|
||||
semi: "error",
|
||||
"vue/html-indent": [ "error", 4 ], // default: 2
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
|
||||
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||
"no-multi-spaces": ["error", {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"no-multi-spaces": [ "error", {
|
||||
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",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"curly": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"object-curly-spacing": [ "error", "always" ],
|
||||
"object-curly-newline": "off",
|
||||
"object-property-newline": "error",
|
||||
"comma-spacing": "error",
|
||||
@ -59,37 +73,37 @@ module.exports = {
|
||||
"keyword-spacing": "warn",
|
||||
"space-infix-ops": "warn",
|
||||
"arrow-spacing": "warn",
|
||||
"no-trailing-spaces": "warn",
|
||||
"no-constant-condition": ["error", {
|
||||
"no-trailing-spaces": "error",
|
||||
"no-constant-condition": [ "error", {
|
||||
"checkLoops": false,
|
||||
}],
|
||||
"space-before-blocks": "warn",
|
||||
//'no-console': 'warn',
|
||||
"no-extra-boolean-cast": "off",
|
||||
"no-multiple-empty-lines": ["warn", {
|
||||
"no-multiple-empty-lines": [ "warn", {
|
||||
"max": 1,
|
||||
"maxBOF": 0,
|
||||
}],
|
||||
"lines-between-class-members": ["warn", "always", {
|
||||
"lines-between-class-members": [ "warn", "always", {
|
||||
exceptAfterSingleLine: true,
|
||||
}],
|
||||
"no-unneeded-ternary": "error",
|
||||
"array-bracket-newline": ["error", "consistent"],
|
||||
"eol-last": ["error", "always"],
|
||||
"array-bracket-newline": [ "error", "consistent" ],
|
||||
"eol-last": [ "error", "always" ],
|
||||
//'prefer-template': 'error',
|
||||
"comma-dangle": ["warn", "only-multiline"],
|
||||
"no-empty": ["error", {
|
||||
"comma-dangle": [ "warn", "only-multiline" ],
|
||||
"no-empty": [ "error", {
|
||||
"allowEmptyCatch": true
|
||||
}],
|
||||
"no-control-regex": "off",
|
||||
"one-var": ["error", "never"],
|
||||
"max-statements-per-line": ["error", { "max": 1 }]
|
||||
"one-var": [ "error", "never" ],
|
||||
"max-statements-per-line": [ "error", { "max": 1 }]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [ "src/languages/*.js", "src/icon.js" ],
|
||||
"rules": {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-dangle": [ "error", "always-multiline" ],
|
||||
}
|
||||
},
|
||||
|
||||
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -20,6 +20,7 @@ Please delete any options that are not relevant.
|
||||
- [ ] I ran ESLint and other linters for modified files
|
||||
- [ ] I have performed a self-review of my own code and tested it
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
(including JSDoc for methods)
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||
|
||||
|
29
.github/workflows/auto-test.yml
vendored
29
.github/workflows/auto-test.yml
vendored
@ -11,25 +11,42 @@ on:
|
||||
|
||||
jobs:
|
||||
auto-test:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [14.x, 16.x, 17.x]
|
||||
node: [ 14, 16, 17, 18 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
- run: npm run install-legacy
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
env:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||
check-linters:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
|
@ -1,9 +1,13 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"customSyntax": "postcss-html",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"no-descending-specificity": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"declaration-empty-line-before": null
|
||||
"declaration-empty-line-before": null,
|
||||
"alpha-value-notation": "number",
|
||||
"color-function-notation": "legacy",
|
||||
"shorthand-property-no-redundant-values": null
|
||||
}
|
||||
}
|
||||
|
@ -27,24 +27,20 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
||||
|
||||
## Can I create a pull request for Uptime Kuma?
|
||||
|
||||
⚠️ 2022-03-02 Update:
|
||||
|
||||
Since I found that merging pull requests is a pretty heavy task for me, I try to rearrange it.
|
||||
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first.
|
||||
|
||||
✅ Accept:
|
||||
- Bug/Security fix
|
||||
- Translations
|
||||
- Adding notification providers
|
||||
|
||||
❌ Avoid:
|
||||
⚠️ Discuss First
|
||||
- Large pull requests
|
||||
- New big features
|
||||
|
||||
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
|
||||
- New features
|
||||
|
||||
### Recommended Pull Request Guideline
|
||||
|
||||
Before deep into coding, disscussion first is preferred. Creating an empty pull request for disscussion would be recommended.
|
||||
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||
|
||||
1. Fork the project
|
||||
1. Clone your fork repo to local
|
||||
@ -79,6 +75,7 @@ I personally do not like something need to learn so much and need to config so m
|
||||
- 4 spaces indentation
|
||||
- Follow `.editorconfig`
|
||||
- Follow ESLint
|
||||
- Methods and functions should be documented with JSDoc
|
||||
|
||||
## Name convention
|
||||
|
||||
@ -89,9 +86,10 @@ I personally do not like something need to learn so much and need to config so m
|
||||
## Tools
|
||||
|
||||
- Node.js >= 14
|
||||
- NPM >= 8.5
|
||||
- Git
|
||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||
|
||||
## Install dependencies
|
||||
|
||||
@ -99,39 +97,45 @@ I personally do not like something need to learn so much and need to config so m
|
||||
npm ci
|
||||
```
|
||||
|
||||
## How to start the Backend Dev Server
|
||||
## Dev Server
|
||||
|
||||
(2021-09-23 Update)
|
||||
(2022-04-26 Update)
|
||||
|
||||
We can start the frontend dev server and the backend dev server in one command.
|
||||
|
||||
Port `3000` and port `3001` will be used.
|
||||
|
||||
```bash
|
||||
npm run start-server-dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Backend Server
|
||||
|
||||
It binds to `0.0.0.0:3001` by default.
|
||||
|
||||
### Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
express.js is used for:
|
||||
- entry point such as redirecting to a status page or the dashboard
|
||||
- serving the frontend built files (index.html, .js and .css etc.)
|
||||
- serving internal APIs of status page
|
||||
|
||||
|
||||
### Structure in /server/
|
||||
|
||||
- model/ (Object model, auto mapping to the database table name)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- notification-providers/ (individual notification logic)
|
||||
- routers/ (Express Routers)
|
||||
- socket-handler (Socket.io Handlers)
|
||||
- server.js (Server main logic)
|
||||
- server.js (Server entry point and main logic)
|
||||
|
||||
## How to start the Frontend Dev Server
|
||||
## Frontend Dev Server
|
||||
|
||||
1. Set the env var `NODE_ENV` to "development".
|
||||
2. Start the frontend dev server by the following command.
|
||||
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
It binds to `0.0.0.0:3000` by default.
|
||||
For production, it is not used. It will be compiled to `dist` directory instead.
|
||||
|
||||
You can use Vue.js devtools Chrome extension for debugging.
|
||||
|
||||
|
20
README.md
20
README.md
@ -25,12 +25,15 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||
* 20 second intervals.
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||
* Simple Status Page
|
||||
* Multiple Status Pages
|
||||
* Map Status Page to Domain
|
||||
* Ping Chart
|
||||
* Certificate Info
|
||||
* Proxy Support
|
||||
* 2FA available
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
@ -154,10 +157,17 @@ https://www.reddit.com/r/UptimeKuma/
|
||||
|
||||
## Contribute
|
||||
|
||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||
### Beta Version
|
||||
|
||||
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
||||
|
||||
### Bug Reports / Feature Requests
|
||||
If you want to report a bug or request a new feature, feel free to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||
|
||||
### Translations
|
||||
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
||||
|
||||
Unfortunately, English proofreading is needed too because my grammar is not that great. Feel free to correct my grammar in this README, source code, or wiki.
|
||||
### Pull Requests
|
||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
@ -13,10 +13,7 @@ currently being supported with security updates.
|
||||
|
||||
### Uptime Kuma Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.9.X | :white_check_mark: |
|
||||
| <= 1.8.X | ❌ |
|
||||
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
@ -24,8 +21,8 @@ currently being supported with security updates.
|
||||
| ------- | ------------------ |
|
||||
| 1 | :white_check_mark: |
|
||||
| 1-debian | :white_check_mark: |
|
||||
| 1-alpine | :white_check_mark: |
|
||||
| latest | :white_check_mark: |
|
||||
| debian | :white_check_mark: |
|
||||
| alpine | :white_check_mark: |
|
||||
| 1-alpine | ⚠️ Deprecated |
|
||||
| alpine | ⚠️ Deprecated |
|
||||
| All other tags | ❌ |
|
||||
|
@ -1,11 +1,11 @@
|
||||
const config = {};
|
||||
|
||||
if (process.env.TEST_FRONTEND) {
|
||||
config.presets = ["@babel/preset-env"];
|
||||
config.presets = [ "@babel/preset-env" ];
|
||||
}
|
||||
|
||||
if (process.env.TEST_BACKEND) {
|
||||
config.plugins = ["babel-plugin-rewire"];
|
||||
config.plugins = [ "babel-plugin-rewire" ];
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
|
@ -10,15 +10,15 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ["ie > 11"],
|
||||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
|
||||
targets: [ "ie > 11" ],
|
||||
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
|
||||
})
|
||||
],
|
||||
css: {
|
||||
postcss: {
|
||||
"parser": postCssScss,
|
||||
"map": false,
|
||||
"plugins": [postcssRTLCSS]
|
||||
"plugins": [ postcssRTLCSS ]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
16
db/patch-added-mqtt-monitor.sql
Normal file
16
db/patch-added-mqtt-monitor.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD mqtt_topic TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD mqtt_success_message VARCHAR(255);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD mqtt_username VARCHAR(255);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD mqtt_password VARCHAR(255);
|
||||
|
||||
COMMIT;
|
6
db/patch-status-page-footer-css.sql
Normal file
6
db/patch-status-page-footer-css.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
ALTER TABLE status_page ADD footer_text TEXT;
|
||||
ALTER TABLE status_page ADD custom_css TEXT;
|
||||
ALTER TABLE status_page ADD show_powered_by BOOLEAN NOT NULL DEFAULT 1;
|
||||
COMMIT;
|
@ -4,5 +4,5 @@ WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
||||
rm -rf /root/.cache
|
||||
|
@ -11,7 +11,7 @@ WORKDIR /app
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init && \
|
||||
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install cloudflared
|
||||
|
@ -8,7 +8,7 @@ services:
|
||||
image: louislam/uptime-kuma:1
|
||||
container_name: uptime-kuma
|
||||
volumes:
|
||||
- ./uptime-kuma:/app/data
|
||||
- ./uptime-kuma-data:/app/data
|
||||
ports:
|
||||
- 3001:3001
|
||||
- 3001:3001 # <Host Port>:<Container Port>
|
||||
restart: always
|
||||
|
@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "uptime-kuma",
|
||||
script: "./server/server.js",
|
||||
}]
|
||||
}
|
||||
apps: [{
|
||||
name: "uptime-kuma",
|
||||
script: "./server/server.js",
|
||||
}]
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
const pkg = require("../../package.json");
|
||||
const fs = require("fs");
|
||||
const child_process = require("child_process");
|
||||
const childProcess = require("child_process");
|
||||
const util = require("../../src/util");
|
||||
|
||||
util.polyfill();
|
||||
|
||||
const oldVersion = pkg.version;
|
||||
const version = process.env.VERSION;
|
||||
|
||||
console.log("Beta Version: " + version);
|
||||
@ -21,6 +20,10 @@ if (! exists) {
|
||||
// Process package.json
|
||||
pkg.version = version;
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
// Also update package-lock.json
|
||||
childProcess.spawnSync("npm", [ "install" ]);
|
||||
|
||||
commit(version);
|
||||
tag(version);
|
||||
|
||||
@ -32,7 +35,7 @@ if (! exists) {
|
||||
function commit(version) {
|
||||
let msg = "Update to " + version;
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
console.log(stdout);
|
||||
|
||||
@ -40,15 +43,15 @@ function commit(version) {
|
||||
throw new Error("commit error");
|
||||
}
|
||||
|
||||
res = child_process.spawnSync("git", ["push", "origin", "master"]);
|
||||
res = childProcess.spawnSync("git", [ "push", "origin", "master" ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
function tag(version) {
|
||||
let res = child_process.spawnSync("git", ["tag", version]);
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
|
||||
res = child_process.spawnSync("git", ["push", "origin", version]);
|
||||
res = childProcess.spawnSync("git", [ "push", "origin", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
@ -57,15 +60,7 @@ function tagExists(version) {
|
||||
throw new Error("invalid version");
|
||||
}
|
||||
|
||||
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
||||
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmdirSync(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ const github = require("@actions/github");
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["invalid-format"]
|
||||
labels: [ "invalid-format" ]
|
||||
});
|
||||
|
||||
// Add the issue closing comment
|
||||
|
@ -4,21 +4,21 @@ const util = require("../src/util");
|
||||
|
||||
util.polyfill();
|
||||
|
||||
const oldVersion = pkg.version
|
||||
const newVersion = oldVersion + "-nightly"
|
||||
const oldVersion = pkg.version;
|
||||
const newVersion = oldVersion + "-nightly";
|
||||
|
||||
console.log("Old Version: " + oldVersion)
|
||||
console.log("New Version: " + newVersion)
|
||||
console.log("Old Version: " + oldVersion);
|
||||
console.log("New Version: " + newVersion);
|
||||
|
||||
if (newVersion) {
|
||||
// Process package.json
|
||||
pkg.version = newVersion
|
||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
|
||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
|
||||
pkg.version = newVersion;
|
||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
// Process README.md
|
||||
if (fs.existsSync("README.md")) {
|
||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion))
|
||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ const Database = require("../server/database");
|
||||
const { R } = require("redbean-node");
|
||||
const readline = require("readline");
|
||||
const { initJWTSecret } = require("../server/util-server");
|
||||
const User = require("../server/model/user");
|
||||
const args = require("args-parser")(process.argv);
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@ -30,7 +31,7 @@ const main = async () => {
|
||||
let confirmPassword = await question("Confirm New Password: ");
|
||||
|
||||
if (password === confirmPassword) {
|
||||
await user.resetPassword(password);
|
||||
await User.resetPassword(user.id, password);
|
||||
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
@ -26,7 +26,7 @@ server.on("request", (request, send, rinfo) => {
|
||||
ttl: 300,
|
||||
address: "1.2.3.4"
|
||||
});
|
||||
} if (question.type === Packet.TYPE.AAAA) {
|
||||
} else if (question.type === Packet.TYPE.AAAA) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
|
50
extra/simple-mqtt-server.js
Normal file
50
extra/simple-mqtt-server.js
Normal file
@ -0,0 +1,50 @@
|
||||
const { log } = require("../src/util");
|
||||
|
||||
const mqttUsername = "louis1";
|
||||
const mqttPassword = "!@#$LLam";
|
||||
|
||||
class SimpleMqttServer {
|
||||
aedes = require("aedes")();
|
||||
server = require("net").createServer(this.aedes.handle);
|
||||
|
||||
constructor(port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.server.listen(this.port, () => {
|
||||
console.log("server started and listening on port ", this.port);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let server1 = new SimpleMqttServer(10000);
|
||||
|
||||
server1.aedes.authenticate = function (client, username, password, callback) {
|
||||
if (username && password) {
|
||||
console.log(password.toString("utf-8"));
|
||||
callback(null, username === mqttUsername && password.toString("utf-8") === mqttPassword);
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
};
|
||||
|
||||
server1.aedes.on("subscribe", (subscriptions, client) => {
|
||||
console.log(subscriptions);
|
||||
|
||||
for (let s of subscriptions) {
|
||||
if (s.topic === "test") {
|
||||
server1.aedes.publish({
|
||||
topic: "test",
|
||||
payload: Buffer.from("ok"),
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
log.error("mqtt_server", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
server1.start();
|
@ -1,7 +1,6 @@
|
||||
const pkg = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const rmSync = require("./fs-rmSync.js");
|
||||
const child_process = require("child_process");
|
||||
const childProcess = require("child_process");
|
||||
const util = require("../src/util");
|
||||
|
||||
util.polyfill();
|
||||
@ -26,6 +25,9 @@ if (! exists) {
|
||||
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
// Also update package-lock.json
|
||||
childProcess.spawnSync("npm", [ "install" ]);
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
@ -42,7 +44,7 @@ if (! exists) {
|
||||
function commit(version) {
|
||||
let msg = "Update to " + version;
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
console.log(stdout);
|
||||
|
||||
@ -52,7 +54,7 @@ function commit(version) {
|
||||
}
|
||||
|
||||
function tag(version) {
|
||||
let res = child_process.spawnSync("git", ["tag", version]);
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
@ -67,7 +69,7 @@ function tagExists(version) {
|
||||
throw new Error("invalid version");
|
||||
}
|
||||
|
||||
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
const child_process = require("child_process");
|
||||
const childProcess = require("child_process");
|
||||
const fs = require("fs");
|
||||
|
||||
const newVersion = process.env.VERSION;
|
||||
@ -16,23 +16,23 @@ function updateWiki(newVersion) {
|
||||
|
||||
safeDelete(wikiDir);
|
||||
|
||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||
childProcess.spawnSync("git", [ "clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir ]);
|
||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||
|
||||
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||
fs.writeFileSync(howToUpdateFilename, content);
|
||||
|
||||
child_process.spawnSync("git", ["add", "-A"], {
|
||||
childProcess.spawnSync("git", [ "add", "-A" ], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
|
||||
childProcess.spawnSync("git", [ "commit", "-m", `Update to ${newVersion}` ], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
console.log("Pushing to Github");
|
||||
child_process.spawnSync("git", ["push"], {
|
||||
childProcess.spawnSync("git", [ "push" ], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
|
3546
package-lock.json
generated
3546
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.14.0",
|
||||
"version": "1.16.0-beta.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -10,12 +10,15 @@
|
||||
"node": "14.* || >=16.*"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install --legacy-peer-deps",
|
||||
"update-legacy": "npm update --legacy-peer-deps",
|
||||
"install-legacy": "npm install",
|
||||
"update-legacy": "npm update",
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "vite --host --config ./config/vite.config.js",
|
||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||
@ -36,7 +39,7 @@
|
||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.14.0 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@ -48,6 +51,7 @@
|
||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||
"ncu-patch": "npm-check-updates -u -t patch",
|
||||
@ -60,10 +64,11 @@
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||
"@louislam/sqlite3": "~6.0.1",
|
||||
"@louislam/sqlite3": "~15.0.6",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.26.1",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "5.1.3",
|
||||
"bree": "~7.1.5",
|
||||
@ -71,6 +76,7 @@
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"check-password-strength": "^2.0.5",
|
||||
"chroma-js": "^2.1.2",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.8",
|
||||
@ -85,12 +91,14 @@
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"limiter": "^2.1.0",
|
||||
"mqtt": "^4.2.8",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.3",
|
||||
"prismjs": "^1.27.0",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"qrcode": "~1.5.0",
|
||||
@ -110,6 +118,7 @@
|
||||
"vue-i18n": "~9.1.9",
|
||||
"vue-image-crop-upload": "~3.0.3",
|
||||
"vue-multiselect": "~3.0.0-alpha.2",
|
||||
"vue-prism-editor": "^2.0.0-alpha.2",
|
||||
"vue-qrcode": "~1.0.0",
|
||||
"vue-router": "~4.0.14",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
@ -117,26 +126,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
"@babel/eslint-parser": "~7.15.8",
|
||||
"@babel/eslint-parser": "~7.17.0",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~1.6.4",
|
||||
"@vitejs/plugin-vue": "~1.9.4",
|
||||
"@vue/compiler-sfc": "~3.2.31",
|
||||
"aedes": "^0.46.3",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"concurrently": "^7.1.0",
|
||||
"core-js": "~3.18.3",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"eslint": "~8.14.0",
|
||||
"eslint-plugin-vue": "~8.7.1",
|
||||
"jest": "~27.2.5",
|
||||
"jest-puppeteer": "~6.0.3",
|
||||
"npm-check-updates": "^12.5.5",
|
||||
"npm-check-updates": "^12.5.9",
|
||||
"postcss-html": "^1.3.1",
|
||||
"puppeteer": "~13.1.3",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~14.2.0",
|
||||
"stylelint-config-standard": "~24.0.0",
|
||||
"stylelint": "~14.7.1",
|
||||
"stylelint-config-standard": "~25.0.0",
|
||||
"typescript": "~4.4.4",
|
||||
"vite": "~2.6.14"
|
||||
"vite": "~2.6.14",
|
||||
"wait-on": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
const { checkLogin } = require("./util-server");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class TwoFA {
|
||||
|
||||
/**
|
||||
* Disable 2FA for specified user
|
||||
* @param {number} userID ID of user to disable
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async disable2FA(userID) {
|
||||
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||
userID,
|
||||
|
@ -2,14 +2,13 @@ const basicAuth = require("express-basic-auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { debug } = require("../src/util");
|
||||
const { loginRateLimiter } = require("./rate-limiter");
|
||||
|
||||
/**
|
||||
*
|
||||
* @param username : string
|
||||
* @param password : string
|
||||
* @returns {Promise<Bean|null>}
|
||||
* Login to web app
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @returns {Promise<(Bean|null)>}
|
||||
*/
|
||||
exports.login = async function (username, password) {
|
||||
if (typeof username !== "string" || typeof password !== "string") {
|
||||
@ -35,11 +34,17 @@ exports.login = async function (username, password) {
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that checks if a user is logged in.
|
||||
* @param {string} username The username of the user to check for.
|
||||
* @param {function} callback The callback to call when done, with an error and result parameter.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Callback for myAuthorizer
|
||||
* @callback myAuthorizerCB
|
||||
* @param {any} err Any error encountered
|
||||
* @param {boolean} authorized Is the client authorized?
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom authorizer for express-basic-auth
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {myAuthorizerCB} callback
|
||||
*/
|
||||
function myAuthorizer(username, password, callback) {
|
||||
// Login Rate Limit
|
||||
|
@ -7,6 +7,7 @@ exports.latestVersion = null;
|
||||
|
||||
let interval;
|
||||
|
||||
/** Start 48 hour check interval */
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
try {
|
||||
@ -42,6 +43,11 @@ exports.startInterval = () => {
|
||||
interval = setInterval(check, 3600 * 1000 * 48);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the check update feature
|
||||
* @param {boolean} value Should the check update feature be enabled?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.enableCheckUpdate = async (value) => {
|
||||
await setSetting("checkUpdate", value);
|
||||
|
||||
|
@ -3,15 +3,15 @@
|
||||
*/
|
||||
const { TimeLogger } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { io } = require("./server");
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
const io = UptimeKumaServer.getInstance().io;
|
||||
const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
|
||||
/**
|
||||
* Send a list of notifications to the user.
|
||||
* @param {Socket} socket The socket object that is connected to the client.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Send list of notification providers to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
*/
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -34,8 +34,11 @@ async function sendNotificationList(socket) {
|
||||
|
||||
/**
|
||||
* Send Heartbeat History list to socket
|
||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param overwrite Overwrite client-side's heartbeat list
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -61,11 +64,12 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
||||
}
|
||||
|
||||
/**
|
||||
* Important Heart beat list (aka event list)
|
||||
* @param socket
|
||||
* @param monitorID
|
||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param overwrite Overwrite client-side's heartbeat list
|
||||
* Important Heart beat list (aka event list)
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -90,15 +94,14 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers proxy list
|
||||
*
|
||||
* @param socket
|
||||
* Emit proxy list to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @return {Promise<Bean[]>}
|
||||
*/
|
||||
async function sendProxyList(socket) {
|
||||
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()));
|
||||
|
||||
timeLogger.print("Send Proxy List");
|
||||
@ -108,9 +111,8 @@ async function sendProxyList(socket) {
|
||||
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket The socket object that is connected to the client.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendInfo(socket) {
|
||||
socket.emit("info", {
|
||||
|
@ -1,7 +1,20 @@
|
||||
const args = require("args-parser")(process.argv);
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
const badgeConstants = {
|
||||
naColor: "#999",
|
||||
defaultUpColor: "#66c20a",
|
||||
defaultDownColor: "#c2290a",
|
||||
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||
defaultStyle: "flat",
|
||||
defaultPingValueSuffix: "ms",
|
||||
defaultPingLabelSuffix: "h",
|
||||
defaultUptimeValueSuffix: "%",
|
||||
defaultUptimeLabelSuffix: "h",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
demoMode
|
||||
demoMode,
|
||||
badgeConstants,
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
const fs = require("fs");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { debug, sleep } = require("../src/util");
|
||||
const { log, sleep } = require("../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const knex = require("knex");
|
||||
|
||||
@ -57,7 +57,9 @@ class Database {
|
||||
"patch-status-page.sql": true,
|
||||
"patch-proxy.sql": true,
|
||||
"patch-monitor-expiry-notification.sql": true,
|
||||
}
|
||||
"patch-status-page-footer-css.sql": true,
|
||||
"patch-added-mqtt-monitor.sql": true,
|
||||
};
|
||||
|
||||
/**
|
||||
* The final version should be 10 after merged tag feature
|
||||
@ -67,6 +69,10 @@ class Database {
|
||||
|
||||
static noReject = true;
|
||||
|
||||
/**
|
||||
* Initialize the database
|
||||
* @param {Object} args Arguments to initialize DB with
|
||||
*/
|
||||
static init(args) {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
@ -81,9 +87,18 @@ class Database {
|
||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Data Dir: ${Database.dataDir}`);
|
||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database
|
||||
* @param {boolean} [testMode=false] Should the connection be
|
||||
* started in test mode?
|
||||
* @param {boolean} [autoloadModels=true] Should models be
|
||||
* automatically loaded?
|
||||
* @param {boolean} [noLog=false] Should logs not be output?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
@ -136,13 +151,14 @@ class Database {
|
||||
await R.exec("PRAGMA synchronous = FULL");
|
||||
|
||||
if (!noLog) {
|
||||
console.log("SQLite config:");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
console.log(await R.getAll("PRAGMA cache_size"));
|
||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
log.info("db", "SQLite config:");
|
||||
log.info("db", await R.getAll("PRAGMA journal_mode"));
|
||||
log.info("db", await R.getAll("PRAGMA cache_size"));
|
||||
log.info("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Patch the database */
|
||||
static async patch() {
|
||||
let version = parseInt(await setting("database_version"));
|
||||
|
||||
@ -150,15 +166,15 @@ class Database {
|
||||
version = 0;
|
||||
}
|
||||
|
||||
console.info("Your database version: " + version);
|
||||
console.info("Latest database version: " + this.latestVersion);
|
||||
log.info("db", "Your database version: " + version);
|
||||
log.info("db", "Latest database version: " + this.latestVersion);
|
||||
|
||||
if (version === this.latestVersion) {
|
||||
console.info("Database patch not needed");
|
||||
log.info("db", "Database patch not needed");
|
||||
} else if (version > this.latestVersion) {
|
||||
console.info("Warning: Database version is newer than expected");
|
||||
log.info("db", "Warning: Database version is newer than expected");
|
||||
} else {
|
||||
console.info("Database patch is needed");
|
||||
log.info("db", "Database patch is needed");
|
||||
|
||||
this.backup(version);
|
||||
|
||||
@ -166,17 +182,17 @@ class Database {
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
const sqlFile = `./db/patch${i}.sql`;
|
||||
console.info(`Patching ${sqlFile}`);
|
||||
log.info("db", `Patching ${sqlFile}`);
|
||||
await Database.importSQLFile(sqlFile);
|
||||
console.info(`Patched ${sqlFile}`);
|
||||
log.info("db", `Patched ${sqlFile}`);
|
||||
await setSetting("database_version", i);
|
||||
}
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
|
||||
console.error(ex);
|
||||
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
||||
console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
log.error("db", ex);
|
||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
process.exit(1);
|
||||
@ -188,19 +204,21 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch DB using new process
|
||||
* Call it from patch() only
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2() {
|
||||
console.log("Database Patch 2.0 Process");
|
||||
log.info("db", "Database Patch 2.0 Process");
|
||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
||||
|
||||
if (! databasePatchedFiles) {
|
||||
databasePatchedFiles = {};
|
||||
}
|
||||
|
||||
debug("Patched files:");
|
||||
debug(databasePatchedFiles);
|
||||
log.debug("db", "Patched files:");
|
||||
log.debug("db", databasePatchedFiles);
|
||||
|
||||
try {
|
||||
for (let sqlFilename in this.patchList) {
|
||||
@ -208,15 +226,15 @@ class Database {
|
||||
}
|
||||
|
||||
if (this.patched) {
|
||||
console.log("Database Patched Successfully");
|
||||
log.info("db", "Database Patched Successfully");
|
||||
}
|
||||
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
|
||||
console.error(ex);
|
||||
console.error("Start Uptime-Kuma failed due to issue patching the database");
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
log.error("db", ex);
|
||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
|
||||
@ -295,24 +313,27 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch database using new patching process
|
||||
* Used it patch2() only
|
||||
* @private
|
||||
* @param sqlFilename
|
||||
* @param databasePatchedFiles
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||
let value = this.patchList[sqlFilename];
|
||||
|
||||
if (! value) {
|
||||
console.log(sqlFilename + " skip");
|
||||
log.info("db", sqlFilename + " skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if patched
|
||||
if (! databasePatchedFiles[sqlFilename]) {
|
||||
console.log(sqlFilename + " is not patched");
|
||||
log.info("db", sqlFilename + " is not patched");
|
||||
|
||||
if (value.parents) {
|
||||
console.log(sqlFilename + " need parents");
|
||||
log.info("db", sqlFilename + " need parents");
|
||||
for (let parentSQLFilename of value.parents) {
|
||||
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
|
||||
}
|
||||
@ -320,24 +341,24 @@ class Database {
|
||||
|
||||
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||
|
||||
console.log(sqlFilename + " is patching");
|
||||
log.info("db", sqlFilename + " is patching");
|
||||
this.patched = true;
|
||||
await this.importSQLFile("./db/" + sqlFilename);
|
||||
databasePatchedFiles[sqlFilename] = true;
|
||||
console.log(sqlFilename + " was patched successfully");
|
||||
log.info("db", sqlFilename + " was patched successfully");
|
||||
|
||||
} else {
|
||||
debug(sqlFilename + " is already patched, skip");
|
||||
log.debug("db", sqlFilename + " is already patched, skip");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||
* @param filename
|
||||
* Load an SQL file and execute it
|
||||
* @param filename Filename of SQL file to import
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async importSQLFile(filename) {
|
||||
|
||||
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||
await R.getCell("SELECT 1");
|
||||
|
||||
let text = fs.readFileSync(filename).toString();
|
||||
@ -365,6 +386,10 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aquire a direct connection to database
|
||||
* @returns {any}
|
||||
*/
|
||||
static getBetterSQLite3Database() {
|
||||
return R.knex.client.acquireConnection();
|
||||
}
|
||||
@ -379,7 +404,7 @@ class Database {
|
||||
};
|
||||
process.addListener("unhandledRejection", listener);
|
||||
|
||||
console.log("Closing the database");
|
||||
log.info("db", "Closing the database");
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
@ -389,10 +414,10 @@ class Database {
|
||||
if (Database.noReject) {
|
||||
break;
|
||||
} else {
|
||||
console.log("Waiting to close the database");
|
||||
log.info("db", "Waiting to close the database");
|
||||
}
|
||||
}
|
||||
console.log("SQLite closed");
|
||||
log.info("db", "SQLite closed");
|
||||
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
}
|
||||
@ -400,11 +425,11 @@ class Database {
|
||||
/**
|
||||
* One backup one time in this process.
|
||||
* Reset this.backupPath if you want to backup again
|
||||
* @param version
|
||||
* @param {string} version Version code of backup
|
||||
*/
|
||||
static backup(version) {
|
||||
if (! this.backupPath) {
|
||||
console.info("Backing up the database");
|
||||
log.info("db", "Backing up the database");
|
||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, this.backupPath);
|
||||
|
||||
@ -422,12 +447,10 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
/** Restore from most recent backup */
|
||||
static restore() {
|
||||
if (this.backupPath) {
|
||||
console.error("Patching the database failed!!! Restoring the backup");
|
||||
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
const walPath = Database.path + "-wal";
|
||||
@ -446,7 +469,7 @@ class Database {
|
||||
fs.unlinkSync(walPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Restore failed; you may need to restore the backup manually");
|
||||
log.error("db", "Restore failed; you may need to restore the backup manually");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -462,17 +485,22 @@ class Database {
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log("Nothing to restore");
|
||||
log.info("db", "Nothing to restore");
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the size of the database */
|
||||
static getSize() {
|
||||
debug("Database.getSize()");
|
||||
log.debug("db", "Database.getSize()");
|
||||
let stats = fs.statSync(Database.path);
|
||||
debug(stats);
|
||||
log.debug("db", stats);
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shrink the database
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async shrink() {
|
||||
await R.exec("VACUUM");
|
||||
}
|
||||
|
@ -3,18 +3,21 @@
|
||||
Modified with 0 dependencies
|
||||
*/
|
||||
let fs = require("fs");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
let ImageDataURI = (() => {
|
||||
|
||||
/**
|
||||
* @param {string} dataURI - A string that is a valid Data URI.
|
||||
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Decode the data:image/ URI
|
||||
* @param {string} dataURI data:image/ URI to decode
|
||||
* @returns {?Object} An object with properties "imageType" and "dataBase64".
|
||||
* The former is the image type, e.g., "png", and the latter is a base64
|
||||
* encoded string of the image's binary data. If it fails to parse, returns
|
||||
* null instead of an object.
|
||||
*/
|
||||
function decode(dataURI) {
|
||||
if (!/data:image\//.test(dataURI)) {
|
||||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||
log.error("image-data-uri", "It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -27,15 +30,15 @@ let ImageDataURI = (() => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data - The image data to be encoded.
|
||||
* @param {String} mediaType - The type of the image, e.g., "image/png".
|
||||
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Endcode an image into data:image/ URI
|
||||
* @param {(Buffer|string)} data Data to encode
|
||||
* @param {string} mediaType Media type of data
|
||||
* @returns {(string|null)} A string representing the base64-encoded
|
||||
* version of the given Buffer object or null if an error occurred.
|
||||
*/
|
||||
function encode(data, mediaType) {
|
||||
if (!data || !mediaType) {
|
||||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
||||
log.error("image-data-uri", "Missing some of the required params: data, mediaType");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -47,11 +50,10 @@ let ImageDataURI = (() => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a data URI to a file path.
|
||||
* @param {string} dataURI The Data URI of the image.
|
||||
* @param {string} [filePath] The path where the image will be saved, defaults to "./".
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Write data URI to file
|
||||
* @param {string} dataURI data:image/ URI
|
||||
* @param {string} [filePath] Path to write file to
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function outputFile(dataURI, filePath) {
|
||||
filePath = filePath || "./";
|
||||
|
@ -1,6 +1,7 @@
|
||||
const path = require("path");
|
||||
const Bree = require("bree");
|
||||
const { SHARE_ENV } = require("worker_threads");
|
||||
const { log } = require("../src/util");
|
||||
let bree;
|
||||
const jobs = [
|
||||
{
|
||||
@ -9,6 +10,11 @@ const jobs = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize background jobs
|
||||
* @param {Object} args Arguments to pass to workers
|
||||
* @returns {Bree}
|
||||
*/
|
||||
const initBackgroundJobs = function (args) {
|
||||
bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
@ -18,7 +24,7 @@ const initBackgroundJobs = function (args) {
|
||||
workerData: args,
|
||||
},
|
||||
workerMessageHandler: (message) => {
|
||||
console.log("[Background Job]:", message);
|
||||
log.info("jobs", message);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -30,7 +30,7 @@ const DEFAULT_KEEP_PERIOD = 180;
|
||||
try {
|
||||
await R.exec(
|
||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||
[parsedPeriod]
|
||||
[ parsedPeriod ]
|
||||
);
|
||||
} catch (e) {
|
||||
log(`Failed to clear old data: ${e.message}`);
|
||||
|
@ -2,14 +2,24 @@ const { parentPort, workerData } = require("worker_threads");
|
||||
const Database = require("../database");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Send message to parent process for logging
|
||||
* since worker_thread does not have access to stdout, this is used
|
||||
* instead of console.log()
|
||||
* @param {any} any The message to log
|
||||
*/
|
||||
const log = function (any) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(any);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exit the worker process
|
||||
* @param {number} error The status code to exit
|
||||
*/
|
||||
const exit = function (error) {
|
||||
if (error && error != 0) {
|
||||
if (error && error !== 0) {
|
||||
process.exit(error);
|
||||
} else {
|
||||
if (parentPort) {
|
||||
@ -20,6 +30,7 @@ const exit = function (error) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Connects to the database */
|
||||
const connectDb = async function () {
|
||||
const dbPath = path.join(
|
||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||
|
@ -3,6 +3,12 @@ const { R } = require("redbean-node");
|
||||
|
||||
class Group extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
@ -19,6 +25,10 @@ class Group extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all monitors
|
||||
* @returns {Bean[]}
|
||||
*/
|
||||
async getMonitorList() {
|
||||
return R.convertToBeans("monitor", await R.getAll(`
|
||||
SELECT monitor.* FROM monitor, monitor_group
|
||||
|
@ -13,6 +13,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
*/
|
||||
class Heartbeat extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
status: this.status,
|
||||
@ -22,6 +27,10 @@ class Heartbeat extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
monitorID: this.monitor_id,
|
||||
|
@ -2,6 +2,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Incident extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
|
@ -6,8 +6,8 @@ dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server");
|
||||
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
@ -15,6 +15,7 @@ const { Proxy } = require("../proxy");
|
||||
const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@ -27,6 +28,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false) {
|
||||
let obj = {
|
||||
@ -41,8 +43,9 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON() {
|
||||
async toJSON(includeSensitiveData = true) {
|
||||
|
||||
let notificationIDList = {};
|
||||
|
||||
@ -56,15 +59,11 @@ class Monitor extends BeanModel {
|
||||
|
||||
const tags = await this.getTags();
|
||||
|
||||
return {
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers,
|
||||
basic_auth_user: this.basic_auth_user,
|
||||
basic_auth_pass: this.basic_auth_pass,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
@ -89,11 +88,32 @@ class Monitor extends BeanModel {
|
||||
proxyId: this.proxy_id,
|
||||
notificationIDList,
|
||||
tags: tags,
|
||||
mqttUsername: this.mqttUsername,
|
||||
mqttPassword: this.mqttPassword,
|
||||
mqttTopic: this.mqttTopic,
|
||||
mqttSuccessMessage: this.mqttSuccessMessage
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
data = {
|
||||
...data,
|
||||
headers: this.headers,
|
||||
body: this.body,
|
||||
basic_auth_user: this.basic_auth_user,
|
||||
basic_auth_pass: this.basic_auth_pass,
|
||||
pushToken: this.pushToken,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags applied to this monitor
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
async getTags() {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,6 +125,10 @@ class Monitor extends BeanModel {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the TLS expiry notification enabled?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEnabledExpiryNotification() {
|
||||
return Boolean(this.expiryNotification);
|
||||
}
|
||||
@ -125,10 +149,18 @@ class Monitor extends BeanModel {
|
||||
return Boolean(this.upsideDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accepted status codes
|
||||
* @returns {Object}
|
||||
*/
|
||||
getAcceptedStatuscodes() {
|
||||
return JSON.parse(this.accepted_statuscodes_json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
*/
|
||||
start(io) {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
@ -154,7 +186,7 @@ class Monitor extends BeanModel {
|
||||
// undefined if not https
|
||||
let tlsInfo = undefined;
|
||||
|
||||
if (! previousBeat) {
|
||||
if (!previousBeat || this.type === "push") {
|
||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||
this.id,
|
||||
]);
|
||||
@ -172,7 +204,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (! isFirstBeat) {
|
||||
if (!isFirstBeat) {
|
||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
||||
} else {
|
||||
bean.duration = 0;
|
||||
@ -196,7 +228,7 @@ class Monitor extends BeanModel {
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
};
|
||||
|
||||
debug(`[${this.name}] Prepare Options for axios`);
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||
|
||||
const options = {
|
||||
url: this.url,
|
||||
@ -233,8 +265,8 @@ class Monitor extends BeanModel {
|
||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||
}
|
||||
|
||||
debug(`[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
debug(`[${this.name}] Axios Request`);
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
let res = await axios.request(options);
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
@ -243,29 +275,30 @@ class Monitor extends BeanModel {
|
||||
// Check certificate if https is used
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
debug(`[${this.name}] Check cert`);
|
||||
log.debug("monitor", `[${this.name}] Check cert`);
|
||||
try {
|
||||
let tlsInfoObject = checkCertificate(res);
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
debug(`[${this.name}] call sendCertNotification`);
|
||||
log.debug("monitor", `[${this.name}] call sendCertNotification`);
|
||||
await this.sendCertNotification(tlsInfoObject);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (e.message !== "No TLS certificate in response") {
|
||||
console.error(e.message);
|
||||
log.error("monitor", "Caught error");
|
||||
log.error("monitor", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
||||
console.log(res.data);
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
||||
log.info("monitor", res.data);
|
||||
}
|
||||
|
||||
if (this.type === "http") {
|
||||
@ -304,24 +337,24 @@ class Monitor extends BeanModel {
|
||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
|
||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
||||
dnsMessage += "Records: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
|
||||
} else if (this.dns_resolve_type === "CNAME" || this.dns_resolve_type === "PTR") {
|
||||
dnsMessage = dnsRes[0];
|
||||
} else if (this.dns_resolve_type == "CAA") {
|
||||
} else if (this.dns_resolve_type === "CAA") {
|
||||
dnsMessage = dnsRes[0].issue;
|
||||
} else if (this.dns_resolve_type == "MX") {
|
||||
} else if (this.dns_resolve_type === "MX") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
} else if (this.dns_resolve_type == "NS") {
|
||||
} else if (this.dns_resolve_type === "NS") {
|
||||
dnsMessage += "Servers: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type == "SOA") {
|
||||
} else if (this.dns_resolve_type === "SOA") {
|
||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||
} else if (this.dns_resolve_type == "SRV") {
|
||||
} else if (this.dns_resolve_type === "SRV") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||
});
|
||||
@ -345,12 +378,9 @@ class Monitor extends BeanModel {
|
||||
time
|
||||
]);
|
||||
|
||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
||||
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
|
||||
|
||||
if (heartbeatCount <= 0) {
|
||||
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
||||
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
||||
|
||||
throw new Error("No heartbeat in the time window");
|
||||
} else {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
@ -376,7 +406,7 @@ class Monitor extends BeanModel {
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
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,
|
||||
validateStatus: (status) => {
|
||||
@ -398,7 +428,6 @@ class Monitor extends BeanModel {
|
||||
} else {
|
||||
throw new Error("Server not found on Steam");
|
||||
}
|
||||
|
||||
} else if (this.type === "docker") {
|
||||
debug(`[${this.name}] Prepare Options for Axios`);
|
||||
|
||||
@ -426,7 +455,14 @@ class Monitor extends BeanModel {
|
||||
bean.status = UP;
|
||||
bean.msg = "";
|
||||
}
|
||||
|
||||
} else if (this.type === "mqtt") {
|
||||
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
||||
port: this.port,
|
||||
username: this.mqttUsername,
|
||||
password: this.mqttPassword,
|
||||
interval: this.interval,
|
||||
});
|
||||
bean.status = UP;
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
@ -457,7 +493,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
debug(`[${this.name}] Check isImportant`);
|
||||
log.debug("monitor", `[${this.name}] Check isImportant`);
|
||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
@ -465,11 +501,11 @@ class Monitor extends BeanModel {
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
debug(`[${this.name}] sendNotification`);
|
||||
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
|
||||
// Clear Status Page Cache
|
||||
debug(`[${this.name}] apicache clear`);
|
||||
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||
apicache.clear();
|
||||
|
||||
} else {
|
||||
@ -477,47 +513,48 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else if (bean.status === PENDING) {
|
||||
if (this.retryInterval > 0) {
|
||||
beatInterval = this.retryInterval;
|
||||
}
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
}
|
||||
|
||||
debug(`[${this.name}] Send to socket`);
|
||||
log.debug("monitor", `[${this.name}] Send to socket`);
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
Monitor.sendStats(io, this.id, this.user_id);
|
||||
|
||||
debug(`[${this.name}] Store`);
|
||||
log.debug("monitor", `[${this.name}] Store`);
|
||||
await R.store(bean);
|
||||
|
||||
debug(`[${this.name}] prometheus.update`);
|
||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
if (! this.isStop) {
|
||||
debug(`[${this.name}] SetTimeout for next check.`);
|
||||
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);
|
||||
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||
} else {
|
||||
console.log(`[${this.name}] isStop = true, no next check.`);
|
||||
log.info("monitor", `[${this.name}] isStop = true, no next check.`);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/** Get a heartbeat and handle errors */
|
||||
const safeBeat = async () => {
|
||||
try {
|
||||
await beat();
|
||||
} catch (e) {
|
||||
console.trace(e);
|
||||
errorLog(e, false);
|
||||
console.error("Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
UptimeKumaServer.errorLog(e, false);
|
||||
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
if (! this.isStop) {
|
||||
console.log("Try to restart the monitor");
|
||||
log.info("monitor", "Try to restart the monitor");
|
||||
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
||||
}
|
||||
}
|
||||
@ -533,6 +570,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop monitor */
|
||||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
@ -540,6 +578,10 @@ class Monitor extends BeanModel {
|
||||
this.prometheus().remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new prometheus instance
|
||||
* @returns {Prometheus}
|
||||
*/
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
}
|
||||
@ -548,7 +590,7 @@ class Monitor extends BeanModel {
|
||||
* Helper Method:
|
||||
* returns URL object for further usage
|
||||
* returns null if url is invalid
|
||||
* @returns {null|URL}
|
||||
* @returns {(null|URL)}
|
||||
*/
|
||||
getUrl() {
|
||||
try {
|
||||
@ -561,48 +603,54 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Store TLS info to database
|
||||
* @param checkCertificateResult
|
||||
* @returns {Promise<object>}
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async updateTlsInfo(checkCertificateResult) {
|
||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
if (tls_info_bean == null) {
|
||||
tls_info_bean = R.dispense("monitor_tls_info");
|
||||
tls_info_bean.monitor_id = this.id;
|
||||
if (tlsInfoBean == null) {
|
||||
tlsInfoBean = R.dispense("monitor_tls_info");
|
||||
tlsInfoBean.monitor_id = this.id;
|
||||
} else {
|
||||
|
||||
// Clear sent history if the cert changed.
|
||||
try {
|
||||
let oldCertInfo = JSON.parse(tls_info_bean.info_json);
|
||||
let oldCertInfo = JSON.parse(tlsInfoBean.info_json);
|
||||
|
||||
let isValidObjects = oldCertInfo && oldCertInfo.certInfo && checkCertificateResult && checkCertificateResult.certInfo;
|
||||
|
||||
if (isValidObjects) {
|
||||
if (oldCertInfo.certInfo.fingerprint256 !== checkCertificateResult.certInfo.fingerprint256) {
|
||||
debug("Resetting sent_history");
|
||||
log.debug("monitor", "Resetting sent_history");
|
||||
await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [
|
||||
this.id
|
||||
]);
|
||||
} else {
|
||||
debug("No need to reset sent_history");
|
||||
debug(oldCertInfo.certInfo.fingerprint256);
|
||||
debug(checkCertificateResult.certInfo.fingerprint256);
|
||||
log.debug("monitor", "No need to reset sent_history");
|
||||
log.debug("monitor", oldCertInfo.certInfo.fingerprint256);
|
||||
log.debug("monitor", checkCertificateResult.certInfo.fingerprint256);
|
||||
}
|
||||
} else {
|
||||
debug("Not valid object");
|
||||
log.debug("monitor", "Not valid object");
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
}
|
||||
|
||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
||||
await R.store(tls_info_bean);
|
||||
tlsInfoBean.info_json = JSON.stringify(checkCertificateResult);
|
||||
await R.store(tlsInfoBean);
|
||||
|
||||
return checkCertificateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send statistics to clients
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendStats(io, monitorID, userID) {
|
||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||
|
||||
@ -612,13 +660,13 @@ class Monitor extends BeanModel {
|
||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||
} else {
|
||||
debug("No clients in the room, no need to send stats");
|
||||
log.debug("monitor", "No clients in the room, no need to send stats");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param duration : int Hours
|
||||
* Send the average ping to user
|
||||
* @param {number} duration Hours
|
||||
*/
|
||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -638,12 +686,18 @@ class Monitor extends BeanModel {
|
||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send certificate information to client
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendCertInfo(io, monitorID, userID) {
|
||||
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
monitorID,
|
||||
]);
|
||||
if (tls_info != null) {
|
||||
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
||||
if (tlsInfo != null) {
|
||||
io.to(userID).emit("certInfo", monitorID, tlsInfo.info_json);
|
||||
}
|
||||
}
|
||||
|
||||
@ -651,7 +705,8 @@ class Monitor extends BeanModel {
|
||||
* Uptime with calculation
|
||||
* Calculation based on:
|
||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||
* @param duration : int Hours
|
||||
* @param {number} duration Hours
|
||||
* @param {number} monitorID ID of monitor to calculate
|
||||
*/
|
||||
static async calcUptime(duration, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -717,13 +772,23 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Send Uptime
|
||||
* @param duration : int Hours
|
||||
* @param {number} duration Hours
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
const uptime = await this.calcUptime(duration, monitorID);
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Has status of monitor changed since last beat?
|
||||
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||
* @param {const} previousBeatStatus Status of the previous beat
|
||||
* @param {const} currentBeatStatus Status of the current beat
|
||||
* @returns {boolean} True if is an important beat else false
|
||||
*/
|
||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
@ -742,6 +807,12 @@ class Monitor extends BeanModel {
|
||||
return isImportant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification about a monitor
|
||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||
* @param {Bean} bean Status information about monitor
|
||||
*/
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
const notificationList = await Monitor.getNotificationList(monitor);
|
||||
@ -757,15 +828,20 @@ class Monitor extends BeanModel {
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of notification providers for a given monitor
|
||||
* @param {Monitor} monitor Monitor to get notification providers for
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
static async getNotificationList(monitor) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
@ -773,21 +849,33 @@ class Monitor extends BeanModel {
|
||||
return notificationList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification about a certificate
|
||||
* @param {Object} tlsInfoObject Information about certificate
|
||||
*/
|
||||
async sendCertNotification(tlsInfoObject) {
|
||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||
const notificationList = await Monitor.getNotificationList(this);
|
||||
|
||||
debug("call sendCertNotificationByTargetDays");
|
||||
log.debug("monitor", "call sendCertNotificationByTargetDays");
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a certificate notification when certificate expires in less
|
||||
* than target days
|
||||
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||
* @param {number} targetDays Number of days to alert after
|
||||
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||
|
||||
if (daysRemaining > targetDays) {
|
||||
debug(`No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||
log.debug("monitor", `No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -801,21 +889,21 @@ class Monitor extends BeanModel {
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
debug("Sent already, no need to send again");
|
||||
log.debug("monitor", "Sent already, no need to send again");
|
||||
return;
|
||||
}
|
||||
|
||||
let sent = false;
|
||||
debug("Send certificate notification");
|
||||
log.debug("monitor", "Send certificate notification");
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
debug("Sending to " + notification.name);
|
||||
log.debug("monitor", "Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
console.error("Cannot send cert notification to " + notification.name);
|
||||
console.error(e);
|
||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -827,10 +915,15 @@ class Monitor extends BeanModel {
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
debug("No notification, no need to send cert notification");
|
||||
log.debug("monitor", "No notification, no need to send cert notification");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of the previous heartbeat
|
||||
* @param {number} monitorID ID of monitor to check
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
static async getPreviousHeartbeat(monitorID) {
|
||||
return await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
|
@ -1,6 +1,10 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Proxy extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this._id,
|
||||
|
@ -6,6 +6,7 @@ class StatusPage extends BeanModel {
|
||||
static domainMappingList = { };
|
||||
|
||||
/**
|
||||
* Loads domain mapping from DB
|
||||
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@ -17,6 +18,12 @@ class StatusPage extends BeanModel {
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send status page list to client
|
||||
* @param {Server} io io Socket server instance
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
*/
|
||||
static async sendStatusPageList(io, socket) {
|
||||
let result = {};
|
||||
|
||||
@ -30,6 +37,11 @@ class StatusPage extends BeanModel {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update list of domain names
|
||||
* @param {string[]} domainNameList
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateDomainNameList(domainNameList) {
|
||||
|
||||
if (!Array.isArray(domainNameList)) {
|
||||
@ -69,6 +81,10 @@ class StatusPage extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of domain names
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
getDomainNameList() {
|
||||
let domainList = [];
|
||||
for (let domain in StatusPage.domainMappingList) {
|
||||
@ -81,6 +97,10 @@ class StatusPage extends BeanModel {
|
||||
return domainList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
@ -92,9 +112,17 @@ class StatusPage extends BeanModel {
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
domainNameList: this.getDomainNameList(),
|
||||
customCSS: this.custom_css,
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
@ -104,15 +132,26 @@ class StatusPage extends BeanModel {
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
customCSS: this.custom_css,
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert slug to status page ID
|
||||
* @param {string} slug
|
||||
*/
|
||||
static async slugToID(slug) {
|
||||
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to the icon for the page
|
||||
* @returns {string}
|
||||
*/
|
||||
getIcon() {
|
||||
if (!this.icon) {
|
||||
return "/icon.svg";
|
||||
|
@ -1,6 +1,11 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Tag extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this._id,
|
||||
|
@ -3,19 +3,30 @@ const passwordHash = require("../password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class User extends BeanModel {
|
||||
/**
|
||||
* Reset user password
|
||||
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
||||
* @param {number} userID ID of user to update
|
||||
* @param {string} newPassword
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async resetPassword(userID, newPassword) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(newPassword),
|
||||
userID
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct execute, no need R.store()
|
||||
* @param newPassword
|
||||
* Reset this users password
|
||||
* @param {string} newPassword
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(newPassword),
|
||||
this.id
|
||||
]);
|
||||
await User.resetPassword(this.id, newPassword);
|
||||
this.password = newPassword;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
|
@ -13,27 +13,49 @@ let t = {
|
||||
|
||||
let instances = [];
|
||||
|
||||
/**
|
||||
* Does a === b
|
||||
* @param {any} a
|
||||
* @returns {function(any): boolean}
|
||||
*/
|
||||
let matches = function (a) {
|
||||
return function (b) {
|
||||
return a === b;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Does a!==b
|
||||
* @param {any} a
|
||||
* @returns {function(any): boolean}
|
||||
*/
|
||||
let doesntMatch = function (a) {
|
||||
return function (b) {
|
||||
return !matches(a)(b);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get log duration
|
||||
* @param {number} d Time in ms
|
||||
* @param {string} prefix Prefix for log
|
||||
* @returns {string} Coloured log string
|
||||
*/
|
||||
let logDuration = function (d, prefix) {
|
||||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get safe headers
|
||||
* @param {Object} res Express response object
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getSafeHeaders(res) {
|
||||
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||
}
|
||||
|
||||
/** Constructor for ApiCache instance */
|
||||
function ApiCache() {
|
||||
let memCache = new MemoryCache();
|
||||
|
||||
@ -70,10 +92,10 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Logs a message to the console if the `DEBUG` environment variable is set.
|
||||
* @param {string} a - The first argument to log.
|
||||
* @param {string} b - The second argument to log.
|
||||
* @param {string} c - The third argument to log.
|
||||
* @param {string} d - The fourth argument to log, and so on... (optional)
|
||||
* @param {string} a The first argument to log.
|
||||
* @param {string} b The second argument to log.
|
||||
* @param {string} c The third argument to log.
|
||||
* @param {string} d The fourth argument to log, and so on... (optional)
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@ -90,8 +112,8 @@ function ApiCache() {
|
||||
* Returns true if the given request and response should be logged.
|
||||
* @param {Object} request The HTTP request object.
|
||||
* @param {Object} response The HTTP response object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @param {function(Object, Object):boolean} toggle
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldCacheResponse(request, response, toggle) {
|
||||
let opt = globalOptions;
|
||||
@ -116,10 +138,9 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key to the index.
|
||||
* @param {string} key The key to add.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Add key to index array
|
||||
* @param {string} key Key to add
|
||||
* @param {Object} req Express request object
|
||||
*/
|
||||
function addIndexEntries(key, req) {
|
||||
let groupName = req.apicacheGroup;
|
||||
@ -135,8 +156,11 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Returns a new object containing only the whitelisted headers.
|
||||
* @param {Object} headers The original object of header names and values.
|
||||
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
|
||||
* @param {Object} headers The original object of header names and
|
||||
* values.
|
||||
* @param {string[]} globalOptions.headerWhitelist An array of
|
||||
* strings representing the whitelisted header names to keep in the
|
||||
* output object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@ -152,8 +176,10 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache object
|
||||
* @param {Object} headers The response headers to filter.
|
||||
* @returns {Object} A new object containing only the whitelisted response headers.
|
||||
* @returns {Object} A new object containing only the whitelisted
|
||||
* response headers.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@ -170,8 +196,9 @@ function ApiCache() {
|
||||
/**
|
||||
* Sets a cache value for the given key.
|
||||
* @param {string} key The cache key to set.
|
||||
* @param {*} value The cache value to set.
|
||||
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
|
||||
* @param {any} value The cache value to set.
|
||||
* @param {number} duration How long in milliseconds the cached
|
||||
* response should be valid for (defaults to 1 hour).
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@ -199,7 +226,8 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Appends content to the response.
|
||||
* @param {string|Buffer} content The content to append.
|
||||
* @param {Object} res Express response object
|
||||
* @param {(string|Buffer)} content The content to append.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@ -229,11 +257,15 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Monkeypatches the response object to add cache control headers and create a cache object.
|
||||
* @param {Object} req - The request object.
|
||||
* @param {Object} res - The response object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Monkeypatches the response object to add cache control headers
|
||||
* and create a cache object.
|
||||
* @param {Object} req Express request object
|
||||
* @param {Object} res Express response object
|
||||
* @param {function} next Function to call next
|
||||
* @param {string} key Key to add response as
|
||||
* @param {number} duration Time to cache response for
|
||||
* @param {string} strDuration Duration in string form
|
||||
* @param {function(Object, Object):boolean} toggle
|
||||
*/
|
||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||
// monkeypatch res.end to create cache object
|
||||
@ -302,11 +334,15 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {Response} response
|
||||
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Send a cached response to client
|
||||
* @param {Request} request Express request object
|
||||
* @param {Response} response Express response object
|
||||
* @param {object} cacheObject Cache object to send
|
||||
* @param {function(Object, Object):boolean} toggle
|
||||
* @param {function} next Function to call next
|
||||
* @param {number} duration Not used
|
||||
* @returns {boolean|undefined} true if the request should be
|
||||
* cached, false otherwise. If undefined, defaults to true.
|
||||
*/
|
||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||
if (toggle && !toggle(request, response)) {
|
||||
@ -348,12 +384,19 @@ function ApiCache() {
|
||||
return response.end(data, cacheObject.encoding);
|
||||
}
|
||||
|
||||
/** Sync caching options */
|
||||
function syncOptions() {
|
||||
for (let i in middlewareOptions) {
|
||||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear key from cache
|
||||
* @param {string} target Key to clear
|
||||
* @param {boolean} isAutomatic Is the key being cleared automatically
|
||||
* @returns {number}
|
||||
*/
|
||||
this.clear = function (target, isAutomatic) {
|
||||
let group = index.groups[target];
|
||||
let redis = globalOptions.redisClient;
|
||||
@ -430,10 +473,11 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Converts a duration string to an integer number of milliseconds.
|
||||
* @param {string} duration - The string to convert.
|
||||
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @param {(string|number)} duration The string to convert.
|
||||
* @param {number} defaultDuration The default duration to return if
|
||||
* can't parse duration
|
||||
* @returns {number} The converted value in milliseconds, or the
|
||||
* defaultDuration if it can't be parsed.
|
||||
*/
|
||||
function parseDuration(duration, defaultDuration) {
|
||||
if (typeof duration === "number") {
|
||||
@ -457,17 +501,24 @@ function ApiCache() {
|
||||
return defaultDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse duration
|
||||
* @param {(number|string)} duration
|
||||
* @returns {number} Duration parsed to a number
|
||||
*/
|
||||
this.getDuration = function (duration) {
|
||||
return parseDuration(duration, globalOptions.defaultDuration);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
||||
* Return cache performance statistics (hit rate). Suitable for
|
||||
* putting into a route:
|
||||
* <code>
|
||||
* app.get('/api/cache/performance', (req, res) => {
|
||||
* res.json(apicache.getPerformance())
|
||||
* })
|
||||
* </code>
|
||||
* @returns {any[]}
|
||||
*/
|
||||
this.getPerformance = function () {
|
||||
return performanceArray.map(function (p) {
|
||||
@ -475,6 +526,11 @@ function ApiCache() {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get index of a group
|
||||
* @param {string} group
|
||||
* @returns {number}
|
||||
*/
|
||||
this.getIndex = function (group) {
|
||||
if (group) {
|
||||
return index.groups[group];
|
||||
@ -483,6 +539,14 @@ function ApiCache() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Express middleware
|
||||
* @param {(string|number)} strDuration Duration to cache responses
|
||||
* for.
|
||||
* @param {function(Object, Object):boolean} middlewareToggle
|
||||
* @param {Object} localOptions Options for APICache
|
||||
* @returns
|
||||
*/
|
||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||
let duration = instance.getDuration(strDuration);
|
||||
let opt = {};
|
||||
@ -506,63 +570,72 @@ function ApiCache() {
|
||||
options(localOptions);
|
||||
|
||||
/**
|
||||
* A Function for non tracking performance
|
||||
*/
|
||||
* A Function for non tracking performance
|
||||
*/
|
||||
function NOOPCachePerformance() {
|
||||
this.report = this.hit = this.miss = function () {}; // noop;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
||||
*/
|
||||
* A function for tracking and reporting hit rate. These
|
||||
* statistics are returned by the getPerformance() call above.
|
||||
*/
|
||||
function CachePerformance() {
|
||||
/**
|
||||
* Tracks the hit rate for the last 100 requests.
|
||||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 100 requests. If there
|
||||
* have been fewer than 100 requests, the hit rate just
|
||||
* considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 1000 requests.
|
||||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 1000 requests. If there
|
||||
* have been fewer than 1000 requests, the hit rate just
|
||||
* considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 10000 requests.
|
||||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 10000 requests. If there
|
||||
* have been fewer than 10000 requests, the hit rate just
|
||||
* considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 100000 requests.
|
||||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 100000 requests. If
|
||||
* there have been fewer than 100000 requests, the hit rate
|
||||
* just considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* The number of calls that have passed through the middleware since the server started.
|
||||
*/
|
||||
* The number of calls that have passed through the
|
||||
* middleware since the server started.
|
||||
*/
|
||||
this.callCount = 0;
|
||||
|
||||
/**
|
||||
* The total number of hits since the server started
|
||||
*/
|
||||
* The total number of hits since the server started
|
||||
*/
|
||||
this.hitCount = 0;
|
||||
|
||||
/**
|
||||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
||||
*/
|
||||
* The key from the last cache hit. This is useful in
|
||||
* identifying which route these statistics apply to.
|
||||
*/
|
||||
this.lastCacheHit = null;
|
||||
|
||||
/**
|
||||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
||||
*/
|
||||
* The key from the last cache miss. This is useful in
|
||||
* identifying which route these statistics apply to.
|
||||
*/
|
||||
this.lastCacheMiss = null;
|
||||
|
||||
/**
|
||||
* Return performance statistics
|
||||
*/
|
||||
* Return performance statistics
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.report = function () {
|
||||
return {
|
||||
lastCacheHit: this.lastCacheHit,
|
||||
@ -579,10 +652,13 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes a cache hit rate from an array of hits and misses.
|
||||
* @param {Uint8Array} array An array representing hits and misses.
|
||||
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
||||
*/
|
||||
* Computes a cache hit rate from an array of hits and
|
||||
* misses.
|
||||
* @param {Uint8Array} array An array representing hits and
|
||||
* misses.
|
||||
* @returns {?number} a number between 0 and 1, or null if
|
||||
* the array has no hits or misses
|
||||
*/
|
||||
this.hitRate = function (array) {
|
||||
let hits = 0;
|
||||
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
|
||||
* by the current value of the callCount variable.
|
||||
* @param {Uint8Array} array An array representing hits and misses.
|
||||
* @param {boolean} hit true for a hit, false for a miss
|
||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
||||
* Each hit or miss is encoded as to bits as follows:
|
||||
* 00 means no hit or miss has been recorded in these bits
|
||||
* 01 encodes a hit
|
||||
* 10 encodes a miss
|
||||
*/
|
||||
* Record a hit or miss in the given array. It will be
|
||||
* recorded at a position determined by the current value of
|
||||
* the callCount variable.
|
||||
* @param {Uint8Array} array An array representing hits and
|
||||
* misses.
|
||||
* @param {boolean} hit true for a hit, false for a miss
|
||||
* Each element in the array is 8 bits, and encodes 4
|
||||
* hit/miss records. Each hit or miss is encoded as to bits
|
||||
* as follows: 00 means no hit or miss has been recorded in
|
||||
* these bits 01 encodes a hit 10 encodes a miss
|
||||
*/
|
||||
this.recordHitInArray = function (array, hit) {
|
||||
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||
@ -627,9 +704,11 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Records the hit or miss in the tracking arrays and increments the call count.
|
||||
* @param {boolean} hit true records a hit, false records a miss
|
||||
*/
|
||||
* Records the hit or miss in the tracking arrays and
|
||||
* increments the call count.
|
||||
* @param {boolean} hit true records a hit, false records a
|
||||
* miss
|
||||
*/
|
||||
this.recordHit = function (hit) {
|
||||
this.recordHitInArray(this.hitsLast100, hit);
|
||||
this.recordHitInArray(this.hitsLast1000, hit);
|
||||
@ -642,18 +721,18 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a hit event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache hit
|
||||
*/
|
||||
* Records a hit event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache hit
|
||||
*/
|
||||
this.hit = function (key) {
|
||||
this.recordHit(true);
|
||||
this.lastCacheHit = key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a miss event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache miss
|
||||
*/
|
||||
* Records a miss event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache miss
|
||||
*/
|
||||
this.miss = function (key) {
|
||||
this.recordHit(false);
|
||||
this.lastCacheMiss = key;
|
||||
@ -664,6 +743,13 @@ function ApiCache() {
|
||||
|
||||
performanceArray.push(perf);
|
||||
|
||||
/**
|
||||
* Cache a request
|
||||
* @param {Object} req Express request object
|
||||
* @param {Object} res Express response object
|
||||
* @param {function} next Function to call next
|
||||
* @returns {any}
|
||||
*/
|
||||
let cache = function (req, res, next) {
|
||||
function bypass() {
|
||||
debug("bypass detected, skipping cache.");
|
||||
@ -771,6 +857,11 @@ function ApiCache() {
|
||||
return cache;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process options
|
||||
* @param {Object} options
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.options = function (options) {
|
||||
if (options) {
|
||||
Object.assign(globalOptions, options);
|
||||
@ -791,6 +882,7 @@ function ApiCache() {
|
||||
}
|
||||
};
|
||||
|
||||
/** Reset the index */
|
||||
this.resetIndex = function () {
|
||||
index = {
|
||||
all: [],
|
||||
@ -798,6 +890,11 @@ function ApiCache() {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new instance of ApiCache
|
||||
* @param {Object} config Config to pass
|
||||
* @returns {ApiCache}
|
||||
*/
|
||||
this.newInstance = function (config) {
|
||||
let instance = new ApiCache();
|
||||
|
||||
@ -808,6 +905,7 @@ function ApiCache() {
|
||||
return instance;
|
||||
};
|
||||
|
||||
/** Clone this instance */
|
||||
this.clone = function () {
|
||||
return this.newInstance(this.options());
|
||||
};
|
||||
|
@ -3,6 +3,15 @@ function MemoryCache() {
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key Key to store cache as
|
||||
* @param {any} value Value to store
|
||||
* @param {number} time Time to store for
|
||||
* @param {function(any, string)} timeoutCallback Callback to call in
|
||||
* case of timeout
|
||||
* @returns {Object}
|
||||
*/
|
||||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||
let old = this.cache[key];
|
||||
let instance = this;
|
||||
@ -22,6 +31,11 @@ MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||
return entry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a cache entry
|
||||
* @param {string} key Key to delete
|
||||
* @returns {null}
|
||||
*/
|
||||
MemoryCache.prototype.delete = function (key) {
|
||||
let entry = this.cache[key];
|
||||
|
||||
@ -36,18 +50,32 @@ MemoryCache.prototype.delete = function (key) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get value of key
|
||||
* @param {string} key
|
||||
* @returns {Object}
|
||||
*/
|
||||
MemoryCache.prototype.get = function (key) {
|
||||
let entry = this.cache[key];
|
||||
|
||||
return entry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get value of cache entry
|
||||
* @param {string} key
|
||||
* @returns {any}
|
||||
*/
|
||||
MemoryCache.prototype.getValue = function (key) {
|
||||
let entry = this.get(key);
|
||||
|
||||
return entry && entry.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* @returns {boolean}
|
||||
*/
|
||||
MemoryCache.prototype.clear = function () {
|
||||
Object.keys(this.cache).forEach(function (key) {
|
||||
this.delete(key);
|
||||
|
@ -40,17 +40,17 @@ class Alerta extends NotificationProvider {
|
||||
await axios.post(alertaUrl, postData, config);
|
||||
} else {
|
||||
let datadup = Object.assign( {
|
||||
correlate: ["service_up", "service_down"],
|
||||
correlate: [ "service_up", "service_down" ],
|
||||
event: monitorJSON["type"],
|
||||
group: "uptimekuma-" + monitorJSON["type"],
|
||||
resource: monitorJSON["name"],
|
||||
}, data );
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
datadup.severity = notification.alertaAlertState; // critical
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is down.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
datadup.severity = notification.alertaRecoverState; // cleaned
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is up.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
|
@ -37,6 +37,12 @@ class AliyunSMS extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the SMS notification
|
||||
* @param {BeanModel} notification Notification details
|
||||
* @param {string} msgbody Message template
|
||||
* @returns {boolean} True if successful else false
|
||||
*/
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
PhoneNumbers: notification.phonenumber,
|
||||
@ -64,13 +70,18 @@ class AliyunSMS extends NotificationProvider {
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.Message == "OK") {
|
||||
if (result.data.Message === "OK") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Aliyun request sign */
|
||||
/**
|
||||
* Aliyun request sign
|
||||
* @param {Object} param Parameters object to sign
|
||||
* @param {string} AccessKeySecret Secret key to sign parameters with
|
||||
* @returns {string}
|
||||
*/
|
||||
sign(param, AccessKeySecret) {
|
||||
let param2 = {};
|
||||
let data = [];
|
||||
@ -82,8 +93,23 @@ class AliyunSMS extends NotificationProvider {
|
||||
param2[key] = param[key];
|
||||
}
|
||||
|
||||
// Escape more characters than encodeURIComponent does.
|
||||
// For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded.
|
||||
// See https://help.aliyun.com/document_detail/315526.html
|
||||
// This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986)
|
||||
let moreEscapesTable = function (m) {
|
||||
return {
|
||||
"!": "%21",
|
||||
"*": "%2A",
|
||||
"'": "%27",
|
||||
"(": "%28",
|
||||
")": "%29"
|
||||
}[m];
|
||||
};
|
||||
|
||||
for (let key in param2) {
|
||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||
let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable);
|
||||
data.push(`${encodeURIComponent(key)}=${value}`);
|
||||
}
|
||||
|
||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||
@ -93,6 +119,11 @@ class AliyunSMS extends NotificationProvider {
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert status constant to string
|
||||
* @param {const} status The status constant
|
||||
* @returns {string}
|
||||
*/
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
|
@ -1,14 +1,19 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const child_process = require("child_process");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
class Apprise extends NotificationProvider {
|
||||
|
||||
name = "apprise";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||
if (notification.title) {
|
||||
args.push("-t");
|
||||
args.push(notification.title);
|
||||
}
|
||||
const s = childProcess.spawnSync("apprise", args);
|
||||
|
||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
|
||||
@ -16,7 +21,7 @@ class Apprise extends NotificationProvider {
|
||||
return "Sent Successfully";
|
||||
}
|
||||
|
||||
throw new Error(output)
|
||||
throw new Error(output);
|
||||
} else {
|
||||
return "No output from apprise";
|
||||
}
|
||||
|
@ -21,35 +21,35 @@ class Bark extends NotificationProvider {
|
||||
name = "Bark";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
var barkEndpoint = notification.barkEndpoint;
|
||||
let barkEndpoint = notification.barkEndpoint;
|
||||
|
||||
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||
if (barkEndpoint.endsWith("/")) {
|
||||
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||
}
|
||||
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||
if (barkEndpoint.endsWith("/")) {
|
||||
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||
}
|
||||
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == UP) {
|
||||
let title = "UptimeKuma Monitor Up";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
let title = "UptimeKuma Monitor Up";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
||||
let title = "UptimeKuma Monitor Down";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||
let title = "UptimeKuma Monitor Down";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
let title = "UptimeKuma Message";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (msg != null) {
|
||||
let title = "UptimeKuma Message";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// add additional parameter for better on device styles (iOS 15 optimized)
|
||||
/**
|
||||
* Add additional parameter for better on device styles (iOS 15
|
||||
* optimized)
|
||||
* @param {string} postUrl URL to append parameters to
|
||||
* @returns {string}
|
||||
*/
|
||||
appendAdditionalParameters(postUrl) {
|
||||
// grouping all our notifications
|
||||
postUrl += "?group=" + barkNotificationGroup;
|
||||
@ -60,7 +60,11 @@ class Bark extends NotificationProvider {
|
||||
return postUrl;
|
||||
}
|
||||
|
||||
// thrown if failed to check result, result code should be in range 2xx
|
||||
/**
|
||||
* Check if result is successful
|
||||
* @param {Object} result Axios response object
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
if (result.status == null) {
|
||||
throw new Error("Bark notification failed with invalid response!");
|
||||
@ -70,6 +74,13 @@ class Bark extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {string} title Message title
|
||||
* @param {string} subtitle Message
|
||||
* @param {string} endpoint Endpoint to send request to
|
||||
* @returns {string}
|
||||
*/
|
||||
async postNotification(title, subtitle, endpoint) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
|
@ -12,7 +12,7 @@ class ClickSendSMS extends NotificationProvider {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString('base64'),
|
||||
"Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString("base64"),
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
|
@ -37,6 +37,12 @@ class DingDing extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to DingDing
|
||||
* @param {BeanModel} notification
|
||||
* @param {Object} params Parameters of message
|
||||
* @returns {boolean} True if successful else false
|
||||
*/
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
|
||||
@ -50,13 +56,18 @@ class DingDing extends NotificationProvider {
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.errmsg == "ok") {
|
||||
if (result.data.errmsg === "ok") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** DingDing sign */
|
||||
/**
|
||||
* DingDing sign
|
||||
* @param {Date} timestamp Timestamp of message
|
||||
* @param {string} secretKey Secret key to sign data with
|
||||
* @returns {string}
|
||||
*/
|
||||
sign(timestamp, secretKey) {
|
||||
return Crypto
|
||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||
@ -64,7 +75,13 @@ class DingDing extends NotificationProvider {
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert status constant to string
|
||||
* @param {const} status The status constant
|
||||
* @returns {string}
|
||||
*/
|
||||
statusToString(status) {
|
||||
// TODO: Move to notification-provider.js to avoid repetition in classes
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
|
@ -17,25 +17,32 @@ class Discord extends NotificationProvider {
|
||||
let discordtestdata = {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata)
|
||||
};
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
let address;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
address = monitorJSON["url"];
|
||||
break;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let discorddowndata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url,
|
||||
name: "Service URL / Address",
|
||||
value: address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
@ -61,16 +68,16 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discorddowndata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata);
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let discordupdata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
@ -84,7 +91,7 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
||||
value: address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
@ -96,17 +103,17 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discordupdata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata)
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ class Feishu extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let downdata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
@ -48,7 +48,7 @@ class Feishu extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == UP) {
|
||||
if (heartbeatJSON["status"] === UP) {
|
||||
let updata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
|
@ -13,11 +13,11 @@ class GoogleChat extends NotificationProvider {
|
||||
try {
|
||||
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
|
||||
|
||||
let textMsg = ''
|
||||
let textMsg = "";
|
||||
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||
textMsg = `✅ Application is back online\n`;
|
||||
textMsg = "✅ Application is back online\n";
|
||||
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||
textMsg = `🔴 Application went down\n`;
|
||||
textMsg = "🔴 Application went down\n";
|
||||
}
|
||||
|
||||
if (monitorJSON && monitorJSON.name) {
|
||||
|
@ -18,7 +18,7 @@ class Gorush extends NotificationProvider {
|
||||
let data = {
|
||||
"notifications": [
|
||||
{
|
||||
"tokens": [notification.gorushDeviceToken],
|
||||
"tokens": [ notification.gorushDeviceToken ],
|
||||
"platform": platformMapping[notification.gorushPlatform],
|
||||
"message": msg,
|
||||
// Optional
|
||||
|
@ -15,7 +15,7 @@ class Gotify extends NotificationProvider {
|
||||
"message": msg,
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma",
|
||||
})
|
||||
});
|
||||
|
||||
return okMsg;
|
||||
|
||||
|
@ -25,9 +25,9 @@ class Line extends NotificationProvider {
|
||||
"text": "Test Successful!"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, testMessage, config)
|
||||
} else if (heartbeatJSON["status"] == DOWN) {
|
||||
};
|
||||
await axios.post(lineAPIUrl, testMessage, config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
@ -36,9 +36,9 @@ class Line extends NotificationProvider {
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, downMessage, config)
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
};
|
||||
await axios.post(lineAPIUrl, downMessage, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
@ -47,12 +47,12 @@ class Line extends NotificationProvider {
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, upMessage, config)
|
||||
};
|
||||
await axios.post(lineAPIUrl, upMessage, config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,38 +8,38 @@ class LunaSea extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(lunaseadevice, testdata)
|
||||
};
|
||||
await axios.post(lunaseadevice, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, downdata)
|
||||
};
|
||||
await axios.post(lunaseadevice, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == UP) {
|
||||
if (heartbeatJSON["status"] === UP) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, updata)
|
||||
};
|
||||
await axios.post(lunaseadevice, updata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
const { debug } = require("../../src/util");
|
||||
const { log } = require("../../src/util");
|
||||
|
||||
class Matrix extends NotificationProvider {
|
||||
name = "matrix";
|
||||
@ -17,11 +17,11 @@ class Matrix extends NotificationProvider {
|
||||
.slice(0, size)
|
||||
);
|
||||
|
||||
debug("Random String: " + randomString);
|
||||
log.debug("notification", "Random String: " + randomString);
|
||||
|
||||
const roomId = encodeURIComponent(notification.internalRoomId);
|
||||
|
||||
debug("Matrix Room ID: " + roomId);
|
||||
log.debug("notification", "Matrix Room ID: " + roomId);
|
||||
|
||||
try {
|
||||
let config = {
|
||||
|
@ -29,7 +29,7 @@ class Mattermost extends NotificationProvider {
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
@ -73,7 +73,7 @@ class Mattermost extends NotificationProvider {
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
|
@ -7,17 +7,23 @@ class NotificationProvider {
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
* @param notification : BeanModel
|
||||
* @param msg : string General Message
|
||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
||||
* Send a notification
|
||||
* @param {BeanModel} notification
|
||||
* @param {string} msg General Message
|
||||
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Return Successful Message
|
||||
* Throw Error with fail msg
|
||||
* @throws Error with fail msg
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
throw new Error("Have to override Notification.send(...)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error
|
||||
* @param {any} error The error to throw
|
||||
* @throws {any} The error specified
|
||||
*/
|
||||
throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
|
||||
@ -25,11 +31,11 @@ class NotificationProvider {
|
||||
if (typeof error.response.data === "string") {
|
||||
msg += error.response.data;
|
||||
} else {
|
||||
msg += JSON.stringify(error.response.data)
|
||||
msg += JSON.stringify(error.response.data);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(msg)
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ class Octopush extends NotificationProvider {
|
||||
|
||||
try {
|
||||
// Default - V2
|
||||
if (notification.octopushVersion == 2 || !notification.octopushVersion) {
|
||||
if (notification.octopushVersion === 2 || !notification.octopushVersion) {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
@ -30,14 +30,14 @@ class Octopush extends NotificationProvider {
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
} else if (notification.octopushVersion == 1) {
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
|
||||
} else if (notification.octopushVersion === 1) {
|
||||
let data = {
|
||||
"user_login": notification.octopushDMLogin,
|
||||
"api_key": notification.octopushDMAPIKey,
|
||||
"sms_recipients": notification.octopushDMPhoneNumber,
|
||||
"sms_sender": notification.octopushDMSenderName,
|
||||
"sms_type": (notification.octopushDMSMSType == "sms_premium") ? "FR" : "XXX",
|
||||
"sms_type": (notification.octopushDMSMSType === "sms_premium") ? "FR" : "XXX",
|
||||
"transactional": "1",
|
||||
//octopush not supporting non ascii char
|
||||
"sms_text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
@ -49,7 +49,7 @@ class Octopush extends NotificationProvider {
|
||||
},
|
||||
params: data
|
||||
};
|
||||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config)
|
||||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
||||
} else {
|
||||
throw new Error("Unknown Octopush version!");
|
||||
}
|
||||
|
45
server/notification-providers/onebot.js
Normal file
45
server/notification-providers/onebot.js
Normal file
@ -0,0 +1,45 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class OneBot extends NotificationProvider {
|
||||
|
||||
name = "OneBot";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let httpAddr = notification.httpAddr;
|
||||
if (!httpAddr.startsWith("http")) {
|
||||
httpAddr = "http://" + httpAddr;
|
||||
}
|
||||
if (!httpAddr.endsWith("/")) {
|
||||
httpAddr += "/";
|
||||
}
|
||||
let onebotAPIUrl = httpAddr + "send_msg";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notification.accessToken,
|
||||
}
|
||||
};
|
||||
let pushText = "UptimeKuma Alert: " + msg;
|
||||
let data = {
|
||||
"auto_escape": true,
|
||||
"message": pushText,
|
||||
};
|
||||
if (notification.msgType === "group") {
|
||||
data["message_type"] = "group";
|
||||
data["group_id"] = notification.recieverId;
|
||||
} else {
|
||||
data["message_type"] = "private";
|
||||
data["user_id"] = notification.recieverId;
|
||||
}
|
||||
await axios.post(onebotAPIUrl, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OneBot;
|
@ -12,7 +12,7 @@ class PromoSMS extends NotificationProvider {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString('base64'),
|
||||
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString("base64"),
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
|
@ -23,26 +23,26 @@ class Pushbullet extends NotificationProvider {
|
||||
"type": "note",
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(pushbulletUrl, testdata, config)
|
||||
} else if (heartbeatJSON["status"] == DOWN) {
|
||||
};
|
||||
await axios.post(pushbulletUrl, testdata, config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downdata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, downdata, config)
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
};
|
||||
await axios.post(pushbulletUrl, downdata, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let updata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, updata, config)
|
||||
};
|
||||
await axios.post(pushbulletUrl, updata, config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
52
server/notification-providers/pushdeer.js
Normal file
52
server/notification-providers/pushdeer.js
Normal file
@ -0,0 +1,52 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class PushDeer extends NotificationProvider {
|
||||
|
||||
name = "PushDeer";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let pushdeerlink = "https://api2.pushdeer.com/message/push";
|
||||
|
||||
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
||||
|
||||
let title;
|
||||
if (valid && heartbeatJSON.status === UP) {
|
||||
title = "## Uptime Kuma: " + monitorJSON.name + " up";
|
||||
} else if (valid && heartbeatJSON.status === DOWN) {
|
||||
title = "## Uptime Kuma: " + monitorJSON.name + " down";
|
||||
} else {
|
||||
title = "## Uptime Kuma Message";
|
||||
}
|
||||
|
||||
let data = {
|
||||
"pushkey": notification.pushdeerKey,
|
||||
"text": title,
|
||||
"desp": msg.replace(/\n/g, "\n\n"),
|
||||
"type": "markdown",
|
||||
};
|
||||
|
||||
try {
|
||||
let res = await axios.post(pushdeerlink, data);
|
||||
|
||||
if ("error" in res.data) {
|
||||
let error = res.data.error;
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
if (res.data.content.result.length === 0) {
|
||||
let error = "Invalid PushDeer key";
|
||||
this.throwGeneralAxiosError(error);
|
||||
} else if (JSON.parse(res.data.content.result[0]).success !== "ok") {
|
||||
let error = "Unknown error";
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PushDeer;
|
@ -19,10 +19,10 @@ class Pushy extends NotificationProvider {
|
||||
"badge": 1,
|
||||
"sound": "ping.aiff"
|
||||
}
|
||||
})
|
||||
});
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const Slack = require("./slack");
|
||||
const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
|
||||
|
||||
class RocketChat extends NotificationProvider {
|
||||
|
||||
|
@ -16,10 +16,10 @@ class Signal extends NotificationProvider {
|
||||
};
|
||||
let config = {};
|
||||
|
||||
await axios.post(notification.signalURL, data, config)
|
||||
await axios.post(notification.signalURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ class Slack extends NotificationProvider {
|
||||
/**
|
||||
* Deprecated property notification.slackbutton
|
||||
* Set it as primary base url if this is not yet set.
|
||||
* @param {string} url The primary base URL to use
|
||||
*/
|
||||
static async deprecateURL(url) {
|
||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
||||
|
@ -1,6 +1,6 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { DOWN } = require("../../src/util");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
|
@ -5,6 +5,12 @@ const { DOWN, UP } = require("../../src/util");
|
||||
class Teams extends NotificationProvider {
|
||||
name = "teams";
|
||||
|
||||
/**
|
||||
* Generate the message to send
|
||||
* @param {const} status The status constant
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @returns {string}
|
||||
*/
|
||||
_statusMessageFactory = (status, monitorName) => {
|
||||
if (status === DOWN) {
|
||||
return `🔴 Application [${monitorName}] went down`;
|
||||
@ -14,6 +20,11 @@ class Teams extends NotificationProvider {
|
||||
return "Notification";
|
||||
};
|
||||
|
||||
/**
|
||||
* Select theme color to use based on status
|
||||
* @param {const} status The status constant
|
||||
* @returns {string} Selected color in hex RGB format
|
||||
*/
|
||||
_getThemeColor = (status) => {
|
||||
if (status === DOWN) {
|
||||
return "ff0000";
|
||||
@ -24,6 +35,14 @@ class Teams extends NotificationProvider {
|
||||
return "008cff";
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate payload for notification
|
||||
* @param {const} status The status of the monitor
|
||||
* @param {string} monitorMessage Message to send
|
||||
* @param {string} monitorName Name of monitor affected
|
||||
* @param {string} monitorUrl URL of monitor affected
|
||||
* @returns {Object}
|
||||
*/
|
||||
_notificationPayloadFactory = ({
|
||||
status,
|
||||
monitorMessage,
|
||||
@ -74,10 +93,21 @@ class Teams extends NotificationProvider {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the notification
|
||||
* @param {string} webhookUrl URL to send the request to
|
||||
* @param {Object} payload Payload generated by _notificationPayloadFactory
|
||||
*/
|
||||
_sendNotification = async (webhookUrl, payload) => {
|
||||
await axios.post(webhookUrl, payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a general notification
|
||||
* @param {string} webhookUrl URL to send request to
|
||||
* @param {string} msg Message to send
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||
const payload = this._notificationPayloadFactory({
|
||||
monitorMessage: msg
|
||||
|
@ -12,10 +12,10 @@ class TechulusPush extends NotificationProvider {
|
||||
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
|
||||
"title": "Uptime-Kuma",
|
||||
"body": msg,
|
||||
})
|
||||
});
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,12 @@ class Telegram extends NotificationProvider {
|
||||
chat_id: notification.telegramChatID,
|
||||
text: msg,
|
||||
},
|
||||
})
|
||||
});
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
|
||||
throw new Error(msg)
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description";
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,17 +24,17 @@ class Webhook extends NotificationProvider {
|
||||
|
||||
config = {
|
||||
headers: finalData.getHeaders(),
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, finalData, config)
|
||||
await axios.post(notification.webhookURL, finalData, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,12 +24,18 @@ class WeCom extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the message to send
|
||||
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {string} msg General message
|
||||
* @returns {Object}
|
||||
*/
|
||||
composeMessage(heartbeatJSON, msg) {
|
||||
let title;
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON['status'] == UP) {
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
title = "UptimeKuma Monitor Up";
|
||||
}
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] == DOWN) {
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||
title = "UptimeKuma Monitor Down";
|
||||
}
|
||||
if (msg != null) {
|
||||
|
@ -24,19 +24,23 @@ const Feishu = require("./notification-providers/feishu");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Bark = require("./notification-providers/bark");
|
||||
const { log } = require("../src/util");
|
||||
const SerwerSMS = require("./notification-providers/serwersms");
|
||||
const Stackfield = require("./notification-providers/stackfield");
|
||||
const WeCom = require("./notification-providers/wecom");
|
||||
const GoogleChat = require("./notification-providers/google-chat");
|
||||
const Gorush = require("./notification-providers/gorush");
|
||||
const Alerta = require("./notification-providers/alerta");
|
||||
const OneBot = require("./notification-providers/onebot");
|
||||
const PushDeer = require("./notification-providers/pushdeer");
|
||||
|
||||
class Notification {
|
||||
|
||||
providerList = {};
|
||||
|
||||
/** Initialize the notification providers */
|
||||
static init() {
|
||||
console.log("Prepare Notification Providers");
|
||||
log.info("notification", "Prepare Notification Providers");
|
||||
|
||||
this.providerList = {};
|
||||
|
||||
@ -72,6 +76,8 @@ class Notification {
|
||||
new GoogleChat(),
|
||||
new Gorush(),
|
||||
new Alerta(),
|
||||
new OneBot(),
|
||||
new PushDeer(),
|
||||
];
|
||||
|
||||
for (let item of list) {
|
||||
@ -87,13 +93,13 @@ class Notification {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param notification : BeanModel
|
||||
* @param msg : string General Message
|
||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
||||
* Send a notification
|
||||
* @param {BeanModel} notification
|
||||
* @param {string} msg General Message
|
||||
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Successful msg
|
||||
* Throw Error with fail msg
|
||||
* @throws Error with fail msg
|
||||
*/
|
||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
if (this.providerList[notification.type]) {
|
||||
@ -103,28 +109,35 @@ class Notification {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a notification
|
||||
* @param {Object} notification Notification to save
|
||||
* @param {?number} notificationID ID of notification to update
|
||||
* @param {number} userID ID of user who adds notification
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
static async save(notification, notificationID, userID) {
|
||||
let bean
|
||||
let bean;
|
||||
|
||||
if (notificationID) {
|
||||
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
||||
notificationID,
|
||||
userID,
|
||||
])
|
||||
]);
|
||||
|
||||
if (! bean) {
|
||||
throw new Error("notification not found")
|
||||
throw new Error("notification not found");
|
||||
}
|
||||
|
||||
} else {
|
||||
bean = R.dispense("notification")
|
||||
bean = R.dispense("notification");
|
||||
}
|
||||
|
||||
bean.name = notification.name;
|
||||
bean.user_id = userID;
|
||||
bean.config = JSON.stringify(notification);
|
||||
bean.is_default = notification.isDefault || false;
|
||||
await R.store(bean)
|
||||
await R.store(bean);
|
||||
|
||||
if (notification.applyExisting) {
|
||||
await applyNotificationEveryMonitor(bean.id, userID);
|
||||
@ -133,19 +146,29 @@ class Notification {
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification
|
||||
* @param {number} notificationID ID of notification to delete
|
||||
* @param {number} userID ID of user who created notification
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(notificationID, userID) {
|
||||
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
||||
notificationID,
|
||||
userID,
|
||||
])
|
||||
]);
|
||||
|
||||
if (! bean) {
|
||||
throw new Error("notification not found")
|
||||
throw new Error("notification not found");
|
||||
}
|
||||
|
||||
await R.trash(bean)
|
||||
await R.trash(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if apprise exists
|
||||
* @returns {boolean} Does the command apprise exist?
|
||||
*/
|
||||
static checkApprise() {
|
||||
let commandExistsSync = require("command-exists").sync;
|
||||
let exists = commandExistsSync("apprise");
|
||||
@ -155,11 +178,10 @@ class Notification {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new monitor to the database.
|
||||
* @param {number} userID The ID of the user that owns this monitor.
|
||||
* @param {string} name The name of this monitor.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Apply the notification to every monitor
|
||||
* @param {number} notificationID ID of notification to apply
|
||||
* @param {number} userID ID of user who created notification
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
|
||||
@ -170,17 +192,17 @@ async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
|
||||
monitors[i].id,
|
||||
notificationID,
|
||||
])
|
||||
]);
|
||||
|
||||
if (! checkNotification) {
|
||||
let relation = R.dispense("monitor_notification");
|
||||
relation.monitor_id = monitors[i].id;
|
||||
relation.notification_id = notificationID;
|
||||
await R.store(relation)
|
||||
await R.store(relation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Notification,
|
||||
}
|
||||
};
|
||||
|
@ -2,22 +2,42 @@ const passwordHashOld = require("password-hash");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const saltRounds = 10;
|
||||
|
||||
/**
|
||||
* Hash a password
|
||||
* @param {string} password
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.generate = function (password) {
|
||||
return bcrypt.hashSync(password, saltRounds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify a password against a hash
|
||||
* @param {string} password
|
||||
* @param {string} hash
|
||||
* @returns {boolean} Does the password match the hash?
|
||||
*/
|
||||
exports.verify = function (password, hash) {
|
||||
if (isSHA1(hash)) {
|
||||
return passwordHashOld.verify(password, hash)
|
||||
return passwordHashOld.verify(password, hash);
|
||||
}
|
||||
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the hash a SHA1 hash
|
||||
* @param {string} hash
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isSHA1(hash) {
|
||||
return (typeof hash === "string" && hash.startsWith("sha1"))
|
||||
return (typeof hash === "string" && hash.startsWith("sha1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the hash need to be rehashed?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
exports.needRehash = function (hash) {
|
||||
return isSHA1(hash);
|
||||
}
|
||||
};
|
||||
|
@ -9,11 +9,10 @@ const util = require("./util-server");
|
||||
module.exports = Ping;
|
||||
|
||||
/**
|
||||
* @param {string} host - The host to ping
|
||||
* @param {object} [options] - Options for the ping command
|
||||
* Constructor for ping class
|
||||
* @param {string} host Host to ping
|
||||
* @param {object} [options] Options for the ping command
|
||||
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function Ping(host, options) {
|
||||
if (!host) {
|
||||
@ -82,8 +81,17 @@ function Ping(host, options) {
|
||||
|
||||
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
||||
|
||||
// SEND A PING
|
||||
// ===========
|
||||
/**
|
||||
* Callback for send
|
||||
* @callback pingCB
|
||||
* @param {any} err Any error encountered
|
||||
* @param {number} ms Ping time in ms
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a ping
|
||||
* @param {pingCB} callback Callback to call with results
|
||||
*/
|
||||
Ping.prototype.send = function (callback) {
|
||||
let self = this;
|
||||
callback = callback || function (err, ms) {
|
||||
@ -157,8 +165,10 @@ Ping.prototype.send = function (callback) {
|
||||
}
|
||||
};
|
||||
|
||||
// CALL Ping#send(callback) ON A TIMER
|
||||
// ===================================
|
||||
/**
|
||||
* Ping every interval
|
||||
* @param {pingCB} callback Callback to call with results
|
||||
*/
|
||||
Ping.prototype.start = function (callback) {
|
||||
let self = this;
|
||||
this._i = setInterval(function () {
|
||||
@ -167,8 +177,7 @@ Ping.prototype.start = function (callback) {
|
||||
self.send(callback);
|
||||
};
|
||||
|
||||
// STOP SENDING PINGS
|
||||
// ==================
|
||||
/** Stop sending pings */
|
||||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
@ -177,7 +186,7 @@ Ping.prototype.stop = function () {
|
||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||
* Thank @pemassi
|
||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||
* @param data
|
||||
* @param {any} data
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertOutput(data) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
const PrometheusClient = require("prom-client");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
const commonLabels = [
|
||||
"monitor_name",
|
||||
@ -8,32 +9,35 @@ const commonLabels = [
|
||||
"monitor_port",
|
||||
];
|
||||
|
||||
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
|
||||
const monitorCertDaysRemaining = new PrometheusClient.Gauge({
|
||||
name: "monitor_cert_days_remaining",
|
||||
help: "The number of days remaining until the certificate expires",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
const monitor_cert_is_valid = new PrometheusClient.Gauge({
|
||||
const monitorCertIsValid = new PrometheusClient.Gauge({
|
||||
name: "monitor_cert_is_valid",
|
||||
help: "Is the certificate still valid? (1 = Yes, 0= No)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
const monitor_response_time = new PrometheusClient.Gauge({
|
||||
const monitorResponseTime = new PrometheusClient.Gauge({
|
||||
name: "monitor_response_time",
|
||||
help: "Monitor Response Time (ms)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
const monitor_status = new PrometheusClient.Gauge({
|
||||
const monitorStatus = new PrometheusClient.Gauge({
|
||||
name: "monitor_status",
|
||||
help: "Monitor Status (1 = UP, 0= DOWN)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
class Prometheus {
|
||||
monitorLabelValues = {}
|
||||
monitorLabelValues = {};
|
||||
|
||||
/**
|
||||
* @param {Object} monitor Monitor object to monitor
|
||||
*/
|
||||
constructor(monitor) {
|
||||
this.monitorLabelValues = {
|
||||
monitor_name: monitor.name,
|
||||
@ -44,54 +48,63 @@ class Prometheus {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metrics page
|
||||
* @param {Object} heartbeat Heartbeat details
|
||||
* @param {Object} tlsInfo TLS details
|
||||
*/
|
||||
update(heartbeat, tlsInfo) {
|
||||
|
||||
if (typeof tlsInfo !== "undefined") {
|
||||
try {
|
||||
let is_valid = 0;
|
||||
if (tlsInfo.valid == true) {
|
||||
is_valid = 1;
|
||||
let isValid;
|
||||
if (tlsInfo.valid === true) {
|
||||
isValid = 1;
|
||||
} else {
|
||||
is_valid = 0;
|
||||
isValid = 0;
|
||||
}
|
||||
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid);
|
||||
monitorCertIsValid.set(this.monitorLabelValues, isValid);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (tlsInfo.certInfo != null) {
|
||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||
monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status);
|
||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping);
|
||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitor_response_time.set(this.monitorLabelValues, -1);
|
||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
try {
|
||||
monitor_cert_days_remaining.remove(this.monitorLabelValues);
|
||||
monitor_cert_is_valid.remove(this.monitorLabelValues);
|
||||
monitor_response_time.remove(this.monitorLabelValues);
|
||||
monitor_status.remove(this.monitorLabelValues);
|
||||
monitorCertDaysRemaining.remove(this.monitorLabelValues);
|
||||
monitorCertIsValid.remove(this.monitorLabelValues);
|
||||
monitorResponseTime.remove(this.monitorLabelValues);
|
||||
monitorStatus.remove(this.monitorLabelValues);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ const HttpProxyAgent = require("http-proxy-agent");
|
||||
const HttpsProxyAgent = require("https-proxy-agent");
|
||||
const SocksProxyAgent = require("socks-proxy-agent");
|
||||
const { debug } = require("../src/util");
|
||||
const server = require("./server");
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
|
||||
class Proxy {
|
||||
|
||||
static SUPPORTED_PROXY_PROTOCOLS = ["http", "https", "socks", "socks5", "socks4"]
|
||||
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
|
||||
|
||||
/**
|
||||
* Saves and updates given proxy entity
|
||||
@ -21,7 +21,7 @@ class Proxy {
|
||||
let bean;
|
||||
|
||||
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) {
|
||||
throw new Error("proxy not found");
|
||||
@ -71,14 +71,14 @@ class Proxy {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
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) {
|
||||
throw new Error("proxy not found");
|
||||
}
|
||||
|
||||
// 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
|
||||
await R.trash(bean);
|
||||
@ -151,6 +151,8 @@ class Proxy {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async reloadProxy() {
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
|
||||
|
||||
for (let monitorID in server.monitorList) {
|
||||
@ -172,12 +174,12 @@ class Proxy {
|
||||
*/
|
||||
async function applyProxyEveryMonitor(proxyID, userID) {
|
||||
// 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
|
||||
for (const monitor of monitors) {
|
||||
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 ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,30 @@
|
||||
const { RateLimiter } = require("limiter");
|
||||
const { debug } = require("../src/util");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
class KumaRateLimiter {
|
||||
/**
|
||||
* @param {Object} config Rate limiter configuration object
|
||||
*/
|
||||
constructor(config) {
|
||||
this.errorMessage = config.errorMessage;
|
||||
this.rateLimiter = new RateLimiter(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for pass
|
||||
* @callback passCB
|
||||
* @param {Object} err Too many requests
|
||||
*/
|
||||
|
||||
/**
|
||||
* Should the request be passed through
|
||||
* @param {passCB} callback
|
||||
* @param {number} [num=1] Number of tokens to remove
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async pass(callback, num = 1) {
|
||||
const remainingRequests = await this.removeTokens(num);
|
||||
debug("Rate Limit (remainingRequests):" + remainingRequests);
|
||||
log.info("rate-limit", "remaining requests: " + remainingRequests);
|
||||
if (remainingRequests < 0) {
|
||||
if (callback) {
|
||||
callback({
|
||||
@ -22,6 +37,11 @@ class KumaRateLimiter {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a given number of tokens
|
||||
* @param {number} [num=1] Number of tokens to remove
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async removeTokens(num = 1) {
|
||||
return await this.rateLimiter.removeTokens(num);
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
let express = require("express");
|
||||
const { allowDevAllOrigin, getSettings, setting } = require("../util-server");
|
||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const server = require("../server");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
const { UP, DOWN, flipStatus, log } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { badgeConstants } = require("../config");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
let io = server.io;
|
||||
|
||||
router.get("/api/entry-page", async (request, response) => {
|
||||
@ -33,6 +37,8 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping || null;
|
||||
let statusString = request.query.status || "up";
|
||||
let status = (statusString === "up") ? UP : DOWN;
|
||||
|
||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||
pushToken
|
||||
@ -44,7 +50,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
|
||||
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||
|
||||
let status = UP;
|
||||
if (monitor.isUpsideDown()) {
|
||||
status = flipStatus(status);
|
||||
}
|
||||
@ -62,8 +67,8 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||
}
|
||||
|
||||
debug("PreviousStatus: " + previousStatus);
|
||||
debug("Current Status: " + status);
|
||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
||||
log.debug("router", "Current Status: " + status);
|
||||
|
||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
||||
bean.monitor_id = monitor.id;
|
||||
@ -124,7 +129,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
|
||||
// Public Group List
|
||||
const publicGroupList = [];
|
||||
const showTags = !!statusPage.show_tags;
|
||||
debug("Show Tags???" + showTags);
|
||||
|
||||
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||
statusPage.id
|
||||
]);
|
||||
@ -195,14 +200,187 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Default is published
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isPublished() {
|
||||
return true;
|
||||
}
|
||||
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
||||
allowAllOrigin(response);
|
||||
|
||||
const {
|
||||
label,
|
||||
upLabel = "Up",
|
||||
downLabel = "Down",
|
||||
upColor = badgeConstants.defaultUpColor,
|
||||
downColor = badgeConstants.defaultDownColor,
|
||||
style = badgeConstants.defaultStyle,
|
||||
value, // for demo purpose only
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
const overrideValue = value !== undefined ? parseInt(value) : undefined;
|
||||
|
||||
let publicMonitor = await R.getRow(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND monitor_group.monitor_id = ?
|
||||
AND public = 1
|
||||
`,
|
||||
[ requestedMonitorId ]
|
||||
);
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!publicMonitor) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
||||
|
||||
badgeValues.color = state ? upColor : downColor;
|
||||
badgeValues.message = label ?? state ? upLabel : downLabel;
|
||||
}
|
||||
|
||||
// build the svg based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (request, response) => {
|
||||
allowAllOrigin(response);
|
||||
|
||||
const {
|
||||
label,
|
||||
labelPrefix,
|
||||
labelSuffix = badgeConstants.defaultUptimeLabelSuffix,
|
||||
prefix,
|
||||
suffix = badgeConstants.defaultUptimeValueSuffix,
|
||||
color,
|
||||
labelColor,
|
||||
style = badgeConstants.defaultStyle,
|
||||
value, // for demo purpose only
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
// if no duration is given, set value to 24 (h)
|
||||
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
let publicMonitor = await R.getRow(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND monitor_group.monitor_id = ?
|
||||
AND public = 1
|
||||
`,
|
||||
[ requestedMonitorId ]
|
||||
);
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!publicMonitor) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const uptime = overrideValue ?? await Monitor.calcUptime(
|
||||
requestedDuration,
|
||||
requestedMonitorId
|
||||
);
|
||||
|
||||
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||
const cleanUptime = parseFloat(uptime.toPrecision(4));
|
||||
|
||||
// use a given, custom color or calculate one based on the uptime value
|
||||
badgeValues.color = color ?? percentageToColor(uptime);
|
||||
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||
badgeValues.labelColor = labelColor ?? "";
|
||||
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
|
||||
}
|
||||
|
||||
// build the SVG based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response) => {
|
||||
allowAllOrigin(response);
|
||||
|
||||
const {
|
||||
label,
|
||||
labelPrefix,
|
||||
labelSuffix = badgeConstants.defaultPingLabelSuffix,
|
||||
prefix,
|
||||
suffix = badgeConstants.defaultPingValueSuffix,
|
||||
color = badgeConstants.defaultPingColor,
|
||||
labelColor,
|
||||
style = badgeConstants.defaultStyle,
|
||||
value, // for demo purpose only
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
|
||||
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
||||
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
const publicAvgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND heartbeat.time > DATETIME('now', ? || ' hours')
|
||||
AND heartbeat.ping IS NOT NULL
|
||||
AND public = 1
|
||||
AND heartbeat.monitor_id = ?
|
||||
`,
|
||||
[ -requestedDuration, requestedMonitorId ]
|
||||
));
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!publicAvgPing) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const avgPing = parseInt(overrideValue ?? publicAvgPing);
|
||||
|
||||
badgeValues.color = color;
|
||||
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||
badgeValues.labelColor = labelColor ?? "";
|
||||
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
|
||||
}
|
||||
|
||||
// build the SVG based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Send a 403 response
|
||||
* @param {Object} res Express response object
|
||||
* @param {string} [msg=""] Message to send
|
||||
*/
|
||||
function send403(res, msg = "") {
|
||||
res.status(403).json({
|
||||
"status": "fail",
|
||||
|
377
server/server.js
377
server/server.js
@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Uptime Kuma Server
|
||||
* node "server/server.js"
|
||||
* DO NOT require("./server") in other modules, it likely creates circular dependency!
|
||||
*/
|
||||
console.log("Welcome to Uptime Kuma");
|
||||
|
||||
// Check Node.js Version
|
||||
@ -11,81 +16,63 @@ if (nodeVersion < requiredVersion) {
|
||||
}
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
|
||||
const config = require("./config");
|
||||
|
||||
debug(args);
|
||||
log.info("server", "Welcome to Uptime Kuma");
|
||||
log.debug("server", "Arguments");
|
||||
log.debug("server", args);
|
||||
|
||||
if (! process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
}
|
||||
|
||||
console.log("Node Env: " + process.env.NODE_ENV);
|
||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||
|
||||
console.log("Importing Node libraries");
|
||||
log.info("server", "Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
|
||||
console.log("Importing 3rd-party libraries");
|
||||
debug("Importing express");
|
||||
log.info("server", "Importing 3rd-party libraries");
|
||||
log.debug("server", "Importing express");
|
||||
const express = require("express");
|
||||
debug("Importing socket.io");
|
||||
const { Server } = require("socket.io");
|
||||
debug("Importing redbean-node");
|
||||
log.debug("server", "Importing redbean-node");
|
||||
const { R } = require("redbean-node");
|
||||
debug("Importing jsonwebtoken");
|
||||
log.debug("server", "Importing jsonwebtoken");
|
||||
const jwt = require("jsonwebtoken");
|
||||
debug("Importing http-graceful-shutdown");
|
||||
log.debug("server", "Importing http-graceful-shutdown");
|
||||
const gracefulShutdown = require("http-graceful-shutdown");
|
||||
debug("Importing prometheus-api-metrics");
|
||||
log.debug("server", "Importing prometheus-api-metrics");
|
||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||
debug("Importing compare-versions");
|
||||
log.debug("server", "Importing compare-versions");
|
||||
const compareVersions = require("compare-versions");
|
||||
const { passwordStrength } = require("check-password-strength");
|
||||
|
||||
debug("Importing 2FA Modules");
|
||||
log.debug("server", "Importing 2FA Modules");
|
||||
const notp = require("notp");
|
||||
const base32 = require("thirty-two");
|
||||
|
||||
/**
|
||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||
* @type {UptimeKumaServer}
|
||||
*/
|
||||
class UptimeKumaServer {
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
monitorList = {};
|
||||
entryPage = "dashboard";
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
const server = UptimeKumaServer.getInstance(args);
|
||||
const io = module.exports.io = server.io;
|
||||
const app = server.app;
|
||||
|
||||
async sendMonitorList(socket) {
|
||||
let list = await getMonitorJSONList(socket.userID);
|
||||
io.to(socket.userID).emit("monitorList", list);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
const server = module.exports = new UptimeKumaServer();
|
||||
|
||||
console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
log.info("server", "Importing this project modules");
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
log.debug("server", "Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
Notification.init();
|
||||
|
||||
debug("Importing Proxy");
|
||||
log.debug("server", "Importing Proxy");
|
||||
const { Proxy } = require("./proxy");
|
||||
|
||||
debug("Importing Database");
|
||||
log.debug("server", "Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
log.debug("server", "Importing Background Jobs");
|
||||
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
|
||||
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||
|
||||
@ -94,7 +81,7 @@ const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
console.info("Version: " + checkVersion.version);
|
||||
log.info("server", "Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
@ -103,21 +90,18 @@ let hostEnv = FBSD ? null : process.env.HOST;
|
||||
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
if (hostname) {
|
||||
console.log("Custom hostname: " + hostname);
|
||||
log.info("server", "Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = [args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001]
|
||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
// SSL
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
const disableFrameSameOrigin = args["disable-frame-sameorigin"] || !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || false;
|
||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||
|
||||
// 2FA / notp verification defaults
|
||||
const twofa_verification_opts = {
|
||||
const twoFAVerifyOptions = {
|
||||
"window": 1,
|
||||
"time": 30
|
||||
};
|
||||
@ -129,28 +113,9 @@ const twofa_verification_opts = {
|
||||
const testMode = !!args["test"] || false;
|
||||
|
||||
if (config.demoMode) {
|
||||
console.log("==== Demo Mode ====");
|
||||
log.info("server", "==== Demo Mode ====");
|
||||
}
|
||||
|
||||
console.log("Creating express and socket.io instance");
|
||||
const app = express();
|
||||
|
||||
let httpServer;
|
||||
|
||||
if (sslKey && sslCert) {
|
||||
console.log("Server Type: HTTPS");
|
||||
httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert)
|
||||
}, app);
|
||||
} else {
|
||||
console.log("Server Type: HTTP");
|
||||
httpServer = http.createServer(app);
|
||||
}
|
||||
|
||||
const io = new Server(httpServer);
|
||||
module.exports.io = io;
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
@ -171,12 +136,6 @@ app.use(function (req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Total WebSocket client connected to server currently, no actual use
|
||||
* @type {number}
|
||||
*/
|
||||
let totalClient = 0;
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
@ -200,7 +159,7 @@ try {
|
||||
} catch (e) {
|
||||
// "dist/index.html" is not necessary for development
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
console.error("Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@ -212,7 +171,7 @@ try {
|
||||
exports.entryPage = await setting("entryPage");
|
||||
await StatusPage.loadDomainMappingList();
|
||||
|
||||
console.log("Adding route");
|
||||
log.info("server", "Adding route");
|
||||
|
||||
// ***************************
|
||||
// Normal Router here
|
||||
@ -232,6 +191,13 @@ try {
|
||||
}
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
app.post("/test-webhook", async (request, response) => {
|
||||
log.debug("test", request.body);
|
||||
response.send("OK");
|
||||
});
|
||||
}
|
||||
|
||||
// Robots.txt
|
||||
app.get("/robots.txt", async (_request, response) => {
|
||||
let txt = "User-agent: *\nDisallow:";
|
||||
@ -270,54 +236,55 @@ try {
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Adding socket handler");
|
||||
log.info("server", "Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
sendInfo(socket);
|
||||
|
||||
totalClient++;
|
||||
|
||||
if (needSetup) {
|
||||
console.log("Redirect to setup page");
|
||||
log.info("server", "Redirect to setup page");
|
||||
socket.emit("setup");
|
||||
}
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
totalClient--;
|
||||
});
|
||||
|
||||
// ***************************
|
||||
// Public Socket API
|
||||
// ***************************
|
||||
|
||||
socket.on("loginByToken", async (token, callback) => {
|
||||
log.info("auth", `Login by token. IP=${getClientIp(socket)}`);
|
||||
|
||||
try {
|
||||
let decoded = jwt.verify(token, jwtSecret);
|
||||
|
||||
console.log("Username from JWT: " + decoded.username);
|
||||
log.info("auth", "Username from JWT: " + decoded.username);
|
||||
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
decoded.username,
|
||||
]);
|
||||
|
||||
if (user) {
|
||||
debug("afterLogin");
|
||||
|
||||
log.debug("auth", "afterLogin");
|
||||
afterLogin(socket, user);
|
||||
log.debug("auth", "afterLogin ok");
|
||||
|
||||
debug("afterLogin ok");
|
||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "The user is inactive or deleted.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid token.",
|
||||
@ -327,7 +294,7 @@ try {
|
||||
});
|
||||
|
||||
socket.on("login", async (data, callback) => {
|
||||
console.log("Login");
|
||||
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`);
|
||||
|
||||
// Checking
|
||||
if (typeof callback !== "function") {
|
||||
@ -340,14 +307,18 @@ try {
|
||||
|
||||
// Login Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let user = await login(data.username, data.password);
|
||||
|
||||
if (user) {
|
||||
if (user.twofa_status == 0) {
|
||||
if (user.twofa_status === 0) {
|
||||
afterLogin(socket, user);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
@ -356,14 +327,17 @@ try {
|
||||
});
|
||||
}
|
||||
|
||||
if (user.twofa_status == 1 && !data.token) {
|
||||
if (user.twofa_status === 1 && !data.token) {
|
||||
|
||||
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
tokenRequired: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
|
||||
|
||||
if (user.twofa_last_token !== data.token && verify) {
|
||||
afterLogin(socket, user);
|
||||
@ -373,6 +347,8 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
@ -380,6 +356,9 @@ try {
|
||||
}, jwtSecret),
|
||||
});
|
||||
} else {
|
||||
|
||||
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token!",
|
||||
@ -387,6 +366,9 @@ try {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Incorrect username or password.",
|
||||
@ -422,7 +404,7 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user.twofa_status == 0) {
|
||||
if (user.twofa_status === 0) {
|
||||
let newSecret = genSecret();
|
||||
let encodedSecret = base32.encode(newSecret);
|
||||
|
||||
@ -469,11 +451,16 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "2FA Enabled.",
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
@ -491,11 +478,16 @@ try {
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await TwoFA.disable2FA(socket.userID);
|
||||
|
||||
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "2FA Disabled.",
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
@ -512,7 +504,7 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twoFAVerifyOptions);
|
||||
|
||||
if (user.twofa_last_token !== token && verify) {
|
||||
callback({
|
||||
@ -543,7 +535,7 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user.twofa_status == 1) {
|
||||
if (user.twofa_status === 1) {
|
||||
callback({
|
||||
ok: true,
|
||||
status: true,
|
||||
@ -622,6 +614,8 @@ try {
|
||||
await server.sendMonitorList(socket);
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
|
||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
@ -629,6 +623,9 @@ try {
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
|
||||
log.error("monitor", `Error adding Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
@ -676,6 +673,10 @@ try {
|
||||
bean.docker_daemon = monitor.docker_daemon;
|
||||
bean.docker_type = monitor.docker_type;
|
||||
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
||||
bean.mqttUsername = monitor.mqttUsername;
|
||||
bean.mqttPassword = monitor.mqttPassword;
|
||||
bean.mqttTopic = monitor.mqttTopic;
|
||||
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
@ -694,7 +695,7 @@ try {
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
log.error("monitor", e);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
@ -710,7 +711,7 @@ try {
|
||||
ok: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
log.error("monitor", e);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
@ -722,7 +723,7 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
@ -746,7 +747,7 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`);
|
||||
log.info("monitor", `Get Monitor Beats: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (period == null) {
|
||||
throw new Error("Invalid period.");
|
||||
@ -817,7 +818,7 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
log.info("manage", `Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (monitorID in server.monitorList) {
|
||||
server.monitorList[monitorID].stop();
|
||||
@ -1049,7 +1050,13 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (data.disableAuth) {
|
||||
// If currently is disabled auth, don't need to check
|
||||
// Disabled Auth + Want to Disable Auth => No Check
|
||||
// Disabled Auth + Want to Enable Auth => No Check
|
||||
// Enabled Auth + Want to Disable Auth => Check!!
|
||||
// Enabled Auth + Want to Enable Auth => No Check
|
||||
const currentDisabledAuth = await setting("disableAuth");
|
||||
if (!currentDisabledAuth && data.disableAuth) {
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
@ -1149,7 +1156,7 @@ try {
|
||||
|
||||
let backupData = JSON.parse(uploadedJSON);
|
||||
|
||||
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
||||
log.info("manage", `Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
||||
|
||||
let notificationListData = backupData.notificationList;
|
||||
let proxyListData = backupData.proxyList;
|
||||
@ -1158,7 +1165,7 @@ try {
|
||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
||||
|
||||
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
||||
if (importHandle == "overwrite") {
|
||||
if (importHandle === "overwrite") {
|
||||
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
@ -1182,7 +1189,7 @@ try {
|
||||
|
||||
for (let i = 0; i < notificationListData.length; i++) {
|
||||
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||
if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
|
||||
if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||
|
||||
let notification = JSON.parse(notificationListData[i].config);
|
||||
await Notification.save(notification, null, socket.userID);
|
||||
@ -1192,7 +1199,7 @@ try {
|
||||
}
|
||||
|
||||
// Only starts importing if the backup file contains at least one proxy
|
||||
if (proxyListData.length >= 1) {
|
||||
if (proxyListData && proxyListData.length >= 1) {
|
||||
const proxies = await R.findAll("proxy");
|
||||
|
||||
// Loop over proxy list and save proxies
|
||||
@ -1200,7 +1207,7 @@ try {
|
||||
const exists = proxies.find(item => item.id === proxy.id);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -1217,7 +1224,7 @@ try {
|
||||
|
||||
for (let i = 0; i < monitorListData.length; i++) {
|
||||
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||
if ((importHandle == "skip" && monitorNameListString.includes(monitorListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
|
||||
if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||
|
||||
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
|
||||
// --- Start ---
|
||||
@ -1314,7 +1321,7 @@ try {
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
// If monitor was active start it immediately, otherwise pause it
|
||||
if (monitorListData[i].active == 1) {
|
||||
if (monitorListData[i].active === 1) {
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
} else {
|
||||
await pauseMonitor(socket.userID, bean.id);
|
||||
@ -1344,7 +1351,7 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
log.info("manage", `Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [
|
||||
"",
|
||||
@ -1370,7 +1377,7 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
log.info("manage", `Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
|
||||
monitorID
|
||||
@ -1394,7 +1401,7 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Clear Statistics User ID: ${socket.userID}`);
|
||||
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM heartbeat");
|
||||
|
||||
@ -1416,35 +1423,35 @@ try {
|
||||
databaseSocketHandler(socket);
|
||||
proxySocketHandler(socket);
|
||||
|
||||
debug("added all socket handlers");
|
||||
log.debug("server", "added all socket handlers");
|
||||
|
||||
// ***************************
|
||||
// Better do anything after added all socket handlers here
|
||||
// ***************************
|
||||
|
||||
debug("check auto login");
|
||||
log.debug("auth", "check auto login");
|
||||
if (await setting("disableAuth")) {
|
||||
console.log("Disabled Auth: auto login to admin");
|
||||
log.info("auth", "Disabled Auth: auto login to admin");
|
||||
afterLogin(socket, await R.findOne("user"));
|
||||
socket.emit("autoLogin");
|
||||
} else {
|
||||
debug("need auth");
|
||||
log.debug("auth", "need auth");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
console.log("Init the server");
|
||||
log.info("server", "Init the server");
|
||||
|
||||
httpServer.once("error", async (err) => {
|
||||
server.httpServer.once("error", async (err) => {
|
||||
console.error("Cannot listen: " + err.message);
|
||||
await shutdownFunction();
|
||||
});
|
||||
|
||||
httpServer.listen(port, hostname, () => {
|
||||
server.httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
console.log(`Listening on ${hostname}:${port}`);
|
||||
log.info("server", `Listening on ${hostname}:${port}`);
|
||||
} else {
|
||||
console.log(`Listening on ${port}`);
|
||||
log.info("server", `Listening on ${port}`);
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
@ -1462,11 +1469,11 @@ try {
|
||||
})();
|
||||
|
||||
/**
|
||||
* Adds or removes notifications from a monitor.
|
||||
* @param {number} monitorID The ID of the monitor to add/remove notifications from.
|
||||
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Update notifications for a given monitor
|
||||
* @param {number} monitorID ID of monitor to update
|
||||
* @param {number[]} notificationIDList List of new notification
|
||||
* providers to add
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||
@ -1484,11 +1491,11 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks if the user owns a monitor with the given ID.
|
||||
* @param {number} monitorID - The ID of the monitor to check ownership for.
|
||||
* @param {number} userID - The ID of the user who is trying to access this data.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Check if a given user owns a specific monitor
|
||||
* @param {number} userID
|
||||
* @param {number} monitorID
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} The specified user does not own the monitor
|
||||
*/
|
||||
async function checkOwner(userID, monitorID) {
|
||||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
@ -1502,8 +1509,11 @@ async function checkOwner(userID, monitorID) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after user login
|
||||
* This function is used to send the heartbeat list of a monitor.
|
||||
* @param {Socket} socket - The socket object that will be used to send the data.
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {Object} user User object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function afterLogin(socket, user) {
|
||||
socket.userID = user.id;
|
||||
@ -1531,40 +1541,20 @@ async function afterLogin(socket, user) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of monitors for the given user.
|
||||
* @param {string} userID - The ID of the user to get monitors for.
|
||||
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
|
||||
userID,
|
||||
]);
|
||||
|
||||
for (let monitor of monitorList) {
|
||||
result[monitor.id] = await monitor.toJSON();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database and patch it if necessary.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Initialize the database
|
||||
* @param {boolean} [testMode=false] Should the connection be
|
||||
* started in test mode?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database");
|
||||
log.info("server", "Copying Database");
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
console.log("Connecting to the Database");
|
||||
log.info("server", "Connecting to the Database");
|
||||
await Database.connect(testMode);
|
||||
console.log("Connected");
|
||||
log.info("server", "Connected");
|
||||
|
||||
// Patch the database
|
||||
await Database.patch();
|
||||
@ -1574,16 +1564,16 @@ async function initDatabase(testMode = false) {
|
||||
]);
|
||||
|
||||
if (! jwtSecretBean) {
|
||||
console.log("JWT secret is not found, generate one.");
|
||||
log.info("server", "JWT secret is not found, generate one.");
|
||||
jwtSecretBean = await initJWTSecret();
|
||||
console.log("Stored JWT secret into database");
|
||||
log.info("server", "Stored JWT secret into database");
|
||||
} else {
|
||||
console.log("Load JWT secret from database.");
|
||||
log.info("server", "Load JWT secret from database.");
|
||||
}
|
||||
|
||||
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
|
||||
if ((await R.count("user")) === 0) {
|
||||
console.log("No user, need setup");
|
||||
log.info("server", "No user, need setup");
|
||||
needSetup = true;
|
||||
}
|
||||
|
||||
@ -1591,16 +1581,15 @@ async function initDatabase(testMode = false) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a monitor.
|
||||
* @param {string} userID - The ID of the user who owns the monitor.
|
||||
* @param {string} monitorID - The ID of the monitor to resume.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Start the specified monitor
|
||||
* @param {number} userID ID of user who owns monitor
|
||||
* @param {number} monitorID ID of monitor to start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function startMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`);
|
||||
log.info("manage", `Resume Monitor: ${monitorID} User ID: ${userID}`);
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
@ -1619,21 +1608,26 @@ async function startMonitor(userID, monitorID) {
|
||||
monitor.start(io);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a given monitor
|
||||
* @param {number} userID ID of user who owns monitor
|
||||
* @param {number} monitorID ID of monitor to start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function restartMonitor(userID, monitorID) {
|
||||
return await startMonitor(userID, monitorID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a monitor.
|
||||
* @param {string} userID - The ID of the user who owns the monitor.
|
||||
* @param {string} monitorID - The ID of the monitor to pause.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Pause a given monitor
|
||||
* @param {number} userID ID of user who owns monitor
|
||||
* @param {number} monitorID ID of monitor to start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function pauseMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`);
|
||||
log.info("manage", `Pause Monitor: ${monitorID} User ID: ${userID}`);
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
@ -1645,9 +1639,7 @@ async function pauseMonitor(userID, monitorID) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume active monitors
|
||||
*/
|
||||
/** Resume active monitors */
|
||||
async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ");
|
||||
|
||||
@ -1663,16 +1655,16 @@ async function startMonitors() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the application
|
||||
* Stops all monitors and closes the database connection.
|
||||
* @param {string} signal The signal that triggered this function to be called.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function shutdownFunction(signal) {
|
||||
console.log("Shutdown requested");
|
||||
console.log("Called signal: " + signal);
|
||||
log.info("server", "Shutdown requested");
|
||||
log.info("server", "Called signal: " + signal);
|
||||
|
||||
console.log("Stopping all monitors");
|
||||
log.info("server", "Stopping all monitors");
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
monitor.stop();
|
||||
@ -1684,11 +1676,16 @@ async function shutdownFunction(signal) {
|
||||
await cloudflaredStop();
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log("Graceful shutdown successful!");
|
||||
function getClientIp(socket) {
|
||||
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
||||
}
|
||||
|
||||
gracefulShutdown(httpServer, {
|
||||
/** Final function called before application exits */
|
||||
function finalFunction() {
|
||||
log.info("server", "Graceful shutdown successful!");
|
||||
}
|
||||
|
||||
gracefulShutdown(server.httpServer, {
|
||||
signals: "SIGINT SIGTERM",
|
||||
timeout: 30000, // timeout: 30 secs
|
||||
development: false, // not in dev mode
|
||||
@ -1700,6 +1697,6 @@ gracefulShutdown(httpServer, {
|
||||
// Catch unexpected errors here
|
||||
process.addListener("unhandledRejection", (error, promise) => {
|
||||
console.trace(error);
|
||||
errorLog(error, false);
|
||||
UptimeKumaServer.errorLog(error, false);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
});
|
||||
|
@ -1,19 +1,33 @@
|
||||
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
|
||||
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
||||
const { io } = require("../server");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const io = UptimeKumaServer.getInstance().io;
|
||||
|
||||
const prefix = "cloudflared_";
|
||||
const cloudflared = new CloudflaredTunnel();
|
||||
|
||||
/**
|
||||
* Change running state
|
||||
* @param {string} running Is it running?
|
||||
* @param {string} message Message to pass
|
||||
*/
|
||||
cloudflared.change = (running, message) => {
|
||||
io.to("cloudflared").emit(prefix + "running", running);
|
||||
io.to("cloudflared").emit(prefix + "message", message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit an error message
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
cloudflared.error = (errorMessage) => {
|
||||
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for cloudflared
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
|
||||
socket.on(prefix + "join", async () => {
|
||||
@ -68,6 +82,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically start cloudflared
|
||||
* @param {string} token Cloudflared tunnel token
|
||||
*/
|
||||
module.exports.autoStart = async (token) => {
|
||||
if (!token) {
|
||||
token = await setting("cloudflaredTunnelToken");
|
||||
@ -84,7 +102,10 @@ module.exports.autoStart = async (token) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Stop cloudflared */
|
||||
module.exports.stop = async () => {
|
||||
console.log("Stop cloudflared");
|
||||
cloudflared.stop();
|
||||
if (cloudflared) {
|
||||
cloudflared.stop();
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,10 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const Database = require("../database");
|
||||
|
||||
/**
|
||||
* Handlers for database
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
|
@ -1,8 +1,13 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { Proxy } = require("../proxy");
|
||||
const { sendProxyList } = require("../client");
|
||||
const server = require("../server");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
/**
|
||||
* Handlers for proxy
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports.proxySocketHandler = (socket) => {
|
||||
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||
try {
|
||||
|
@ -1,13 +1,17 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||
const { checkLogin, setSetting } = require("../util-server");
|
||||
const dayjs = require("dayjs");
|
||||
const { debug } = require("../../src/util");
|
||||
const { log } = require("../../src/util");
|
||||
const ImageDataURI = require("../image-data-uri");
|
||||
const Database = require("../database");
|
||||
const apicache = require("../modules/apicache");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const server = require("../server");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
/**
|
||||
* Socket handlers for status page
|
||||
* @param {Socket} socket Socket.io instance to add listeners on
|
||||
*/
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
@ -155,6 +159,9 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
//statusPage.search_engine_index = ;
|
||||
statusPage.show_tags = config.showTags;
|
||||
//statusPage.password = null;
|
||||
statusPage.footer_text = config.footerText;
|
||||
statusPage.custom_css = config.customCSS;
|
||||
statusPage.show_powered_by = config.showPoweredBy;
|
||||
statusPage.modified_date = R.isoDateTime();
|
||||
|
||||
await R.store(statusPage);
|
||||
@ -202,8 +209,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
group.id = groupBean.id;
|
||||
}
|
||||
|
||||
// Delete groups that not in the list
|
||||
debug("Delete groups that not in the list");
|
||||
// Delete groups that are not in the list
|
||||
log.debug("socket", "Delete groups that are not in the list");
|
||||
const slots = groupIDList.map(() => "?").join(",");
|
||||
|
||||
const data = [
|
||||
@ -212,6 +219,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
];
|
||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
||||
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||
server.entryPage = "statusPage-" + statusPage.slug;
|
||||
@ -226,7 +235,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
log.error("socket", error);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@ -281,6 +290,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Delete a status page
|
||||
socket.on("deleteStatusPage", async (slug, callback) => {
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
@ -331,6 +342,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
/**
|
||||
* Check slug a-z, 0-9, - only
|
||||
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||
* @param {string} slug Slug to test
|
||||
*/
|
||||
function checkSlug(slug) {
|
||||
if (typeof slug !== "string") {
|
||||
|
117
server/uptime-kuma-server.js
Normal file
117
server/uptime-kuma-server.js
Normal file
@ -0,0 +1,117 @@
|
||||
const express = require("express");
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const { Server } = require("socket.io");
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../src/util");
|
||||
const Database = require("./database");
|
||||
const util = require("util");
|
||||
|
||||
/**
|
||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||
* @type {UptimeKumaServer}
|
||||
*/
|
||||
class UptimeKumaServer {
|
||||
/**
|
||||
*
|
||||
* @type {UptimeKumaServer}
|
||||
*/
|
||||
static instance = null;
|
||||
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
monitorList = {};
|
||||
entryPage = "dashboard";
|
||||
app = undefined;
|
||||
httpServer = undefined;
|
||||
io = undefined;
|
||||
|
||||
static getInstance(args) {
|
||||
if (UptimeKumaServer.instance == null) {
|
||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||
}
|
||||
return UptimeKumaServer.instance;
|
||||
}
|
||||
|
||||
constructor(args) {
|
||||
// SSL
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
|
||||
log.info("server", "Creating express and socket.io instance");
|
||||
this.app = express();
|
||||
|
||||
if (sslKey && sslCert) {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert)
|
||||
}, this.app);
|
||||
} else {
|
||||
log.info("server", "Server Type: HTTP");
|
||||
this.httpServer = http.createServer(this.app);
|
||||
}
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
async sendMonitorList(socket) {
|
||||
let list = await this.getMonitorJSONList(socket.userID);
|
||||
this.io.to(socket.userID).emit("monitorList", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of monitors for the given user.
|
||||
* @param {string} userID - The ID of the user to get monitors for.
|
||||
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
|
||||
userID,
|
||||
]);
|
||||
|
||||
for (let monitor of monitorList) {
|
||||
result[monitor.id] = await monitor.toJSON();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write error to log file
|
||||
* @param {any} error The error to write
|
||||
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||
*/
|
||||
static errorLog(error, outputToConsole = true) {
|
||||
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
||||
flags: "a"
|
||||
});
|
||||
|
||||
errorLogStream.on("error", () => {
|
||||
log.info("", "Cannot write to error.log");
|
||||
});
|
||||
|
||||
if (errorLogStream) {
|
||||
const dateTime = R.isoDateTime();
|
||||
errorLogStream.write(`[${dateTime}] ` + util.format(error) + "\n");
|
||||
|
||||
if (outputToConsole) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
errorLogStream.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UptimeKumaServer
|
||||
};
|
@ -1,14 +1,15 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const Ping = require("./ping-lite");
|
||||
const { R } = require("redbean-node");
|
||||
const { debug, genSecret } = require("../src/util");
|
||||
const { log, genSecret } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const childProcess = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const fs = require("fs");
|
||||
const nodeJsUtil = require("util");
|
||||
const mqtt = require("mqtt");
|
||||
const chroma = require("chroma-js");
|
||||
const { badgeConstants } = require("./config");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
@ -26,7 +27,7 @@ exports.initJWTSecret = async () => {
|
||||
"jwtSecret",
|
||||
]);
|
||||
|
||||
if (! jwtSecretBean) {
|
||||
if (!jwtSecretBean) {
|
||||
jwtSecretBean = R.dispense("setting");
|
||||
jwtSecretBean.key = "jwtSecret";
|
||||
}
|
||||
@ -36,6 +37,12 @@ exports.initJWTSecret = async () => {
|
||||
return jwtSecretBean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send TCP request to specified hostname and port
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
* @param {number} port TCP port to test
|
||||
* @returns {Promise<number>} Maximum time in ms rounded to nearest integer
|
||||
*/
|
||||
exports.tcping = function (hostname, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tcpp.ping({
|
||||
@ -57,6 +64,11 @@ exports.tcping = function (hostname, port) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Ping the specified machine
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||
*/
|
||||
exports.ping = async (hostname) => {
|
||||
try {
|
||||
return await exports.pingAsync(hostname);
|
||||
@ -70,6 +82,12 @@ exports.ping = async (hostname) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ping the specified machine
|
||||
* @param {string} hostname Hostname / address of machine to ping
|
||||
* @param {boolean} ipv6 Should IPv6 be used?
|
||||
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||
*/
|
||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ping = new Ping(hostname, {
|
||||
@ -88,11 +106,84 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
});
|
||||
};
|
||||
|
||||
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([resolver_server]);
|
||||
/**
|
||||
* MQTT Monitor
|
||||
* @param {string} hostname Hostname / address of machine to test
|
||||
* @param {string} topic MQTT topic
|
||||
* @param {string} okMessage Expected result
|
||||
* @param {Object} [options={}] MQTT options. Contains port, username,
|
||||
* password and interval (interval defaults to 20)
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (rrtype == "PTR") {
|
||||
const { port, username, password, interval = 20 } = options;
|
||||
|
||||
// Adds MQTT protocol to the hostname if not already present
|
||||
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) {
|
||||
hostname = "mqtt://" + hostname;
|
||||
}
|
||||
|
||||
const timeoutID = setTimeout(() => {
|
||||
log.debug("mqtt", "MQTT timeout triggered");
|
||||
client.end();
|
||||
reject(new Error("Timeout"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
log.debug("mqtt", "MQTT connecting");
|
||||
|
||||
let client = mqtt.connect(hostname, {
|
||||
port,
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
log.debug("mqtt", "MQTT connected");
|
||||
|
||||
try {
|
||||
log.debug("mqtt", "MQTT subscribe topic");
|
||||
client.subscribe(topic);
|
||||
} catch (e) {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Cannot subscribe topic"));
|
||||
}
|
||||
});
|
||||
|
||||
client.on("error", (error) => {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
client.on("message", (messageTopic, message) => {
|
||||
if (messageTopic === topic) {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
|
||||
reject(new Error(`Message Mismatch - Topic: ${messageTopic}; Message: ${message.toString()}`));
|
||||
} else {
|
||||
resolve(`Topic: ${messageTopic}; Message: ${message.toString()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a given record using the specified DNS server
|
||||
* @param {string} hostname The hostname of the record to lookup
|
||||
* @param {string} resolverServer The DNS server to use
|
||||
* @param {string} rrtype The type of record to request
|
||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||
*/
|
||||
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([ resolverServer ]);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (rrtype === "PTR") {
|
||||
resolver.reverse(hostname, (err, records) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
@ -112,6 +203,11 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve value of setting based on key
|
||||
* @param {string} key Key of setting to retrieve
|
||||
* @returns {Promise<any>} Value
|
||||
*/
|
||||
exports.setting = async function (key) {
|
||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||
key,
|
||||
@ -119,13 +215,20 @@ exports.setting = async function (key) {
|
||||
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
debug(`Get Setting: ${key}: ${v}`);
|
||||
log.debug("util", `Get Setting: ${key}: ${v}`);
|
||||
return v;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the specified setting to specifed value
|
||||
* @param {string} key Key of setting to set
|
||||
* @param {any} value Value to set to
|
||||
* @param {?string} type Type of setting
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.setSetting = async function (key, value, type = null) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
@ -139,6 +242,11 @@ exports.setSetting = async function (key, value, type = null) {
|
||||
await R.store(bean);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get settings based on type
|
||||
* @param {?string} type The type of setting
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
exports.getSettings = async function (type) {
|
||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||
type,
|
||||
@ -157,6 +265,12 @@ exports.getSettings = async function (type) {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set settings based on type
|
||||
* @param {?string} type Type of settings to set
|
||||
* @param {Object} data Values of settings
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.setSettings = async function (type, data) {
|
||||
let keyList = Object.keys(data);
|
||||
|
||||
@ -183,12 +297,23 @@ exports.setSettings = async function (type, data) {
|
||||
};
|
||||
|
||||
// ssl-checker by @dyaa
|
||||
// param: res - response object from axios
|
||||
// return an object containing the certificate information
|
||||
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
|
||||
|
||||
/**
|
||||
* Get number of days between two dates
|
||||
* @param {Date} validFrom Start date
|
||||
* @param {Date} validTo End date
|
||||
* @returns {number}
|
||||
*/
|
||||
const getDaysBetween = (validFrom, validTo) =>
|
||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||
|
||||
/**
|
||||
* Get days remaining from a time range
|
||||
* @param {Date} validFrom Start date
|
||||
* @param {Date} validTo End date
|
||||
* @returns {number}
|
||||
*/
|
||||
const getDaysRemaining = (validFrom, validTo) => {
|
||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||
@ -197,8 +322,11 @@ const getDaysRemaining = (validFrom, validTo) => {
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
// Fix certificate Info for display
|
||||
// param: info - the chain obtained from getPeerCertificate()
|
||||
/**
|
||||
* Fix certificate info for display
|
||||
* @param {Object} info The chain obtained from getPeerCertificate()
|
||||
* @returns {Object} An object representing certificate information
|
||||
*/
|
||||
const parseCertificateInfo = function (info) {
|
||||
let link = info;
|
||||
let i = 0;
|
||||
@ -206,7 +334,7 @@ const parseCertificateInfo = function (info) {
|
||||
const existingList = {};
|
||||
|
||||
while (link) {
|
||||
debug(`[${i}] ${link.fingerprint}`);
|
||||
log.debug("cert", `[${i}] ${link.fingerprint}`);
|
||||
|
||||
if (!link.valid_from || !link.valid_to) {
|
||||
break;
|
||||
@ -221,7 +349,7 @@ const parseCertificateInfo = function (info) {
|
||||
if (link.issuerCertificate == null) {
|
||||
break;
|
||||
} else if (link.issuerCertificate.fingerprint in existingList) {
|
||||
debug(`[Last] ${link.issuerCertificate.fingerprint}`);
|
||||
log.debug("cert", `[Last] ${link.issuerCertificate.fingerprint}`);
|
||||
link.issuerCertificate = null;
|
||||
break;
|
||||
} else {
|
||||
@ -238,11 +366,16 @@ const parseCertificateInfo = function (info) {
|
||||
return info;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if certificate is valid
|
||||
* @param {Object} res Response object from axios
|
||||
* @returns {Object} Object containing certificate information
|
||||
*/
|
||||
exports.checkCertificate = function (res) {
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
debug("Parsing Certificate Info");
|
||||
log.debug("cert", "Parsing Certificate Info");
|
||||
const parsedInfo = parseCertificateInfo(info);
|
||||
|
||||
return {
|
||||
@ -251,25 +384,26 @@ exports.checkCertificate = function (res) {
|
||||
};
|
||||
};
|
||||
|
||||
// Check if the provided status code is within the accepted ranges
|
||||
// Param: status - the status code to check
|
||||
// Param: accepted_codes - an array of accepted status codes
|
||||
// Return: true if the status code is within the accepted ranges, false otherwise
|
||||
// Will throw an error if the provided status code is not a valid range string or code string
|
||||
|
||||
exports.checkStatusCode = function (status, accepted_codes) {
|
||||
if (accepted_codes == null || accepted_codes.length === 0) {
|
||||
/**
|
||||
* Check if the provided status code is within the accepted ranges
|
||||
* @param {string} status The status code to check
|
||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||
* @returns {boolean} True if status code within range, false otherwise
|
||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
||||
*/
|
||||
exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const code_range of accepted_codes) {
|
||||
const code_range_split = code_range.split("-").map(string => parseInt(string));
|
||||
if (code_range_split.length === 1) {
|
||||
if (status === code_range_split[0]) {
|
||||
for (const codeRange of acceptedCodes) {
|
||||
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
|
||||
if (codeRangeSplit.length === 1) {
|
||||
if (status === codeRangeSplit[0]) {
|
||||
return true;
|
||||
}
|
||||
} else if (code_range_split.length === 2) {
|
||||
if (status >= code_range_split[0] && status <= code_range_split[1]) {
|
||||
} else if (codeRangeSplit.length === 2) {
|
||||
if (status >= codeRangeSplit[0] && status <= codeRangeSplit[1]) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
@ -280,17 +414,23 @@ exports.checkStatusCode = function (status, accepted_codes) {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get total number of clients in room
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {string} roomName Name of room to check
|
||||
* @returns {number}
|
||||
*/
|
||||
exports.getTotalClientInRoom = (io, roomName) => {
|
||||
|
||||
const sockets = io.sockets;
|
||||
|
||||
if (! sockets) {
|
||||
if (!sockets) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const adapter = sockets.adapter;
|
||||
|
||||
if (! adapter) {
|
||||
if (!adapter) {
|
||||
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) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
exports.allowAllOrigin(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow CORS all origins
|
||||
* @param {Object} res Response object from axios
|
||||
*/
|
||||
exports.allowAllOrigin = (res) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a user is logged in
|
||||
* @param {Socket} socket Socket instance
|
||||
*/
|
||||
exports.checkLogin = (socket) => {
|
||||
if (! socket.userID) {
|
||||
if (!socket.userID) {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For logged-in users, double-check the password
|
||||
* @param socket
|
||||
* @param currentPassword
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {string} currentPassword
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
@ -342,10 +494,11 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
return user;
|
||||
};
|
||||
|
||||
/** Start Unit tests */
|
||||
exports.startUnitTest = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
const child = child_process.spawn(npm, ["run", "jest"]);
|
||||
const child = childProcess.spawn(npm, [ "run", "jest" ]);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
@ -362,33 +515,42 @@ exports.startUnitTest = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param body : Buffer
|
||||
* Convert unknown string to UTF8
|
||||
* @param {Uint8Array} body Buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.convertToUTF8 = (body) => {
|
||||
const guessEncoding = chardet.detect(body);
|
||||
//debug("Guess Encoding: " + guessEncoding);
|
||||
const str = iconv.decode(body, guessEncoding);
|
||||
return str.toString();
|
||||
};
|
||||
|
||||
let logFile;
|
||||
|
||||
try {
|
||||
logFile = fs.createWriteStream("./data/error.log", {
|
||||
flags: "a"
|
||||
});
|
||||
} catch (_) { }
|
||||
|
||||
exports.errorLog = (error, outputToConsole = true) => {
|
||||
/**
|
||||
* Returns a color code in hex format based on a given percentage:
|
||||
* 0% => hue = 10 => red
|
||||
* 100% => hue = 90 => green
|
||||
*
|
||||
* @param {number} percentage float, 0 to 1
|
||||
* @param {number} maxHue
|
||||
* @param {number} minHue, int
|
||||
* @returns {string}, hex value
|
||||
*/
|
||||
exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
|
||||
const hue = percentage * (maxHue - minHue) + minHue;
|
||||
try {
|
||||
if (logFile) {
|
||||
const dateTime = R.isoDateTime();
|
||||
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
|
||||
|
||||
if (outputToConsole) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
} catch (_) { }
|
||||
return chroma(`hsl(${hue}, 90%, 40%)`).hex();
|
||||
} catch (err) {
|
||||
return badgeConstants.naColor;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Joins and array of string to one string after filtering out empty values
|
||||
*
|
||||
* @param {string[]} parts
|
||||
* @param {string} connector
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.filterAndJoin = (parts, connector = "") => {
|
||||
return parts.filter((part) => !!part && part !== "").join(connector);
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setPageLocale } from "./util-frontend";
|
||||
export default {
|
||||
created() {
|
||||
setPageLocale();
|
||||
},
|
||||
created() {
|
||||
setPageLocale();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -469,6 +469,10 @@ textarea.form-control {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.prism-editor__textarea {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from "bootstrap"
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -42,19 +42,20 @@ export default {
|
||||
default: "No",
|
||||
},
|
||||
},
|
||||
emits: [ "yes" ],
|
||||
data: () => ({
|
||||
modal: null,
|
||||
}),
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal.show()
|
||||
this.modal.show();
|
||||
},
|
||||
yes() {
|
||||
this.$emit("yes");
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -57,6 +57,7 @@ export default {
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: [ "update:modelValue" ],
|
||||
data() {
|
||||
return {
|
||||
visibility: "password",
|
||||
|
@ -5,12 +5,15 @@
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { sleep } from "../util.ts"
|
||||
import { sleep } from "../util.ts";
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
value: [String, Number],
|
||||
value: {
|
||||
type: [ String, Number ],
|
||||
default: 0,
|
||||
},
|
||||
time: {
|
||||
type: Number,
|
||||
default: 0.3,
|
||||
@ -25,12 +28,12 @@ export default {
|
||||
return {
|
||||
output: "",
|
||||
frameDuration: 30,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isNum() {
|
||||
return typeof this.value === "number"
|
||||
return typeof this.value === "number";
|
||||
},
|
||||
},
|
||||
|
||||
@ -45,7 +48,7 @@ export default {
|
||||
} else {
|
||||
for (let i = 1; i < frames; i++) {
|
||||
this.output += step;
|
||||
await sleep(15)
|
||||
await sleep(15);
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,5 +62,5 @@ export default {
|
||||
|
||||
methods: {},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -4,16 +4,19 @@
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import timezone from "dayjs/plugin/timezone" // dependent on utc plugin
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(relativeTime)
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
|
||||
import utc from "dayjs/plugin/utc";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
dateOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -29,5 +32,5 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -38,7 +38,7 @@ export default {
|
||||
beatMargin: 4,
|
||||
move: false,
|
||||
maxBeat: -1,
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@ -69,12 +69,12 @@ export default {
|
||||
if (start < 0) {
|
||||
// Add empty placeholder
|
||||
for (let i = start; i < 0; i++) {
|
||||
placeholders.push(0)
|
||||
placeholders.push(0);
|
||||
}
|
||||
start = 0;
|
||||
}
|
||||
|
||||
return placeholders.concat(this.beatList.slice(start))
|
||||
return placeholders.concat(this.beatList.slice(start));
|
||||
},
|
||||
|
||||
wrapStyle() {
|
||||
@ -84,7 +84,7 @@ export default {
|
||||
return {
|
||||
padding: `${topBottom}px ${leftRight}px`,
|
||||
width: "100%",
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
barStyle() {
|
||||
@ -94,12 +94,12 @@ export default {
|
||||
return {
|
||||
transition: "all ease-in-out 0.25s",
|
||||
transform: `translateX(${width}px)`,
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
return {
|
||||
transform: "translateX(0)",
|
||||
}
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
@ -109,7 +109,7 @@ export default {
|
||||
height: this.beatHeight + "px",
|
||||
margin: this.beatMargin + "px",
|
||||
"--hover-scale": this.hoverScale,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
},
|
||||
@ -120,7 +120,7 @@ export default {
|
||||
|
||||
setTimeout(() => {
|
||||
this.move = false;
|
||||
}, 300)
|
||||
}, 300);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
@ -162,15 +162,16 @@ export default {
|
||||
methods: {
|
||||
resize() {
|
||||
if (this.$refs.wrap) {
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||
}
|
||||
},
|
||||
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ``);
|
||||
}
|
||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -48,18 +48,19 @@ export default {
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: [ "update:modelValue" ],
|
||||
data() {
|
||||
return {
|
||||
visibility: "password",
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value)
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -74,5 +75,5 @@ export default {
|
||||
this.visibility = "password";
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
@ -47,8 +47,8 @@
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
@ -105,7 +105,7 @@ export default {
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText != "") {
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
@ -170,12 +170,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.footer {
|
||||
// background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
@ -198,7 +192,7 @@ export default {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.monitorItem {
|
||||
.monitor-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,6 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import { ucfirst } from "../util.ts";
|
||||
|
||||
import Confirm from "./Confirm.vue";
|
||||
import NotificationFormList from "./notifications";
|
||||
@ -79,7 +78,7 @@ export default {
|
||||
Confirm,
|
||||
},
|
||||
props: {},
|
||||
emits: ["added"],
|
||||
emits: [ "added" ],
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user