mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 22:06:59 +08:00
Compare commits
112 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
243726b03c | ||
|
936665aac3 | ||
|
1185b259c2 | ||
|
a81f949f98 | ||
|
59f10d542b | ||
|
2778929f74 | ||
|
f71d35e53e | ||
|
1490443618 | ||
|
add5c128ce | ||
|
e797abd108 | ||
|
7a9e2f5de6 | ||
|
7b5d2a71ff | ||
|
893278bd3d | ||
|
0e30ea830d | ||
|
c67a2070b8 | ||
|
9863a10321 | ||
|
ee7f8680c1 | ||
|
c1301804d4 | ||
|
b385e81608 | ||
|
f37f55e06c | ||
|
87d7a780e3 | ||
|
0fc372f558 | ||
|
67a13e1259 | ||
|
2b8f55194f | ||
|
288cab6dd7 | ||
|
b4e45c7ce8 | ||
|
7635ab54a0 | ||
|
458cdf9f9b | ||
|
f1e2ee74ea | ||
|
8d847abf35 | ||
|
8151ac0e25 | ||
|
4185ec20b0 | ||
|
4245ea86e7 | ||
|
f861a48dfc | ||
|
fa1214ae5e | ||
|
621419e434 | ||
|
482049c72b | ||
|
2815cc73cf | ||
|
e1147c06aa | ||
|
abc8f2b131 | ||
|
777ef6bc7b | ||
|
b244e8fcbb | ||
|
031947319a | ||
|
74a908a069 | ||
|
9c56c9b346 | ||
|
37666bf35f | ||
|
90badfabee | ||
|
e3396251a8 | ||
|
9c9a086788 | ||
|
9fb95fe95e | ||
|
1e75d81bcf | ||
|
cb3a104dc0 | ||
|
57a18958d6 | ||
|
1708b67949 | ||
|
73239d441d | ||
|
4ceeb304f1 | ||
|
711380bbbe | ||
|
9536c6aa6a | ||
|
4255496b11 | ||
|
f28dccf4e1 | ||
|
b689733d59 | ||
|
afaa7bb2f0 | ||
|
121d1a11af | ||
|
8e61158758 | ||
|
bf58838b89 | ||
|
33ce0ef02c | ||
|
c1aaad0d85 | ||
|
954e05b72f | ||
|
6d4a45f18c | ||
|
f0975cd929 | ||
|
40d6a21453 | ||
|
b383392e8f | ||
|
9964b6c4d8 | ||
|
d56bf08cd7 | ||
|
291d5d7c55 | ||
|
8e3ff25f7b | ||
|
6e80c850f4 | ||
|
0608881954 | ||
|
38efd97b28 | ||
|
ce0ba6c0ca | ||
|
c43223a16d | ||
|
9f170a68d7 | ||
|
1a862e47ab | ||
|
e64bf0e3fe | ||
|
523d137e2b | ||
|
18169c59a1 | ||
|
4ccf263481 | ||
|
1c13a75970 | ||
|
c3e3f27457 | ||
|
794f1810bf | ||
|
168357d93c | ||
|
476deb9fec | ||
|
a36f2a75ca | ||
|
88afab6571 | ||
|
bd9c44cccf | ||
|
1b148786a5 | ||
|
66a10b8993 | ||
|
2ab21ccf8a | ||
|
90d0e8ccde | ||
|
16a396debb | ||
|
6b3d69e1d3 | ||
|
b3b8e9f3a0 | ||
|
e5345848a2 | ||
|
d8a8f6c08b | ||
|
f98a1ce077 | ||
|
86fa57449e | ||
|
ff51704cdf | ||
|
33804d8823 | ||
|
1e12ca4786 | ||
|
0af4ee6c34 | ||
|
1f29fabe64 | ||
|
c4e222d1e6 |
@@ -34,7 +34,12 @@ tsconfig.json
|
|||||||
/ecosystem.config.js
|
/ecosystem.config.js
|
||||||
/extra/healthcheck.exe
|
/extra/healthcheck.exe
|
||||||
/extra/healthcheck
|
/extra/healthcheck
|
||||||
extra/exe-builder
|
/extra/exe-builder
|
||||||
|
/extra/push-examples
|
||||||
|
/extra/uptime-kuma-push
|
||||||
|
|
||||||
|
# Comment the following line if you want to rebuild the healthcheck binary
|
||||||
|
/extra/healthcheck-armv7
|
||||||
|
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
@@ -78,7 +78,7 @@ module.exports = {
|
|||||||
"checkLoops": false,
|
"checkLoops": false,
|
||||||
}],
|
}],
|
||||||
"space-before-blocks": "warn",
|
"space-before-blocks": "warn",
|
||||||
//'no-console': 'warn',
|
//"no-console": "warn",
|
||||||
"no-extra-boolean-cast": "off",
|
"no-extra-boolean-cast": "off",
|
||||||
"no-multiple-empty-lines": [ "warn", {
|
"no-multiple-empty-lines": [ "warn", {
|
||||||
"max": 1,
|
"max": 1,
|
||||||
@@ -90,7 +90,8 @@ module.exports = {
|
|||||||
"no-unneeded-ternary": "error",
|
"no-unneeded-ternary": "error",
|
||||||
"array-bracket-newline": [ "error", "consistent" ],
|
"array-bracket-newline": [ "error", "consistent" ],
|
||||||
"eol-last": [ "error", "always" ],
|
"eol-last": [ "error", "always" ],
|
||||||
//'prefer-template': 'error',
|
//"prefer-template": "error",
|
||||||
|
"template-curly-spacing": [ "warn", "never" ],
|
||||||
"comma-dangle": [ "warn", "only-multiline" ],
|
"comma-dangle": [ "warn", "only-multiline" ],
|
||||||
"no-empty": [ "error", {
|
"no-empty": [ "error", {
|
||||||
"allowEmptyCatch": true
|
"allowEmptyCatch": true
|
||||||
|
52
.github/workflows/auto-test.yml
vendored
52
.github/workflows/auto-test.yml
vendored
@@ -5,11 +5,11 @@ name: Auto Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master, 1.23.X ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '*.md'
|
- '*.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, 2.0.X ]
|
branches: [ master, 1.23.X ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '*.md'
|
- '*.md'
|
||||||
|
|
||||||
@@ -22,19 +22,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||||
node: [ 14, 20 ]
|
node: [ 16, 20.5 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node }}
|
- name: Use Node.js ${{ matrix.node }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
- run: npm install npm@latest -g
|
- run: npm ci
|
||||||
- run: npm install
|
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
env:
|
env:
|
||||||
@@ -50,18 +49,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ARMv7 ]
|
os: [ ARMv7 ]
|
||||||
node: [ 14, 20 ]
|
node: [ 16, 20.5 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node }}
|
- name: Use Node.js ${{ matrix.node }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
- run: npm install npm@latest -g
|
|
||||||
- run: npm ci --production
|
- run: npm ci --production
|
||||||
|
|
||||||
check-linters:
|
check-linters:
|
||||||
@@ -69,27 +67,27 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 14
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 20.5
|
||||||
- run: npm install
|
- run: npm ci
|
||||||
- run: npm run lint
|
- run: npm run lint:prod
|
||||||
|
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
needs: [ check-linters ]
|
needs: [ check-linters ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 14
|
- name: Use Node.js 16
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 16
|
||||||
- run: npm install
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:test
|
- run: npm run cy:test
|
||||||
|
|
||||||
@@ -98,12 +96,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 14
|
- name: Use Node.js 16
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 16
|
||||||
- run: npm install
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:run:unit
|
- run: npm run cy:run:unit
|
||||||
|
4
.github/workflows/close-incorrect-issue.yml
vendored
4
.github/workflows/close-incorrect-issue.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
|||||||
node-version: [16]
|
node-version: [16]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
6
.github/workflows/json-yaml-validate.yml
vendored
6
.github/workflows/json-yaml-validate.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- 2.0.X
|
- 1.23.X
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -17,11 +17,11 @@ jobs:
|
|||||||
json-yaml-validate:
|
json-yaml-validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: json-yaml-validate
|
- name: json-yaml-validate
|
||||||
id: json-yaml-validate
|
id: json-yaml-validate
|
||||||
uses: GrantBirki/json-yaml-validate@v1.3.0
|
uses: GrantBirki/json-yaml-validate@v2.4.0
|
||||||
with:
|
with:
|
||||||
comment: "true" # enable comment mode
|
comment: "true" # enable comment mode
|
||||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
||||||
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v7
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
||||||
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
|
|||||||
|
|
||||||
### Windows Portable (x64)
|
### Windows Portable (x64)
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
https://github.com/louislam/uptime-kuma/releases/download/1.23.1/uptime-kuma-windows-x64-portable-1.23.1.zip
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import visualizer from "rollup-plugin-visualizer";
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression";
|
||||||
import commonjs from "vite-plugin-commonjs";
|
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
@@ -22,7 +21,6 @@ export default defineConfig({
|
|||||||
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
commonjs(),
|
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: [ "since 2015" ],
|
targets: [ "since 2015" ],
|
||||||
|
34
db/patch-fix-kafka-producer-booleans.sql
Normal file
34
db/patch-fix-kafka-producer-booleans.sql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Rename COLUMNs to another one (suffixed by `_old`)
|
||||||
|
ALTER TABLE monitor
|
||||||
|
RENAME COLUMN kafka_producer_ssl TO kafka_producer_ssl_old;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
RENAME COLUMN kafka_producer_allow_auto_topic_creation TO kafka_producer_allow_auto_topic_creation_old;
|
||||||
|
|
||||||
|
-- Add correct COLUMNs
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD COLUMN kafka_producer_ssl BOOLEAN default 0 NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD COLUMN kafka_producer_allow_auto_topic_creation BOOLEAN default 0 NOT NULL;
|
||||||
|
|
||||||
|
-- These SQL is still not fully safe. See https://github.com/louislam/uptime-kuma/issues/4039.
|
||||||
|
|
||||||
|
-- Set bring old values from `_old` COLUMNs to correct ones
|
||||||
|
-- UPDATE monitor SET kafka_producer_allow_auto_topic_creation = monitor.kafka_producer_allow_auto_topic_creation_old
|
||||||
|
-- WHERE monitor.kafka_producer_allow_auto_topic_creation_old IS NOT NULL;
|
||||||
|
|
||||||
|
-- UPDATE monitor SET kafka_producer_ssl = monitor.kafka_producer_ssl_old
|
||||||
|
-- WHERE monitor.kafka_producer_ssl_old IS NOT NULL;
|
||||||
|
|
||||||
|
-- Remove old COLUMNs
|
||||||
|
ALTER TABLE monitor
|
||||||
|
DROP COLUMN kafka_producer_allow_auto_topic_creation_old;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
DROP COLUMN kafka_producer_ssl_old;
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-monitor-tls-info-add-fk.sql
Normal file
18
db/patch-monitor-tls-info-add-fk.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
PRAGMA writable_schema = TRUE;
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
SQLITE_MASTER
|
||||||
|
SET
|
||||||
|
sql = replace(sql,
|
||||||
|
'monitor_id INTEGER NOT NULL',
|
||||||
|
'monitor_id INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'
|
||||||
|
)
|
||||||
|
WHERE
|
||||||
|
name = 'monitor_tls_info'
|
||||||
|
AND type = 'table';
|
||||||
|
|
||||||
|
PRAGMA writable_schema = RESET;
|
||||||
|
|
||||||
|
COMMIT;
|
10
db/patch-notification-config.sql
Normal file
10
db/patch-notification-config.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- SQLite: Change the data type of the column "config" from VARCHAR to TEXT
|
||||||
|
ALTER TABLE notification RENAME COLUMN config TO config_old;
|
||||||
|
ALTER TABLE notification ADD COLUMN config TEXT;
|
||||||
|
UPDATE notification SET config = config_old;
|
||||||
|
ALTER TABLE notification DROP COLUMN config_old;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/patch-timeout.sql
Normal file
7
db/patch-timeout.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
UPDATE monitor SET timeout = (interval * 0.8)
|
||||||
|
WHERE timeout IS NULL OR timeout <= 0;
|
||||||
|
|
||||||
|
COMMIT;
|
@@ -1,6 +1,6 @@
|
|||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
# DON'T UPDATE TO bullseye-slim, see #372.
|
||||||
# If the image changed, the second stage image should be changed too
|
# There is no 20-buster-slim for armv7 unfortunately, 18-buster-slim is the last one for Uptime Kuma v1.
|
||||||
FROM node:16-buster-slim
|
FROM node:18-buster-slim
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -27,7 +27,7 @@ RUN apt-get update && \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
sudo \
|
sudo \
|
||||||
nscd && \
|
nscd && \
|
||||||
pip3 --no-cache-dir install apprise==1.4.5 && \
|
pip3 --no-cache-dir install apprise==1.6.0 && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
apt --yes autoremove
|
apt --yes autoremove
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
|
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
|
||||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
*/
|
*/
|
||||||
const { FBSD } = require("../server/util-server");
|
const FBSD = /^freebsd/.test(process.platform);
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
|
44
extra/reformat-changelog.js
Normal file
44
extra/reformat-changelog.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Generate on GitHub
|
||||||
|
const input = `
|
||||||
|
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
|
||||||
|
`;
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
### 🆕 New Features
|
||||||
|
|
||||||
|
### 💇♀️ Improvements
|
||||||
|
|
||||||
|
### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
### ⬆️ Security Fixes
|
||||||
|
|
||||||
|
### 🦎 Translation Contributions
|
||||||
|
|
||||||
|
### Others
|
||||||
|
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Split the last " by "
|
||||||
|
const usernamePullRequesURL = line.split(" by ").pop();
|
||||||
|
|
||||||
|
if (!usernamePullRequesURL) {
|
||||||
|
console.log("Unable to parse", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
|
||||||
|
const pullRequestID = "#" + pullRequestURL.split("/").pop();
|
||||||
|
let message = line.split(" by ").shift();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
console.log("Unable to parse", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = message.split("* ").pop();
|
||||||
|
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
||||||
|
}
|
||||||
|
console.log(template);
|
@@ -5,6 +5,8 @@ const { R } = require("redbean-node");
|
|||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
const { initJWTSecret } = require("../server/util-server");
|
const { initJWTSecret } = require("../server/util-server");
|
||||||
const User = require("../server/model/user");
|
const User = require("../server/model/user");
|
||||||
|
const { io } = require("socket.io-client");
|
||||||
|
const { localWebSocketURL } = require("../server/config");
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@@ -36,12 +38,16 @@ const main = async () => {
|
|||||||
// Reset all sessions by reset jwt secret
|
// Reset all sessions by reset jwt secret
|
||||||
await initJWTSecret();
|
await initJWTSecret();
|
||||||
|
|
||||||
|
// Disconnect all other socket clients of the user
|
||||||
|
await disconnectAllSocketClients(user.username, password);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
console.log("Passwords do not match, please try again.");
|
console.log("Passwords do not match, please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("Password reset successfully.");
|
console.log("Password reset successfully.");
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error: " + e.message);
|
console.error("Error: " + e.message);
|
||||||
@@ -66,6 +72,44 @@ function question(question) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disconnectAllSocketClients(username, password) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
|
||||||
|
|
||||||
|
// Disconnect all socket connections
|
||||||
|
const socket = io(localWebSocketURL, {
|
||||||
|
reconnection: false,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit("login", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("Logged in.");
|
||||||
|
socket.emit("disconnectOtherSocketClients");
|
||||||
|
} else {
|
||||||
|
console.warn("Login failed.");
|
||||||
|
console.warn("Please restart the server to disconnect all sessions.");
|
||||||
|
}
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", function () {
|
||||||
|
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
|
||||||
|
// Ask the user to restart the server manually
|
||||||
|
console.warn("Failed to connect to " + localWebSocketURL);
|
||||||
|
console.warn("Please restart the server to disconnect all sessions manually.");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.TEST_BACKEND) {
|
if (!process.env.TEST_BACKEND) {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
9515
package-lock.json
generated
9515
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.23.1",
|
"version": "1.23.14",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -13,10 +13,12 @@
|
|||||||
"install-legacy": "npm install",
|
"install-legacy": "npm install",
|
||||||
"update-legacy": "npm update",
|
"update-legacy": "npm update",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
|
"lint:js-prod": "npm run lint:js -- --max-warnings 0",
|
||||||
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
|
"lint:prod": "npm run lint:js-prod && npm run lint:style",
|
||||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
"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-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||||
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
||||||
@@ -40,7 +42,7 @@
|
|||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.23.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.23.14 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
@@ -56,6 +58,9 @@
|
|||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||||
|
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
|
||||||
|
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
|
||||||
|
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||||
"ncu-patch": "npm-check-updates -u -t patch",
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
@@ -69,14 +74,15 @@
|
|||||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
||||||
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
||||||
"sort-contributors": "node extra/sort-contributors.js"
|
"sort-contributors": "node extra/sort-contributors.js",
|
||||||
|
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "~1.7.3",
|
"@grpc/grpc-js": "~1.8.22",
|
||||||
"@louislam/ping": "~0.4.4-mod.1",
|
"@louislam/ping": "~0.4.4-mod.1",
|
||||||
"@louislam/sqlite3": "15.1.6",
|
"@louislam/sqlite3": "15.1.6",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.27.0",
|
"axios": "~0.28.1",
|
||||||
"axios-ntlm": "1.3.0",
|
"axios-ntlm": "1.3.0",
|
||||||
"badge-maker": "~3.3.1",
|
"badge-maker": "~3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
@@ -91,11 +97,12 @@
|
|||||||
"croner": "~6.0.5",
|
"croner": "~6.0.5",
|
||||||
"dayjs": "~1.11.5",
|
"dayjs": "~1.11.5",
|
||||||
"dotenv": "~16.0.3",
|
"dotenv": "~16.0.3",
|
||||||
"express": "~4.17.3",
|
"express": "~4.21.0",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "~4.0.5",
|
"gamedig": "^4.2.0",
|
||||||
|
"html-escaper": "^3.0.3",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
"https-proxy-agent": "~5.0.1",
|
"https-proxy-agent": "~5.0.1",
|
||||||
@@ -108,38 +115,39 @@
|
|||||||
"kafkajs": "^2.2.4",
|
"kafkajs": "^2.2.4",
|
||||||
"limiter": "~2.1.0",
|
"limiter": "~2.1.0",
|
||||||
"liquidjs": "^10.7.0",
|
"liquidjs": "^10.7.0",
|
||||||
"mongodb": "~4.14.0",
|
"mongodb": "~4.17.1",
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~8.1.4",
|
"mssql": "~8.1.4",
|
||||||
"mysql2": "~2.3.3",
|
"mysql2": "~3.9.6",
|
||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"node-radius-client": "~1.0.0",
|
"node-radius-client": "~1.0.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.9.13",
|
||||||
"nostr-tools": "^1.13.1",
|
"nostr-tools": "^1.13.1",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^5.4.2",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"pg": "~8.8.0",
|
"pg": "~8.11.3",
|
||||||
"pg-connection-string": "~2.5.0",
|
"pg-connection-string": "~2.6.2",
|
||||||
"playwright-core": "~1.35.1",
|
"playwright-core": "~1.35.1",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"promisify-child-process": "~4.1.2",
|
||||||
"protobufjs": "~7.2.4",
|
"protobufjs": "~7.2.4",
|
||||||
"qs": "~6.10.4",
|
"qs": "~6.10.4",
|
||||||
"redbean-node": "~0.3.0",
|
"redbean-node": "~0.3.0",
|
||||||
"redis": "~4.5.1",
|
"redis": "~4.5.1",
|
||||||
"semver": "~7.5.4",
|
"semver": "~7.5.4",
|
||||||
"socket.io": "~4.6.1",
|
"socket.io": "~4.8.0",
|
||||||
"socket.io-client": "~4.6.1",
|
"socket.io-client": "~4.8.0",
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"tar": "~6.1.11",
|
"tar": "~6.2.1",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "~5.0.1",
|
"@actions/github": "~5.1.1",
|
||||||
"@babel/eslint-parser": "^7.22.7",
|
"@babel/eslint-parser": "^7.22.7",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
@@ -160,10 +168,10 @@
|
|||||||
"core-js": "~3.26.1",
|
"core-js": "~3.26.1",
|
||||||
"cronstrue": "~2.24.0",
|
"cronstrue": "~2.24.0",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"cypress": "^12.17.0",
|
"cypress": "^13.2.0",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"dompurify": "~2.4.3",
|
"dompurify": "~3.1.7",
|
||||||
"eslint": "~8.14.0",
|
"eslint": "~8.14.0",
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
@@ -183,8 +191,7 @@
|
|||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"typescript": "~4.4.4",
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
"vite": "~4.4.1",
|
"vite": "~5.2.8",
|
||||||
"vite-plugin-commonjs": "^0.8.0",
|
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "~3.3.4",
|
"vue": "~3.3.4",
|
||||||
"vue-chartjs": "~5.2.0",
|
"vue-chartjs": "~5.2.0",
|
||||||
@@ -198,7 +205,7 @@
|
|||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^6.0.1",
|
"wait-on": "^7.2.0",
|
||||||
"whatwg-url": "~12.0.1"
|
"whatwg-url": "~12.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
<svg width="640" height="640" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="640" height="640" viewBox="0 0 640 640" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" fill="url(#paint0_linear_381_799)"/>
|
<g transform="matrix(1 0 0 1 320 320)">
|
||||||
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" stroke="#F2F2F2" stroke-opacity="0.51" stroke-width="200"/>
|
<linearGradient id="S3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 -319.99875 -320.0001577393)" x1="259.78" y1="261.15" x2="463.85" y2="456.49">
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_381_799" x1="259.78" y1="261.15" x2="463.85" y2="456.49" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#5CDD8B"/>
|
<stop stop-color="#5CDD8B"/>
|
||||||
<stop offset="1" stop-color="#86E6A9"/>
|
<stop offset="1" stop-color="#86E6A9"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
<path style="stroke: rgb(242,242,242); stroke-opacity: 0.51; stroke-width: 200; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#S3); fill-rule: nonzero; opacity: 1;" transform=" translate(0, 0)" d="M 170.40125 -84.36016 C 224.09125 38.37984 224.09125 115.33984 170.40125 146.49984 C 89.85125000000001 193.23984000000002 -120.03875 207.48984000000002 -180.45875 135.63984 C -220.73875 87.73983999999999 -220.73875 14.399839999999998 -180.45875 -84.36016000000001 C -139.49875 -151.82016 -81.28875000000001 -185.55016 -5.828750000000014 -185.55016 C 69.64124999999999 -185.55016 128.38125 -151.82016000000002 170.40124999999998 -84.36016000000001 z" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 893 B After Width: | Height: | Size: 1.1 KiB |
@@ -1,29 +1,42 @@
|
|||||||
|
const isFreeBSD = /^freebsd/.test(process.platform);
|
||||||
|
|
||||||
// Interop with browser
|
// Interop with browser
|
||||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
||||||
const demoMode = args["demo"] || false;
|
|
||||||
|
|
||||||
const badgeConstants = {
|
// 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.
|
||||||
naColor: "#999",
|
// Dual-stack support for (::)
|
||||||
defaultUpColor: "#66c20a",
|
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||||
defaultWarnColor: "#eed202",
|
let hostEnv = isFreeBSD ? null : process.env.HOST;
|
||||||
defaultDownColor: "#c2290a",
|
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||||
defaultPendingColor: "#f8a306",
|
|
||||||
defaultMaintenanceColor: "#1747f5",
|
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||||
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
.map(portValue => parseInt(portValue))
|
||||||
defaultStyle: "flat",
|
.find(portValue => !isNaN(portValue));
|
||||||
defaultPingValueSuffix: "ms",
|
|
||||||
defaultPingLabelSuffix: "h",
|
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||||
defaultUptimeValueSuffix: "%",
|
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||||
defaultUptimeLabelSuffix: "h",
|
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||||
defaultCertExpValueSuffix: " days",
|
|
||||||
defaultCertExpLabelSuffix: "h",
|
const isSSL = sslKey && sslCert;
|
||||||
// Values Come From Default Notification Times
|
|
||||||
defaultCertExpireWarnDays: "14",
|
function getLocalWebSocketURL() {
|
||||||
defaultCertExpireDownDays: "7"
|
const protocol = isSSL ? "wss" : "ws";
|
||||||
};
|
const host = hostname || "localhost";
|
||||||
|
return `${protocol}://${host}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localWebSocketURL = getLocalWebSocketURL();
|
||||||
|
|
||||||
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
args,
|
args,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
sslKey,
|
||||||
|
sslCert,
|
||||||
|
sslKeyPassphrase,
|
||||||
|
isSSL,
|
||||||
|
localWebSocketURL,
|
||||||
demoMode,
|
demoMode,
|
||||||
badgeConstants,
|
|
||||||
};
|
};
|
||||||
|
@@ -81,6 +81,10 @@ class Database {
|
|||||||
"patch-monitor-oauth-cc.sql": true,
|
"patch-monitor-oauth-cc.sql": true,
|
||||||
"patch-add-timeout-monitor.sql": true,
|
"patch-add-timeout-monitor.sql": true,
|
||||||
"patch-add-gamedig-given-port.sql": true,
|
"patch-add-gamedig-given-port.sql": true,
|
||||||
|
"patch-notification-config.sql": true,
|
||||||
|
"patch-fix-kafka-producer-booleans.sql": true,
|
||||||
|
"patch-timeout.sql": true,
|
||||||
|
"patch-monitor-tls-info-add-fk.sql": true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
|
const { escape } = require("html-escaper");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
|
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
|
||||||
@@ -7,15 +8,18 @@ const jsesc = require("jsesc");
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getGoogleAnalyticsScript(tagId) {
|
function getGoogleAnalyticsScript(tagId) {
|
||||||
let escapedTagId = jsesc(tagId, { isScriptContext: true });
|
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
|
||||||
|
|
||||||
if (escapedTagId) {
|
if (escapedTagIdJS) {
|
||||||
escapedTagId = escapedTagId.trim();
|
escapedTagIdJS = escapedTagIdJS.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape the tag ID for use in an HTML attribute.
|
||||||
|
let escapedTagIdHTMLAttribute = escape(tagId);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
|
||||||
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
|
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
|
|||||||
SQL_DATETIME_FORMAT
|
SQL_DATETIME_FORMAT
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
|
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||||
} = require("../util-server");
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
@@ -22,6 +22,9 @@ const { UptimeCacheList } = require("../uptime-cache-list");
|
|||||||
const Gamedig = require("gamedig");
|
const Gamedig = require("gamedig");
|
||||||
const jsonata = require("jsonata");
|
const jsonata = require("jsonata");
|
||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const rootCertificates = rootCertificatesFingerprints();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@@ -53,7 +56,7 @@ class Monitor extends BeanModel {
|
|||||||
obj.tags = await this.getTags();
|
obj.tags = await this.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (certExpiry && this.type === "http") {
|
if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") {
|
||||||
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
|
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
|
||||||
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
|
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
|
||||||
obj.validCert = validCert;
|
obj.validCert = validCert;
|
||||||
@@ -141,8 +144,8 @@ class Monitor extends BeanModel {
|
|||||||
expectedValue: this.expectedValue,
|
expectedValue: this.expectedValue,
|
||||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||||
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
kafkaProducerSsl: this.getKafkaProducerSsl(),
|
||||||
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
|
||||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||||
screenshot,
|
screenshot,
|
||||||
};
|
};
|
||||||
@@ -227,10 +230,12 @@ class Monitor extends BeanModel {
|
|||||||
/**
|
/**
|
||||||
* Encode user and password to Base64 encoding
|
* Encode user and password to Base64 encoding
|
||||||
* for HTTP "basic" auth, as per RFC-7617
|
* for HTTP "basic" auth, as per RFC-7617
|
||||||
|
* @param {string|null} user - The username (nullable if not changed by a user)
|
||||||
|
* @param {string|null} pass - The password (nullable if not changed by a user)
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
encodeBase64(user, pass) {
|
encodeBase64(user, pass) {
|
||||||
return Buffer.from(user + ":" + pass).toString("base64");
|
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -285,6 +290,22 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.gamedigGivenPortOnly);
|
return Boolean(this.gamedigGivenPortOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Kafka Producer Ssl enabled?
|
||||||
|
*/
|
||||||
|
getKafkaProducerSsl() {
|
||||||
|
return Boolean(this.kafkaProducerSsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Kafka Producer Allow Auto Topic Creation Enabled?
|
||||||
|
*/
|
||||||
|
getKafkaProducerAllowAutoTopicCreation() {
|
||||||
|
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitor
|
* Start monitor
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
@@ -339,6 +360,12 @@ class Monitor extends BeanModel {
|
|||||||
bean.duration = 0;
|
bean.duration = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runtime patch timeout if it is 0
|
||||||
|
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
|
||||||
|
if (!this.timeout || this.timeout <= 0) {
|
||||||
|
this.timeout = this.interval * 1000 * 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||||
bean.msg = "Monitor under maintenance";
|
bean.msg = "Monitor under maintenance";
|
||||||
@@ -409,6 +436,7 @@ class Monitor extends BeanModel {
|
|||||||
const httpsAgentOptions = {
|
const httpsAgentOptions = {
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
};
|
};
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||||
@@ -447,6 +475,7 @@ class Monitor extends BeanModel {
|
|||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
|
signal: axiosAbortSignal((this.timeout + 10) * 1000),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bodyValue) {
|
if (bodyValue) {
|
||||||
@@ -483,6 +512,18 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tlsInfo = {};
|
||||||
|
// Store tlsInfo when secureConnect event is emitted
|
||||||
|
// The keylog event listener is a workaround to access the tlsSocket
|
||||||
|
options.httpsAgent.once("keylog", async (line, tlsSocket) => {
|
||||||
|
tlsSocket.once("secureConnect", async () => {
|
||||||
|
tlsInfo = checkCertificate(tlsSocket);
|
||||||
|
tlsInfo.valid = tlsSocket.authorized || false;
|
||||||
|
|
||||||
|
await this.handleTlsInfo(tlsInfo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
|
|
||||||
@@ -492,30 +533,18 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
// Check certificate if https is used
|
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
|
||||||
let certInfoStartTime = dayjs().valueOf();
|
// e.g. if the connection is made through a proxy
|
||||||
if (this.getUrl()?.protocol === "https:") {
|
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
|
||||||
log.debug("monitor", `[${this.name}] Check cert`);
|
const tlsSocket = res.request.res.socket;
|
||||||
try {
|
|
||||||
let tlsInfoObject = checkCertificate(res);
|
|
||||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
|
||||||
|
|
||||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
if (tlsSocket) {
|
||||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
tlsInfo = checkCertificate(tlsSocket);
|
||||||
await this.checkCertExpiryNotifications(tlsInfoObject);
|
tlsInfo.valid = tlsSocket.authorized || false;
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
await this.handleTlsInfo(tlsInfo);
|
||||||
if (e.message !== "No TLS certificate in response") {
|
|
||||||
log.error("monitor", "Caught error");
|
|
||||||
log.error("monitor", e.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.TIMELOGGER === "1") {
|
|
||||||
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
||||||
log.info("monitor", res.data);
|
log.info("monitor", res.data);
|
||||||
@@ -549,8 +578,12 @@ class Monitor extends BeanModel {
|
|||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
|
||||||
// convert data to object
|
// convert data to object
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
|
||||||
|
try {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
|
} catch (_) {
|
||||||
|
// Failed to parse as JSON, just process it as a string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let expression = jsonata(this.jsonPath);
|
let expression = jsonata(this.jsonPath);
|
||||||
@@ -662,6 +695,7 @@ class Monitor extends BeanModel {
|
|||||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
}),
|
}),
|
||||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
maxCachedSessions: 0,
|
maxCachedSessions: 0,
|
||||||
@@ -704,8 +738,6 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type === "docker") {
|
} else if (this.type === "docker") {
|
||||||
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
||||||
|
|
||||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
url: `/containers/${this.docker_container}/json`,
|
url: `/containers/${this.docker_container}/json`,
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
@@ -716,12 +748,19 @@ class Monitor extends BeanModel {
|
|||||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
}),
|
}),
|
||||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
maxCachedSessions: 0,
|
maxCachedSessions: 0,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||||
|
|
||||||
|
if (!dockerHost) {
|
||||||
|
throw new Error("Failed to load docker host config");
|
||||||
|
}
|
||||||
|
|
||||||
if (dockerHost._dockerType === "socket") {
|
if (dockerHost._dockerType === "socket") {
|
||||||
options.socketPath = dockerHost._dockerDaemon;
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
} else if (dockerHost._dockerType === "tcp") {
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
@@ -756,7 +795,7 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type === "sqlserver") {
|
} else if (this.type === "sqlserver") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
|
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
|
||||||
|
|
||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
@@ -795,7 +834,7 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
|
||||||
|
|
||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
@@ -803,7 +842,11 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type === "mysql") {
|
} else if (this.type === "mysql") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
// Use `radius_password` as `password` field, since there are too many unnecessary fields
|
||||||
|
// TODO: rename `radius_password` to `password` later for general use
|
||||||
|
let mysqlPassword = this.radiusPassword;
|
||||||
|
|
||||||
|
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else if (this.type === "mongodb") {
|
} else if (this.type === "mongodb") {
|
||||||
@@ -891,7 +934,11 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
|
if (error?.name === "CanceledError") {
|
||||||
|
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||||
|
} else {
|
||||||
bean.msg = error.message;
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
// If UP come in here, it must be upside down mode
|
// If UP come in here, it must be upside down mode
|
||||||
// Just reset the retries
|
// Just reset the retries
|
||||||
@@ -1075,6 +1122,19 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: http: or https:
|
||||||
|
* @returns {(null|string)}
|
||||||
|
*/
|
||||||
|
getURLProtocol() {
|
||||||
|
const url = this.getUrl();
|
||||||
|
if (url) {
|
||||||
|
return this.getUrl().protocol;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store TLS info to database
|
* Store TLS info to database
|
||||||
* @param checkCertificateResult
|
* @param checkCertificateResult
|
||||||
@@ -1411,7 +1471,10 @@ class Monitor extends BeanModel {
|
|||||||
let certInfo = tlsInfoObject.certInfo;
|
let certInfo = tlsInfoObject.certInfo;
|
||||||
while (certInfo) {
|
while (certInfo) {
|
||||||
let subjectCN = certInfo.subject["CN"];
|
let subjectCN = certInfo.subject["CN"];
|
||||||
if (certInfo.daysRemaining > targetDays) {
|
if (rootCertificates.has(certInfo.fingerprint256)) {
|
||||||
|
log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||||
|
break;
|
||||||
|
} else if (certInfo.daysRemaining > targetDays) {
|
||||||
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||||
} else {
|
} else {
|
||||||
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
||||||
@@ -1619,6 +1682,21 @@ class Monitor extends BeanModel {
|
|||||||
const parentActive = await Monitor.isParentActive(parent.id);
|
const parentActive = await Monitor.isParentActive(parent.id);
|
||||||
return parent.active && parentActive;
|
return parent.active && parentActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store TLS certificate information and check for expiry
|
||||||
|
* @param {Object} tlsInfo Information about the TLS connection
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async handleTlsInfo(tlsInfo) {
|
||||||
|
await this.updateTlsInfo(tlsInfo);
|
||||||
|
this.prometheus?.update(null, tlsInfo);
|
||||||
|
|
||||||
|
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||||
|
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
||||||
|
await this.checkCertExpiryNotifications(tlsInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const passwordHash = require("../password-hash");
|
const passwordHash = require("../password-hash");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const { shake256, SHAKE256_LENGTH } = require("../util-server");
|
||||||
|
|
||||||
class User extends BeanModel {
|
class User extends BeanModel {
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +29,19 @@ class User extends BeanModel {
|
|||||||
this.password = newPassword;
|
this.password = newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new JWT for a user
|
||||||
|
* @param {User} user
|
||||||
|
* @param {string} jwtSecret
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static createJWT(user, jwtSecret) {
|
||||||
|
return jwt.sign({
|
||||||
|
username: user.username,
|
||||||
|
h: shake256(user.password, SHAKE256_LENGTH),
|
||||||
|
}, jwtSecret);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = User;
|
module.exports = User;
|
||||||
|
@@ -9,6 +9,10 @@ const Database = require("../database");
|
|||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached instance of a browser
|
||||||
|
* @type {import ("playwright-core").Browser}
|
||||||
|
*/
|
||||||
let browser = null;
|
let browser = null;
|
||||||
|
|
||||||
let allowedList = [];
|
let allowedList = [];
|
||||||
@@ -40,6 +44,7 @@ if (process.platform === "win32") {
|
|||||||
"/usr/bin/chromium",
|
"/usr/bin/chromium",
|
||||||
"/usr/bin/chromium-browser",
|
"/usr/bin/chromium-browser",
|
||||||
"/usr/bin/google-chrome",
|
"/usr/bin/google-chrome",
|
||||||
|
"/snap/bin/chromium", // Ubuntu
|
||||||
];
|
];
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||||
@@ -61,8 +66,15 @@ async function isAllowedChromeExecutable(executablePath) {
|
|||||||
return allowedList.includes(executablePath);
|
return allowedList.includes(executablePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current instance of the browser. If there isn't one, create
|
||||||
|
* it.
|
||||||
|
* @returns {Promise<import ("playwright-core").Browser>} The browser
|
||||||
|
*/
|
||||||
async function getBrowser() {
|
async function getBrowser() {
|
||||||
if (!browser) {
|
if (browser && browser.isConnected()) {
|
||||||
|
return browser;
|
||||||
|
} else {
|
||||||
let executablePath = await Settings.get("chromeExecutable");
|
let executablePath = await Settings.get("chromeExecutable");
|
||||||
|
|
||||||
executablePath = await prepareChromeExecutable(executablePath);
|
executablePath = await prepareChromeExecutable(executablePath);
|
||||||
@@ -71,9 +83,10 @@ async function getBrowser() {
|
|||||||
//headless: false,
|
//headless: false,
|
||||||
executablePath,
|
executablePath,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareChromeExecutable(executablePath) {
|
async function prepareChromeExecutable(executablePath) {
|
||||||
// Special code for using the playwright_chromium
|
// Special code for using the playwright_chromium
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const { UP, log } = require("../../src/util");
|
const { UP } = require("../../src/util");
|
||||||
const exec = require("child_process").exec;
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TailscalePing class extends the MonitorType.
|
* A TailscalePing class extends the MonitorType.
|
||||||
@@ -23,7 +23,6 @@ class TailscalePing extends MonitorType {
|
|||||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.debug("Tailscale", err);
|
|
||||||
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
||||||
throw new Error(`Error checking Tailscale ping: ${err}`);
|
throw new Error(`Error checking Tailscale ping: ${err}`);
|
||||||
}
|
}
|
||||||
@@ -33,30 +32,24 @@ class TailscalePing extends MonitorType {
|
|||||||
* Runs the Tailscale ping command to the given URL.
|
* Runs the Tailscale ping command to the given URL.
|
||||||
*
|
*
|
||||||
* @param {string} hostname - The hostname to ping.
|
* @param {string} hostname - The hostname to ping.
|
||||||
|
* @param {number} interval
|
||||||
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
||||||
* @throws Will throw an error if the command execution encounters any error.
|
* @throws Will throw an error if the command execution encounters any error.
|
||||||
*/
|
*/
|
||||||
async runTailscalePing(hostname, interval) {
|
async runTailscalePing(hostname, interval) {
|
||||||
let cmd = `tailscale ping ${hostname}`;
|
|
||||||
|
|
||||||
log.debug("Tailscale", cmd);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timeout = interval * 1000 * 0.8;
|
let timeout = interval * 1000 * 0.8;
|
||||||
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||||
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
timeout: timeout,
|
||||||
if (error) {
|
encoding: "utf8",
|
||||||
reject(`Execution error: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
reject(`Error in output: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
if (res.stderr && res.stderr.toString()) {
|
||||||
|
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||||
|
}
|
||||||
|
if (res.stdout && res.stdout.toString()) {
|
||||||
|
return res.stdout.toString();
|
||||||
|
} else {
|
||||||
|
throw new Error("No output from Tailscale ping");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +67,7 @@ class TailscalePing extends MonitorType {
|
|||||||
heartbeat.status = UP;
|
heartbeat.status = UP;
|
||||||
let time = line.split(" in ")[1].split(" ")[0];
|
let time = line.split(" in ")[1].split(" ")[0];
|
||||||
heartbeat.ping = parseInt(time);
|
heartbeat.ping = parseInt(time);
|
||||||
heartbeat.msg = line;
|
heartbeat.msg = "OK";
|
||||||
break;
|
break;
|
||||||
} else if (line.includes("timed out")) {
|
} else if (line.includes("timed out")) {
|
||||||
throw new Error(`Ping timed out: "${line}"`);
|
throw new Error(`Ping timed out: "${line}"`);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
class Apprise extends NotificationProvider {
|
class Apprise extends NotificationProvider {
|
||||||
|
|
||||||
@@ -11,7 +11,9 @@ class Apprise extends NotificationProvider {
|
|||||||
args.push("-t");
|
args.push("-t");
|
||||||
args.push(notification.title);
|
args.push(notification.title);
|
||||||
}
|
}
|
||||||
const s = childProcess.spawnSync("apprise", args);
|
const s = await childProcessAsync.spawn("apprise", args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
|
||||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ class DingDing extends NotificationProvider {
|
|||||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (this.sendToDingDing(notification, params)) {
|
if (await this.sendToDingDing(notification, params)) {
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -28,7 +28,7 @@ class DingDing extends NotificationProvider {
|
|||||||
content: msg
|
content: msg
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (this.sendToDingDing(notification, params)) {
|
if (await this.sendToDingDing(notification, params)) {
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ class DingDing extends NotificationProvider {
|
|||||||
if (result.data.errmsg === "ok") {
|
if (result.data.errmsg === "ok") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
throw new Error(result.data.errmsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -79,6 +79,7 @@ class Prometheus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (heartbeat) {
|
||||||
try {
|
try {
|
||||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -98,6 +99,7 @@ class Prometheus {
|
|||||||
log.error("prometheus", e);
|
log.error("prometheus", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Remove monitor from prometheus */
|
/** Remove monitor from prometheus */
|
||||||
remove() {
|
remove() {
|
||||||
|
@@ -1,15 +1,21 @@
|
|||||||
let express = require("express");
|
let express = require("express");
|
||||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
|
const {
|
||||||
|
setting,
|
||||||
|
allowDevAllOrigin,
|
||||||
|
allowAllOrigin,
|
||||||
|
percentageToColor,
|
||||||
|
filterAndJoin,
|
||||||
|
sendHttpError,
|
||||||
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
|
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||||
const { makeBadge } = require("badge-maker");
|
const { makeBadge } = require("badge-maker");
|
||||||
const { badgeConstants } = require("../config");
|
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
@@ -22,10 +28,14 @@ router.get("/api/entry-page", async (request, response) => {
|
|||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
let result = { };
|
let result = { };
|
||||||
|
let hostname = request.hostname;
|
||||||
|
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
|
||||||
|
hostname = request.headers["x-forwarded-host"];
|
||||||
|
}
|
||||||
|
|
||||||
if (request.hostname in StatusPage.domainMappingList) {
|
if (hostname in StatusPage.domainMappingList) {
|
||||||
result.type = "statusPageMatchedDomain";
|
result.type = "statusPageMatchedDomain";
|
||||||
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
|
result.statusPageSlug = StatusPage.domainMappingList[hostname];
|
||||||
} else {
|
} else {
|
||||||
result.type = "entryPage";
|
result.type = "entryPage";
|
||||||
result.entryPage = server.entryPage;
|
result.entryPage = server.entryPage;
|
||||||
@@ -38,7 +48,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
|
|
||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = parseInt(request.query.ping) || null;
|
let ping = parseFloat(request.query.ping) || null;
|
||||||
let statusString = request.query.status || "up";
|
let statusString = request.query.status || "up";
|
||||||
let status = (statusString === "up") ? UP : DOWN;
|
let status = (statusString === "up") ? UP : DOWN;
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ const StatusPage = require("../model/status_page");
|
|||||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const { badgeConstants } = require("../config");
|
const { badgeConstants } = require("../../src/util");
|
||||||
const { makeBadge } = require("badge-maker");
|
const { makeBadge } = require("badge-maker");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
@@ -48,9 +48,17 @@ if (! process.env.NODE_ENV) {
|
|||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
|
||||||
|
process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
|
||||||
|
}
|
||||||
|
|
||||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||||
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||||
|
|
||||||
|
if (process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass") {
|
||||||
|
log.warn("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("server", "Importing Node libraries");
|
log.info("server", "Importing Node libraries");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
@@ -76,15 +84,18 @@ const notp = require("notp");
|
|||||||
const base32 = require("thirty-two");
|
const base32 = require("thirty-two");
|
||||||
|
|
||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
const server = UptimeKumaServer.getInstance(args);
|
const server = UptimeKumaServer.getInstance();
|
||||||
const io = module.exports.io = server.io;
|
const io = module.exports.io = server.io;
|
||||||
const app = server.app;
|
const app = server.app;
|
||||||
|
|
||||||
log.info("server", "Importing this project modules");
|
log.info("server", "Importing this project modules");
|
||||||
log.debug("server", "Importing Monitor");
|
log.debug("server", "Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
|
const User = require("./model/user");
|
||||||
|
|
||||||
log.debug("server", "Importing Settings");
|
log.debug("server", "Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
|
||||||
|
} = require("./util-server");
|
||||||
|
|
||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
@@ -107,19 +118,13 @@ const passwordHash = require("./password-hash");
|
|||||||
const checkVersion = require("./check-version");
|
const checkVersion = require("./check-version");
|
||||||
log.info("server", "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.
|
const hostname = config.hostname;
|
||||||
// Dual-stack support for (::)
|
|
||||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
|
||||||
let hostEnv = FBSD ? null : process.env.HOST;
|
|
||||||
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
|
||||||
|
|
||||||
if (hostname) {
|
if (hostname) {
|
||||||
log.info("server", "Custom hostname: " + hostname);
|
log.info("server", "Custom hostname: " + hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
const port = config.port;
|
||||||
.map(portValue => parseInt(portValue))
|
|
||||||
.find(portValue => !isNaN(portValue));
|
|
||||||
|
|
||||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["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;
|
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||||
@@ -297,6 +302,11 @@ let needSetup = false;
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
// Check if the password changed
|
||||||
|
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
|
||||||
|
throw new Error("The token is invalid due to password change or old token");
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("auth", "afterLogin");
|
log.debug("auth", "afterLogin");
|
||||||
afterLogin(socket, user);
|
afterLogin(socket, user);
|
||||||
log.debug("auth", "afterLogin ok");
|
log.debug("auth", "afterLogin ok");
|
||||||
@@ -316,9 +326,10 @@ let needSetup = false;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Invalid token. IP=${clientIP}`);
|
log.error("auth", `Invalid token. IP=${clientIP}`);
|
||||||
|
if (error.message) {
|
||||||
|
log.error("auth", error.message, `IP=${clientIP}`);
|
||||||
|
}
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Invalid token.",
|
msg: "Invalid token.",
|
||||||
@@ -357,9 +368,7 @@ let needSetup = false;
|
|||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: User.createJWT(user, server.jwtSecret),
|
||||||
username: data.username,
|
|
||||||
}, server.jwtSecret),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,9 +396,7 @@ let needSetup = false;
|
|||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: User.createJWT(user, server.jwtSecret),
|
||||||
username: data.username,
|
|
||||||
}, server.jwtSecret),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@@ -726,11 +733,11 @@ let needSetup = false;
|
|||||||
bean.basic_auth_user = monitor.basic_auth_user;
|
bean.basic_auth_user = monitor.basic_auth_user;
|
||||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
bean.timeout = monitor.timeout;
|
bean.timeout = monitor.timeout;
|
||||||
bean.oauth_client_id = monitor.oauth_client_id,
|
bean.oauth_client_id = monitor.oauth_client_id;
|
||||||
bean.oauth_client_secret = monitor.oauth_client_secret,
|
bean.oauth_client_secret = monitor.oauth_client_secret;
|
||||||
bean.oauth_auth_method = this.oauth_auth_method,
|
bean.oauth_auth_method = monitor.oauth_auth_method;
|
||||||
bean.oauth_token_url = monitor.oauth_token_url,
|
bean.oauth_token_url = monitor.oauth_token_url;
|
||||||
bean.oauth_scopes = monitor.oauth_scopes,
|
bean.oauth_scopes = monitor.oauth_scopes;
|
||||||
bean.tlsCa = monitor.tlsCa;
|
bean.tlsCa = monitor.tlsCa;
|
||||||
bean.tlsCert = monitor.tlsCert;
|
bean.tlsCert = monitor.tlsCert;
|
||||||
bean.tlsKey = monitor.tlsKey;
|
bean.tlsKey = monitor.tlsKey;
|
||||||
@@ -784,6 +791,9 @@ let needSetup = false;
|
|||||||
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||||
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||||
|
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
|
||||||
|
bean.kafkaProducerAllowAutoTopicCreation =
|
||||||
|
monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
||||||
|
|
||||||
bean.validate();
|
bean.validate();
|
||||||
@@ -796,7 +806,7 @@ let needSetup = false;
|
|||||||
|
|
||||||
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
||||||
|
|
||||||
if (bean.isActive()) {
|
if (await bean.isActive()) {
|
||||||
await restartMonitor(socket.userID, bean.id);
|
await restartMonitor(socket.userID, bean.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,6 +1154,8 @@ let needSetup = false;
|
|||||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
await user.resetPassword(password.newPassword);
|
await user.resetPassword(password.newPassword);
|
||||||
|
|
||||||
|
server.disconnectAllSocketClients(user.id, socket.id);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Password has been updated successfully.",
|
msg: "Password has been updated successfully.",
|
||||||
@@ -1193,6 +1205,12 @@ let needSetup = false;
|
|||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log out all clients if enabling auth
|
||||||
|
// GHSA-23q2-5gf8-gjpp
|
||||||
|
if (currentDisabledAuth && !data.disableAuth) {
|
||||||
|
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||||
|
}
|
||||||
|
|
||||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||||
const previousNSCDStatus = await Settings.get("nscd");
|
const previousNSCDStatus = await Settings.get("nscd");
|
||||||
|
|
||||||
@@ -1215,9 +1233,9 @@ let needSetup = false;
|
|||||||
// Update nscd status
|
// Update nscd status
|
||||||
if (previousNSCDStatus !== data.nscd) {
|
if (previousNSCDStatus !== data.nscd) {
|
||||||
if (data.nscd) {
|
if (data.nscd) {
|
||||||
server.startNSCDServices();
|
await server.startNSCDServices();
|
||||||
} else {
|
} else {
|
||||||
server.stopNSCDServices();
|
await server.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1824,6 +1842,7 @@ async function pauseMonitor(userID, monitorID) {
|
|||||||
|
|
||||||
if (monitorID in server.monitorList) {
|
if (monitorID in server.monitorList) {
|
||||||
server.monitorList[monitorID].stop();
|
server.monitorList[monitorID].stop();
|
||||||
|
server.monitorList[monitorID].active = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1882,8 +1901,10 @@ gracefulShutdown(server.httpServer, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Catch unexpected errors here
|
// Catch unexpected errors here
|
||||||
process.addListener("unhandledRejection", (error, promise) => {
|
let unexpectedErrorHandler = (error, promise) => {
|
||||||
console.trace(error);
|
console.trace(error);
|
||||||
UptimeKumaServer.errorLog(error, false);
|
UptimeKumaServer.errorLog(error, false);
|
||||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
});
|
};
|
||||||
|
process.addListener("unhandledRejection", unexpectedErrorHandler);
|
||||||
|
process.addListener("uncaughtException", unexpectedErrorHandler);
|
||||||
|
@@ -42,13 +42,23 @@ module.exports.generalSocketHandler = (socket, server) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getGameList", async (callback) => {
|
socket.on("getGameList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
gameList: getGameList(),
|
gameList: getGameList(),
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("testChrome", (executable, callback) => {
|
socket.on("testChrome", (executable, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||||
testChrome(executable).then((version) => {
|
testChrome(executable).then((version) => {
|
||||||
callback({
|
callback({
|
||||||
@@ -61,5 +71,21 @@ module.exports.generalSocketHandler = (socket, server) => {
|
|||||||
msg: e.message,
|
msg: e.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disconnect all other socket clients of the user
|
||||||
|
socket.on("disconnectOtherSocketClients", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("disconnectAllSocketClients", e.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -147,7 +147,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
config.logo = `/upload/${filename}?t=` + Date.now();
|
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
config.icon = imgDataUrl;
|
config.logo = imgDataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
statusPage.slug = config.slug;
|
statusPage.slug = config.slug;
|
||||||
|
@@ -4,14 +4,15 @@ const fs = require("fs");
|
|||||||
const http = require("http");
|
const http = require("http");
|
||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { log } = require("../src/util");
|
const { log, isDev } = require("../src/util");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
const util = require("util");
|
const util = require("util");
|
||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
|
||||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,22 +63,17 @@ class UptimeKumaServer {
|
|||||||
*/
|
*/
|
||||||
jwtSecret = null;
|
jwtSecret = null;
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance() {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer();
|
||||||
}
|
}
|
||||||
return UptimeKumaServer.instance;
|
return UptimeKumaServer.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(args) {
|
constructor() {
|
||||||
// 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 sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
|
||||||
|
|
||||||
log.info("server", "Creating express and socket.io instance");
|
log.info("server", "Creating express and socket.io instance");
|
||||||
this.app = express();
|
this.app = express();
|
||||||
if (sslKey && sslCert) {
|
if (isSSL) {
|
||||||
log.info("server", "Server Type: HTTPS");
|
log.info("server", "Server Type: HTTPS");
|
||||||
this.httpServer = https.createServer({
|
this.httpServer = https.createServer({
|
||||||
key: fs.readFileSync(sslKey),
|
key: fs.readFileSync(sslKey),
|
||||||
@@ -103,7 +99,66 @@ class UptimeKumaServer {
|
|||||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||||
|
|
||||||
this.io = new Server(this.httpServer);
|
// Allow all CORS origins (polling) in development
|
||||||
|
let cors = undefined;
|
||||||
|
if (isDev) {
|
||||||
|
cors = {
|
||||||
|
origin: "*",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.io = new Server(this.httpServer, {
|
||||||
|
cors,
|
||||||
|
allowRequest: async (req, callback) => {
|
||||||
|
let transport;
|
||||||
|
// It should be always true, but just in case, because this property is not documented
|
||||||
|
if (req._query) {
|
||||||
|
transport = req._query.transport;
|
||||||
|
} else {
|
||||||
|
log.error("socket", "Ops!!! Cannot get transport type, assume that it is polling");
|
||||||
|
transport = "polling";
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientIP = await this.getClientIPwithProxy(req.connection.remoteAddress, req.headers);
|
||||||
|
log.info("socket", `New ${transport} connection, IP = ${clientIP}`);
|
||||||
|
|
||||||
|
// The following check is only for websocket connections, polling connections are already protected by CORS
|
||||||
|
if (transport === "polling") {
|
||||||
|
callback(null, true);
|
||||||
|
} else if (transport === "websocket") {
|
||||||
|
const bypass = process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
|
||||||
|
if (bypass) {
|
||||||
|
log.info("auth", "WebSocket origin check is bypassed");
|
||||||
|
callback(null, true);
|
||||||
|
} else if (!req.headers.origin) {
|
||||||
|
log.info("auth", "WebSocket with no origin is allowed");
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
let host = req.headers.host;
|
||||||
|
let origin = req.headers.origin;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let originURL = new URL(origin);
|
||||||
|
let xForwardedFor;
|
||||||
|
if (await Settings.get("trustProxy")) {
|
||||||
|
xForwardedFor = req.headers["x-forwarded-for"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host !== originURL.host && xForwardedFor !== originURL.host) {
|
||||||
|
callback(null, false);
|
||||||
|
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${clientIP}`);
|
||||||
|
} else {
|
||||||
|
callback(null, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid origin url, probably not from browser
|
||||||
|
callback(null, false);
|
||||||
|
log.error("auth", `Invalid origin url (${origin}), IP: ${clientIP}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initialise app after the database has been set up */
|
/** Initialise app after the database has been set up */
|
||||||
@@ -238,20 +293,28 @@ class UptimeKumaServer {
|
|||||||
/**
|
/**
|
||||||
* Get the IP of the client connected to the socket
|
* Get the IP of the client connected to the socket
|
||||||
* @param {Socket} socket
|
* @param {Socket} socket
|
||||||
* @returns {string}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async getClientIP(socket) {
|
getClientIP(socket) {
|
||||||
let clientIP = socket.client.conn.remoteAddress;
|
return this.getClientIPwithProxy(socket.client.conn.remoteAddress, socket.client.conn.request.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} clientIP
|
||||||
|
* @param {IncomingHttpHeaders} headers
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async getClientIPwithProxy(clientIP, headers) {
|
||||||
if (clientIP === undefined) {
|
if (clientIP === undefined) {
|
||||||
clientIP = "";
|
clientIP = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await Settings.get("trustProxy")) {
|
if (await Settings.get("trustProxy")) {
|
||||||
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
|
const forwardedFor = headers["x-forwarded-for"];
|
||||||
|
|
||||||
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
||||||
|| socket.client.conn.request.headers["x-real-ip"]
|
|| headers["x-real-ip"]
|
||||||
|| clientIP.replace(/^::ffff:/, "");
|
|| clientIP.replace(/^::ffff:/, "");
|
||||||
} else {
|
} else {
|
||||||
return clientIP.replace(/^::ffff:/, "");
|
return clientIP.replace(/^::ffff:/, "");
|
||||||
@@ -344,7 +407,7 @@ class UptimeKumaServer {
|
|||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.startNSCDServices();
|
await this.startNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +419,7 @@ class UptimeKumaServer {
|
|||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.stopNSCDServices();
|
await this.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,11 +427,11 @@ class UptimeKumaServer {
|
|||||||
* Start all system services (e.g. nscd)
|
* Start all system services (e.g. nscd)
|
||||||
* For now, only used in Docker
|
* For now, only used in Docker
|
||||||
*/
|
*/
|
||||||
startNSCDServices() {
|
async startNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Starting nscd");
|
log.info("services", "Starting nscd");
|
||||||
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
await childProcessAsync.exec("sudo service nscd start");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to start nscd");
|
log.info("services", "Failed to start nscd");
|
||||||
}
|
}
|
||||||
@@ -378,16 +441,35 @@ class UptimeKumaServer {
|
|||||||
/**
|
/**
|
||||||
* Stop all system services
|
* Stop all system services
|
||||||
*/
|
*/
|
||||||
stopNSCDServices() {
|
async stopNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Stopping nscd");
|
log.info("services", "Stopping nscd");
|
||||||
childProcess.execSync("sudo service nscd stop");
|
await childProcessAsync.exec("sudo service nscd stop");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to stop nscd");
|
log.info("services", "Failed to stop nscd");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force connected sockets of a user to refresh and disconnect.
|
||||||
|
* Used for resetting password.
|
||||||
|
* @param {string} userID
|
||||||
|
* @param {string?} currentSocketID
|
||||||
|
*/
|
||||||
|
disconnectAllSocketClients(userID, currentSocketID = undefined) {
|
||||||
|
for (const socket of this.io.sockets.sockets.values()) {
|
||||||
|
if (socket.userID === userID && socket.id !== currentSocketID) {
|
||||||
|
try {
|
||||||
|
socket.emit("refresh");
|
||||||
|
socket.disconnect();
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const tcpp = require("tcp-ping");
|
const tcpp = require("tcp-ping");
|
||||||
const ping = require("@louislam/ping");
|
const ping = require("@louislam/ping");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { log, genSecret } = require("../src/util");
|
const { log, genSecret, badgeConstants } = require("../src/util");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
@@ -9,7 +9,6 @@ const iconv = require("iconv-lite");
|
|||||||
const chardet = require("chardet");
|
const chardet = require("chardet");
|
||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
const chroma = require("chroma-js");
|
const chroma = require("chroma-js");
|
||||||
const { badgeConstants } = require("./config");
|
|
||||||
const mssql = require("mssql");
|
const mssql = require("mssql");
|
||||||
const { Client } = require("pg");
|
const { Client } = require("pg");
|
||||||
const postgresConParse = require("pg-connection-string").parse;
|
const postgresConParse = require("pg-connection-string").parse;
|
||||||
@@ -22,6 +21,7 @@ const protojs = require("protobufjs");
|
|||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
const redis = require("redis");
|
const redis = require("redis");
|
||||||
const oidc = require("openid-client");
|
const oidc = require("openid-client");
|
||||||
|
const tls = require("tls");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dictionaries: {
|
dictionaries: {
|
||||||
@@ -33,6 +33,7 @@ const dayjs = require("dayjs");
|
|||||||
// SASLOptions used in JSDoc
|
// SASLOptions used in JSDoc
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { Kafka, SASLOptions } = require("kafkajs");
|
const { Kafka, SASLOptions } = require("kafkajs");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const isWindows = process.platform === /^win/.test(process.platform);
|
const isWindows = process.platform === /^win/.test(process.platform);
|
||||||
/**
|
/**
|
||||||
@@ -286,22 +287,22 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
|
|||||||
|
|
||||||
producer.connect().then(
|
producer.connect().then(
|
||||||
() => {
|
() => {
|
||||||
try {
|
|
||||||
producer.send({
|
producer.send({
|
||||||
topic: topic,
|
topic: topic,
|
||||||
messages: [{
|
messages: [{
|
||||||
value: message,
|
value: message,
|
||||||
}],
|
}],
|
||||||
});
|
}).then((_) => {
|
||||||
connectedToKafka = true;
|
|
||||||
clearTimeout(timeoutID);
|
|
||||||
resolve("Message sent successfully");
|
resolve("Message sent successfully");
|
||||||
} catch (e) {
|
}).catch((e) => {
|
||||||
connectedToKafka = true;
|
connectedToKafka = true;
|
||||||
producer.disconnect();
|
producer.disconnect();
|
||||||
clearTimeout(timeoutID);
|
clearTimeout(timeoutID);
|
||||||
reject(new Error("Error sending message: " + e.message));
|
reject(new Error("Error sending message: " + e.message));
|
||||||
}
|
}).finally(() => {
|
||||||
|
connectedToKafka = true;
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
).catch(
|
).catch(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -313,8 +314,10 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
|
|||||||
);
|
);
|
||||||
|
|
||||||
producer.on("producer.network.request_timeout", (_) => {
|
producer.on("producer.network.request_timeout", (_) => {
|
||||||
|
if (!connectedToKafka) {
|
||||||
clearTimeout(timeoutID);
|
clearTimeout(timeoutID);
|
||||||
reject(new Error("producer.network.request_timeout"));
|
reject(new Error("producer.network.request_timeout"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
producer.on("producer.disconnect", (_) => {
|
producer.on("producer.disconnect", (_) => {
|
||||||
@@ -392,6 +395,9 @@ exports.mssqlQuery = async function (connectionString, query) {
|
|||||||
try {
|
try {
|
||||||
pool = new mssql.ConnectionPool(connectionString);
|
pool = new mssql.ConnectionPool(connectionString);
|
||||||
await pool.connect();
|
await pool.connect();
|
||||||
|
if (!query) {
|
||||||
|
query = "SELECT 1";
|
||||||
|
}
|
||||||
await pool.request().query(query);
|
await pool.request().query(query);
|
||||||
pool.close();
|
pool.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -412,12 +418,22 @@ exports.postgresQuery = function (connectionString, query) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const config = postgresConParse(connectionString);
|
const config = postgresConParse(connectionString);
|
||||||
|
|
||||||
if (config.password === "") {
|
// Fix #3868, which true/false is not parsed to boolean
|
||||||
// See https://github.com/brianc/node-postgres/issues/1927
|
if (typeof config.ssl === "string") {
|
||||||
return reject(new Error("Password is undefined."));
|
config.ssl = config.ssl === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new Client({ connectionString });
|
if (config.password === "") {
|
||||||
|
// See https://github.com/brianc/node-postgres/issues/1927
|
||||||
|
reject(new Error("Password is undefined."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = new Client(config);
|
||||||
|
|
||||||
|
client.on("error", (error) => {
|
||||||
|
log.debug("postgres", "Error caught in the error event handler.");
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
client.connect((err) => {
|
client.connect((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -441,6 +457,7 @@ exports.postgresQuery = function (connectionString, query) {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
client.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -452,11 +469,15 @@ exports.postgresQuery = function (connectionString, query) {
|
|||||||
* Run a query on MySQL/MariaDB
|
* Run a query on MySQL/MariaDB
|
||||||
* @param {string} connectionString The database connection string
|
* @param {string} connectionString The database connection string
|
||||||
* @param {string} query The query to validate the database with
|
* @param {string} query The query to validate the database with
|
||||||
|
* @param {?string} password The password to use
|
||||||
* @returns {Promise<(string)>}
|
* @returns {Promise<(string)>}
|
||||||
*/
|
*/
|
||||||
exports.mysqlQuery = function (connectionString, query) {
|
exports.mysqlQuery = function (connectionString, query, password = undefined) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const connection = mysql.createConnection(connectionString);
|
const connection = mysql.createConnection({
|
||||||
|
uri: connectionString,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
connection.on("error", (err) => {
|
connection.on("error", (err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -695,20 +716,27 @@ const parseCertificateInfo = function (info) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if certificate is valid
|
* Check if certificate is valid
|
||||||
* @param {Object} res Response object from axios
|
* @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected
|
||||||
* @returns {Object} Object containing certificate information
|
* @returns {Object} Object containing certificate information
|
||||||
*/
|
*/
|
||||||
exports.checkCertificate = function (res) {
|
exports.checkCertificate = function (socket) {
|
||||||
if (!res.request.res.socket) {
|
let certInfoStartTime = dayjs().valueOf();
|
||||||
throw new Error("No socket found");
|
|
||||||
|
// Return null if there is no socket
|
||||||
|
if (socket === undefined || socket == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = res.request.res.socket.getPeerCertificate(true);
|
const info = socket.getPeerCertificate(true);
|
||||||
const valid = res.request.res.socket.authorized || false;
|
const valid = socket.authorized || false;
|
||||||
|
|
||||||
log.debug("cert", "Parsing Certificate Info");
|
log.debug("cert", "Parsing Certificate Info");
|
||||||
const parsedInfo = parseCertificateInfo(info);
|
const parsedInfo = parseCertificateInfo(info);
|
||||||
|
|
||||||
|
if (process.env.TIMELOGGER === "1") {
|
||||||
|
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: valid,
|
valid: valid,
|
||||||
certInfo: parsedInfo
|
certInfo: parsedInfo
|
||||||
@@ -1053,6 +1081,47 @@ module.exports.grpcQuery = async (options) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of SHA256 fingerprints for all known root certificates.
|
||||||
|
* @returns {Set} A set of SHA256 fingerprints.
|
||||||
|
*/
|
||||||
|
module.exports.rootCertificatesFingerprints = () => {
|
||||||
|
let fingerprints = tls.rootCertificates.map(cert => {
|
||||||
|
let certLines = cert.split("\n");
|
||||||
|
certLines.shift();
|
||||||
|
certLines.pop();
|
||||||
|
let certBody = certLines.join("");
|
||||||
|
let buf = Buffer.from(certBody, "base64");
|
||||||
|
|
||||||
|
const shasum = crypto.createHash("sha256");
|
||||||
|
shasum.update(buf);
|
||||||
|
|
||||||
|
return shasum.digest("hex").toUpperCase().replace(/(.{2})(?!$)/g, "$1:");
|
||||||
|
});
|
||||||
|
|
||||||
|
fingerprints.push("6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F"); // ISRG X1 cross-signed with DST X3
|
||||||
|
fingerprints.push("8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01"); // ISRG X2 cross-signed with ISRG X1
|
||||||
|
|
||||||
|
return new Set(fingerprints);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.SHAKE256_LENGTH = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} data
|
||||||
|
* @param {number} len
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
module.exports.shake256 = (data, len) => {
|
||||||
|
if (!data) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return crypto.createHash("shake256", { outputLength: len })
|
||||||
|
.update(data)
|
||||||
|
.digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
// For unit test, export functions
|
// For unit test, export functions
|
||||||
if (process.env.TEST_BACKEND) {
|
if (process.env.TEST_BACKEND) {
|
||||||
module.exports.__test = {
|
module.exports.__test = {
|
||||||
@@ -1062,3 +1131,29 @@ if (process.env.TEST_BACKEND) {
|
|||||||
return module.exports.__test[functionName];
|
return module.exports.__test[functionName];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an abort signal with the specified timeout.
|
||||||
|
* @param {number} timeoutMs - The timeout in milliseconds.
|
||||||
|
* @returns {AbortSignal | null} - The generated abort signal, or null if not supported.
|
||||||
|
*/
|
||||||
|
module.exports.axiosAbortSignal = (timeoutMs) => {
|
||||||
|
try {
|
||||||
|
// Just in case, as 0 timeout here will cause the request to be aborted immediately
|
||||||
|
if (!timeoutMs || timeoutMs <= 0) {
|
||||||
|
timeoutMs = 5000;
|
||||||
|
}
|
||||||
|
return AbortSignal.timeout(timeoutMs);
|
||||||
|
} catch (_) {
|
||||||
|
// v16-: AbortSignal.timeout is not supported
|
||||||
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
setTimeout(() => abortController.abort(), timeoutMs);
|
||||||
|
|
||||||
|
return abortController.signal;
|
||||||
|
} catch (_) {
|
||||||
|
// v15-: AbortController is not supported
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -8,9 +8,9 @@
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
>
|
>
|
||||||
<a class="btn btn-outline-primary" @click="action()">
|
<button type="button" class="btn btn-outline-primary" :aria-label="actionAriaLabel" @click="action()">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,6 +66,13 @@ export default {
|
|||||||
action: {
|
action: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "update:modelValue" ],
|
emits: [ "update:modelValue" ],
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
|
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
||||||
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
|
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
<a class="btn btn-outline-primary" @click="action()">
|
<button type="button" class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" aria-hidden="true" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,6 +20,13 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* The id of the form which will be targeted by a <label for=..
|
||||||
|
*/
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* The value of the select field.
|
* The value of the select field.
|
||||||
*/
|
*/
|
||||||
@@ -50,6 +57,29 @@ export default {
|
|||||||
action: {
|
action: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the action button is disabled.
|
||||||
|
* @example true
|
||||||
|
*/
|
||||||
|
actionDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the select field is required.
|
||||||
|
* @example true
|
||||||
|
*/
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "update:modelValue" ],
|
emits: [ "update:modelValue" ],
|
||||||
|
@@ -135,7 +135,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import CopyableInput from "./CopyableInput.vue";
|
import CopyableInput from "./CopyableInput.vue";
|
||||||
import { default as serverConfig } from "../../server/config.js";
|
import { badgeConstants } from "../util.ts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -230,7 +230,7 @@ export default {
|
|||||||
"labelColor",
|
"labelColor",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
badgeConstants: serverConfig.badgeConstants,
|
badgeConstants,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -70,7 +70,7 @@ export default {
|
|||||||
Confirm,
|
Confirm,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
emits: [ "added" ],
|
emits: [ "added", "deleted" ],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
modal: null,
|
modal: null,
|
||||||
@@ -167,6 +167,7 @@ export default {
|
|||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
this.$emit("deleted", this.id);
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
||||||
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
||||||
>
|
>
|
||||||
<div>{{ timeSinceFirstBeat }} ago</div>
|
<div>{{ timeSinceFirstBeat }} ago</div>
|
||||||
|
@@ -29,10 +29,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
startDateTime() {
|
startDateTime() {
|
||||||
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||||
},
|
},
|
||||||
endDateTime() {
|
endDateTime() {
|
||||||
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -16,7 +16,10 @@
|
|||||||
</a>
|
</a>
|
||||||
<form>
|
<form>
|
||||||
<input
|
<input
|
||||||
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
|
v-model="searchText"
|
||||||
|
class="form-control search-input"
|
||||||
|
:placeholder="$t('Search...')"
|
||||||
|
:aria-label="$t('Search monitored sites')"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -62,7 +62,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="extra-info">
|
<div class="extra-info">
|
||||||
<div v-if="showCertificateExpiry && monitor.element.type === 'http'">
|
<div v-if="showCertificateExpiry && monitor.element.certExpiryDaysRemaining">
|
||||||
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
|
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showTags">
|
<div v-if="showTags">
|
||||||
|
@@ -13,6 +13,15 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||||
|
<div class="form-text">
|
||||||
|
<p v-if="$parent.notification.ntfyPriority >= 5">
|
||||||
|
{{ $t("ntfyPriorityHelptextAllEvents") }}
|
||||||
|
</p>
|
||||||
|
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
|
||||||
|
<code>DOWN</code>
|
||||||
|
<code>{{ $parent.notification.ntfyPriority + 1 }}</code>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>
|
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>
|
||||||
|
@@ -5,6 +5,14 @@
|
|||||||
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<i18n-t tag="div" keypath="Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent" class="form-text">
|
||||||
|
<template #localhost>
|
||||||
|
<code>localhost</code>
|
||||||
|
</template>
|
||||||
|
<template #local_mta>
|
||||||
|
<a href="https://wikipedia.org/wiki/Mail_Transfer_Agent" target="_blank">{{ $t("locally configured mail transfer agent") }}</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||||
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="settings.disableAuth"
|
||||||
|
class="mt-5 d-flex align-items-center justify-content-center my-3"
|
||||||
|
>
|
||||||
|
{{ $t("apiKeysDisabledMsg") }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<div class="add-btn">
|
<div class="add-btn">
|
||||||
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
|
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
|
||||||
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
|
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
|
||||||
@@ -7,7 +14,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span v-if="Object.keys(keyList).length === 0" class="d-flex align-items-center justify-content-center my-3">
|
<span
|
||||||
|
v-if="Object.keys(keyList).length === 0"
|
||||||
|
class="d-flex align-items-center justify-content-center my-3"
|
||||||
|
>
|
||||||
{{ $t("No API Keys") }}
|
{{ $t("No API Keys") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -18,9 +28,7 @@
|
|||||||
:class="item.status"
|
:class="item.status"
|
||||||
>
|
>
|
||||||
<div class="left-part">
|
<div class="left-part">
|
||||||
<div
|
<div class="circle"></div>
|
||||||
class="circle"
|
|
||||||
></div>
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">{{ item.name }}</div>
|
<div class="title">{{ item.name }}</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
@@ -30,7 +38,8 @@
|
|||||||
{{ $t("Created") }}: {{ item.createdDate }}
|
{{ $t("Created") }}: {{ item.createdDate }}
|
||||||
</div>
|
</div>
|
||||||
<div class="date">
|
<div class="date">
|
||||||
{{ $t("Expires") }}: {{ item.expires || $t("Never") }}
|
{{ $t("Expires") }}:
|
||||||
|
{{ item.expires || $t("Never") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,6 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-3" style="font-size: 13px;">
|
<div class="text-center mt-3" style="font-size: 13px;">
|
||||||
<a href="https://github.com/louislam/uptime-kuma/wiki/API-Keys" target="_blank">{{ $t("Learn More") }}</a>
|
<a href="https://github.com/louislam/uptime-kuma/wiki/API-Keys" target="_blank">{{ $t("Learn More") }}</a>
|
||||||
@@ -90,6 +100,9 @@ export default {
|
|||||||
let result = Object.values(this.$root.apiKeyList);
|
let result = Object.values(this.$root.apiKeyList);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -127,7 +140,9 @@ export default {
|
|||||||
* Pause maintenance
|
* Pause maintenance
|
||||||
*/
|
*/
|
||||||
disableKey() {
|
disableKey() {
|
||||||
this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => {
|
this.$root
|
||||||
|
.getSocket()
|
||||||
|
.emit("disableAPIKey", this.selectedKeyID, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@@ -27,13 +27,13 @@
|
|||||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||||
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
|
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeExpiryNotifDay(day)">
|
||||||
<font-awesome-icon class="" icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-6">
|
<div class="col-12 col-xl-6">
|
||||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
|
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||||
|
27
src/i18n.js
27
src/i18n.js
@@ -57,10 +57,29 @@ for (let lang in languageList) {
|
|||||||
|
|
||||||
const rtlLangs = [ "fa", "ar-SY", "ur" ];
|
const rtlLangs = [ "fa", "ar-SY", "ur" ];
|
||||||
|
|
||||||
export const currentLocale = () => localStorage.locale
|
/**
|
||||||
|| languageList[navigator.language] && navigator.language
|
* Find the best matching locale to display
|
||||||
|| languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2)
|
* If no locale can be matched, the default is "en"
|
||||||
|| "en";
|
* @returns {string} the locale that should be displayed
|
||||||
|
*/
|
||||||
|
export function currentLocale() {
|
||||||
|
for (const locale of [ localStorage.locale, navigator.language, ...navigator.languages ]) {
|
||||||
|
// localstorage might not have a value or there might not be a language in `navigator.language`
|
||||||
|
if (!locale) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (locale in messages) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
// some locales are further specified such as "en-US".
|
||||||
|
// If we only have a generic locale for this, we can use it too
|
||||||
|
const genericLocale = locale.split("-")[0];
|
||||||
|
if (genericLocale in messages) {
|
||||||
|
return genericLocale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
export const localeDirection = () => {
|
export const localeDirection = () => {
|
||||||
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";
|
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";
|
||||||
|
@@ -57,6 +57,8 @@
|
|||||||
"Friendly Name": "Friendly Name",
|
"Friendly Name": "Friendly Name",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"Hostname": "Hostname",
|
"Hostname": "Hostname",
|
||||||
|
"locally configured mail transfer agent": "locally configured mail transfer agent",
|
||||||
|
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}",
|
||||||
"Port": "Port",
|
"Port": "Port",
|
||||||
"Heartbeat Interval": "Heartbeat Interval",
|
"Heartbeat Interval": "Heartbeat Interval",
|
||||||
"Request Timeout": "Request Timeout",
|
"Request Timeout": "Request Timeout",
|
||||||
@@ -183,6 +185,7 @@
|
|||||||
"Pink": "Pink",
|
"Pink": "Pink",
|
||||||
"Custom": "Custom",
|
"Custom": "Custom",
|
||||||
"Search...": "Search…",
|
"Search...": "Search…",
|
||||||
|
"Search monitored sites": "Search monitored sites",
|
||||||
"Avg. Ping": "Avg. Ping",
|
"Avg. Ping": "Avg. Ping",
|
||||||
"Avg. Response": "Avg. Response",
|
"Avg. Response": "Avg. Response",
|
||||||
"Entry Page": "Entry Page",
|
"Entry Page": "Entry Page",
|
||||||
@@ -334,6 +337,8 @@
|
|||||||
"Fingerprint:": "Fingerprint:",
|
"Fingerprint:": "Fingerprint:",
|
||||||
"No status pages": "No status pages",
|
"No status pages": "No status pages",
|
||||||
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
||||||
|
"Add a new expiry notification day": "Add a new expiry notification day",
|
||||||
|
"Remove the expiry notification": "Remove the expiry notification day",
|
||||||
"Proxy": "Proxy",
|
"Proxy": "Proxy",
|
||||||
"Date Created": "Date Created",
|
"Date Created": "Date Created",
|
||||||
"Footer Text": "Footer Text",
|
"Footer Text": "Footer Text",
|
||||||
@@ -369,6 +374,8 @@
|
|||||||
"Setup Docker Host": "Setup Docker Host",
|
"Setup Docker Host": "Setup Docker Host",
|
||||||
"Connection Type": "Connection Type",
|
"Connection Type": "Connection Type",
|
||||||
"Docker Daemon": "Docker Daemon",
|
"Docker Daemon": "Docker Daemon",
|
||||||
|
"noDockerHostMsg": "Not Available. Setup a Docker Host First.",
|
||||||
|
"DockerHostRequired": "Please set the Docker Host for this monitor.",
|
||||||
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
||||||
"socket": "Socket",
|
"socket": "Socket",
|
||||||
"tcp": "TCP / HTTP",
|
"tcp": "TCP / HTTP",
|
||||||
@@ -654,6 +661,10 @@
|
|||||||
"Notify Channel": "Notify Channel",
|
"Notify Channel": "Notify Channel",
|
||||||
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
||||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
|
"setup a new monitor group": "setup a new monitor group",
|
||||||
|
"openModalTo": "open modal to {0}",
|
||||||
|
"Add a domain": "Add a domain",
|
||||||
|
"Remove domain": "Remove domain '{0}'",
|
||||||
"Icon Emoji": "Icon Emoji",
|
"Icon Emoji": "Icon Emoji",
|
||||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||||
"aboutWebhooks": "More info about Webhooks on: {0}",
|
"aboutWebhooks": "More info about Webhooks on: {0}",
|
||||||
@@ -747,6 +758,8 @@
|
|||||||
"lunaseaDeviceID": "Device ID",
|
"lunaseaDeviceID": "Device ID",
|
||||||
"lunaseaUserID": "User ID",
|
"lunaseaUserID": "User ID",
|
||||||
"ntfyAuthenticationMethod": "Authentication Method",
|
"ntfyAuthenticationMethod": "Authentication Method",
|
||||||
|
"ntfyPriorityHelptextAllEvents": "All events are send with the maximum priority",
|
||||||
|
"ntfyPriorityHelptextAllExceptDown": "All events are send with this priority, except {0}-events, which have a priority of {1}",
|
||||||
"ntfyUsernameAndPassword": "Username and Password",
|
"ntfyUsernameAndPassword": "Username and Password",
|
||||||
"twilioAccountSID": "Account SID",
|
"twilioAccountSID": "Account SID",
|
||||||
"twilioApiKey": "Api Key (optional)",
|
"twilioApiKey": "Api Key (optional)",
|
||||||
@@ -807,5 +820,6 @@
|
|||||||
"showCertificateExpiry": "Show Certificate Expiry",
|
"showCertificateExpiry": "Show Certificate Expiry",
|
||||||
"noOrBadCertificate": "No/Bad Certificate",
|
"noOrBadCertificate": "No/Bad Certificate",
|
||||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||||
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server."
|
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
|
||||||
|
"apiKeysDisabledMsg": "API keys are disabled because authentication is disabled."
|
||||||
}
|
}
|
||||||
|
@@ -91,21 +91,20 @@ export default {
|
|||||||
|
|
||||||
this.socket.initedSocketIO = true;
|
this.socket.initedSocketIO = true;
|
||||||
|
|
||||||
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
let protocol = location.protocol + "//";
|
||||||
|
|
||||||
let wsHost;
|
let url;
|
||||||
const env = process.env.NODE_ENV || "production";
|
const env = process.env.NODE_ENV || "production";
|
||||||
if (env === "development" && isDevContainer()) {
|
if (env === "development" && isDevContainer()) {
|
||||||
wsHost = protocol + getDevContainerServerHostname();
|
url = protocol + getDevContainerServerHostname();
|
||||||
} else if (env === "development" || localStorage.dev === "dev") {
|
} else if (env === "development" || localStorage.dev === "dev") {
|
||||||
wsHost = protocol + location.hostname + ":3001";
|
url = protocol + location.hostname + ":3001";
|
||||||
} else {
|
} else {
|
||||||
wsHost = protocol + location.host;
|
// Connect to the current url
|
||||||
|
url = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = io(wsHost, {
|
socket = io(url);
|
||||||
transports: [ "websocket" ],
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("info", (info) => {
|
socket.on("info", (info) => {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
@@ -288,6 +287,10 @@ export default {
|
|||||||
socket.on("initServerTimezone", () => {
|
socket.on("initServerTimezone", () => {
|
||||||
socket.emit("initServerTimezone", dayjs.tz.guess());
|
socket.emit("initServerTimezone", dayjs.tz.guess());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("refresh", () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -82,7 +82,7 @@
|
|||||||
<option value="redis">
|
<option value="redis">
|
||||||
Redis
|
Redis
|
||||||
</option>
|
</option>
|
||||||
<option v-if="$root.info.isContainer" value="tailscale-ping">
|
<option v-if="!$root.info.isContainer" value="tailscale-ping">
|
||||||
Tailscale Ping
|
Tailscale Ping
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
@@ -285,22 +285,19 @@
|
|||||||
<!-- Docker Host -->
|
<!-- Docker Host -->
|
||||||
<!-- For Docker Type -->
|
<!-- For Docker Type -->
|
||||||
<div v-if="monitor.type === 'docker'" class="my-3">
|
<div v-if="monitor.type === 'docker'" class="my-3">
|
||||||
<h2 class="mb-2">{{ $t("Docker Host") }}</h2>
|
<div class="mb-3">
|
||||||
<p v-if="$root.dockerHostList.length === 0">
|
|
||||||
{{ $t("Not available, please setup.") }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-else class="mb-3">
|
|
||||||
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
||||||
<select id="docket-host" v-model="monitor.docker_host" class="form-select">
|
<ActionSelect
|
||||||
<option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
|
id="docker-host"
|
||||||
</select>
|
v-model="monitor.docker_host"
|
||||||
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
|
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
|
||||||
|
:options="dockerHostOptionsList"
|
||||||
|
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
|
||||||
|
:icon="'plus'"
|
||||||
|
:action="() => $refs.dockerHostDialog.show()"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
|
|
||||||
{{ $t("Setup Docker Host") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MQTT -->
|
<!-- MQTT -->
|
||||||
@@ -370,11 +367,20 @@
|
|||||||
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
|
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="monitor.type === 'mysql'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
|
||||||
|
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
|
||||||
|
<HiddenInput id="mysql-password" v-model="monitor.radiusPassword" autocomplete="false"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- SQL Server / PostgreSQL / MySQL -->
|
<!-- SQL Server / PostgreSQL / MySQL -->
|
||||||
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
|
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
|
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
|
||||||
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'select getdate()' ])" required></textarea>
|
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'SELECT 1' ])"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -401,7 +407,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timeout: HTTP / Keyword only -->
|
<!-- Timeout: HTTP / Keyword only -->
|
||||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'" class="my-3">
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'" class="my-3">
|
||||||
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
|
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
|
||||||
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
|
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
|
||||||
</div>
|
</div>
|
||||||
@@ -460,7 +466,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTTP / Keyword only -->
|
<!-- HTTP / Keyword only -->
|
||||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
|
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' ">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
||||||
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||||
@@ -494,9 +500,11 @@
|
|||||||
|
|
||||||
<!-- Parent Monitor -->
|
<!-- Parent Monitor -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
|
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
id="monitorGroupSelector"
|
||||||
v-model="monitor.parent"
|
v-model="monitor.parent"
|
||||||
|
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
|
||||||
:options="parentMonitorOptionsList"
|
:options="parentMonitorOptionsList"
|
||||||
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
@@ -840,9 +848,9 @@ import NotificationDialog from "../components/NotificationDialog.vue";
|
|||||||
import DockerHostDialog from "../components/DockerHostDialog.vue";
|
import DockerHostDialog from "../components/DockerHostDialog.vue";
|
||||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||||
import TagsManager from "../components/TagsManager.vue";
|
import TagsManager from "../components/TagsManager.vue";
|
||||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
|
||||||
import { hostNameRegexPattern } from "../util-frontend";
|
import { hostNameRegexPattern } from "../util-frontend";
|
||||||
import { sleep } from "../util";
|
import HiddenInput from "../components/HiddenInput.vue";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -855,7 +863,7 @@ const monitorDefaults = {
|
|||||||
interval: 60,
|
interval: 60,
|
||||||
retryInterval: 60,
|
retryInterval: 60,
|
||||||
resendInterval: 0,
|
resendInterval: 0,
|
||||||
maxretries: 1,
|
maxretries: 0,
|
||||||
timeout: 48,
|
timeout: 48,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
@@ -880,11 +888,14 @@ const monitorDefaults = {
|
|||||||
kafkaProducerSaslOptions: {
|
kafkaProducerSaslOptions: {
|
||||||
mechanism: "None",
|
mechanism: "None",
|
||||||
},
|
},
|
||||||
|
kafkaProducerSsl: false,
|
||||||
|
kafkaProducerAllowAutoTopicCreation: false,
|
||||||
gamedigGivenPortOnly: true,
|
gamedigGivenPortOnly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
HiddenInput,
|
||||||
ActionSelect,
|
ActionSelect,
|
||||||
ProxyDialog,
|
ProxyDialog,
|
||||||
CopyableInput,
|
CopyableInput,
|
||||||
@@ -1106,6 +1117,21 @@ message HealthCheckResponse {
|
|||||||
return list;
|
return list;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dockerHostOptionsList() {
|
||||||
|
if (this.$root.dockerHostList && this.$root.dockerHostList.length > 0) {
|
||||||
|
return this.$root.dockerHostList.map((host) => {
|
||||||
|
return {
|
||||||
|
label: host.name,
|
||||||
|
value: host.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return [{
|
||||||
|
label: this.$t("noDockerHostMsg"),
|
||||||
|
value: null,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$root.proxyList"() {
|
"$root.proxyList"() {
|
||||||
@@ -1338,6 +1364,12 @@ message HealthCheckResponse {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.monitor.type === "docker") {
|
||||||
|
if (this.monitor.docker_host == null) {
|
||||||
|
toast.error(this.$t("DockerHostRequired"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -69,13 +69,17 @@
|
|||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
{{ $t("Domain Names") }}
|
{{ $t("Domain Names") }}
|
||||||
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
|
||||||
|
<font-awesome-icon icon="plus-circle" class="action text-primary" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<ul class="list-group domain-name-list">
|
<ul class="list-group domain-name-list">
|
||||||
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
||||||
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
||||||
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
|
||||||
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +106,7 @@
|
|||||||
|
|
||||||
<!-- Sidebar Footer -->
|
<!-- Sidebar Footer -->
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button class="btn btn-success me-2" @click="save">
|
<button class="btn btn-success me-2" :disabled="loading" @click="save">
|
||||||
<font-awesome-icon icon="save" />
|
<font-awesome-icon icon="save" />
|
||||||
{{ $t("Save") }}
|
{{ $t("Save") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -438,6 +442,7 @@ export default {
|
|||||||
lastUpdateTime: dayjs(),
|
lastUpdateTime: dayjs(),
|
||||||
updateCountdown: null,
|
updateCountdown: null,
|
||||||
updateCountdownText: null,
|
updateCountdownText: null,
|
||||||
|
loading: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -697,6 +702,8 @@ export default {
|
|||||||
this.incident = res.data.incident;
|
this.incident = res.data.incident;
|
||||||
this.maintenanceList = res.data.maintenanceList;
|
this.maintenanceList = res.data.maintenanceList;
|
||||||
this.$root.publicGroupList = res.data.publicGroupList;
|
this.$root.publicGroupList = res.data.publicGroupList;
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
}).catch( function (error) {
|
}).catch( function (error) {
|
||||||
if (error.response.status === 404) {
|
if (error.response.status === 404) {
|
||||||
location.href = "/page-not-found";
|
location.href = "/page-not-found";
|
||||||
@@ -806,6 +813,7 @@ export default {
|
|||||||
|
|
||||||
/** Save the status page */
|
/** Save the status page */
|
||||||
save() {
|
save() {
|
||||||
|
this.loading = true;
|
||||||
let startTime = new Date();
|
let startTime = new Date();
|
||||||
this.config.slug = this.config.slug.trim().toLowerCase();
|
this.config.slug = this.config.slug.trim().toLowerCase();
|
||||||
|
|
||||||
@@ -823,10 +831,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
location.href = "/status/" + this.config.slug;
|
location.href = "/status/" + this.config.slug;
|
||||||
}, time);
|
}, time);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
this.loading = false;
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
47
src/util.js
47
src/util.js
@@ -6,9 +6,12 @@
|
|||||||
//
|
//
|
||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||||
const dayjs = require("dayjs");
|
const dayjs_1 = __importDefault(require("dayjs"));
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
exports.appName = "Uptime Kuma";
|
exports.appName = "Uptime Kuma";
|
||||||
exports.DOWN = 0;
|
exports.DOWN = 0;
|
||||||
@@ -24,6 +27,25 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
|||||||
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
||||||
exports.MAX_INTERVAL_SECOND = 2073600; // 24 days
|
exports.MAX_INTERVAL_SECOND = 2073600; // 24 days
|
||||||
exports.MIN_INTERVAL_SECOND = 20; // 20 seconds
|
exports.MIN_INTERVAL_SECOND = 20; // 20 seconds
|
||||||
|
exports.badgeConstants = {
|
||||||
|
naColor: "#999",
|
||||||
|
defaultUpColor: "#66c20a",
|
||||||
|
defaultWarnColor: "#eed202",
|
||||||
|
defaultDownColor: "#c2290a",
|
||||||
|
defaultPendingColor: "#f8a306",
|
||||||
|
defaultMaintenanceColor: "#1747f5",
|
||||||
|
defaultPingColor: "blue",
|
||||||
|
defaultStyle: "flat",
|
||||||
|
defaultPingValueSuffix: "ms",
|
||||||
|
defaultPingLabelSuffix: "h",
|
||||||
|
defaultUptimeValueSuffix: "%",
|
||||||
|
defaultUptimeLabelSuffix: "h",
|
||||||
|
defaultCertExpValueSuffix: " days",
|
||||||
|
defaultCertExpLabelSuffix: "h",
|
||||||
|
// Values Come From Default Notification Times
|
||||||
|
defaultCertExpireWarnDays: "14",
|
||||||
|
defaultCertExpireDownDays: "7"
|
||||||
|
};
|
||||||
/** Flip the status of s */
|
/** Flip the status of s */
|
||||||
function flipStatus(s) {
|
function flipStatus(s) {
|
||||||
if (s === exports.UP) {
|
if (s === exports.UP) {
|
||||||
@@ -101,17 +123,20 @@ class Logger {
|
|||||||
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
||||||
*/
|
*/
|
||||||
log(module, msg, level) {
|
log(module, msg, level) {
|
||||||
|
if (level === "DEBUG" && !exports.isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
module = module.toUpperCase();
|
module = module.toUpperCase();
|
||||||
level = level.toUpperCase();
|
level = level.toUpperCase();
|
||||||
let now;
|
let now;
|
||||||
if (dayjs.tz) {
|
if (dayjs_1.default.tz) {
|
||||||
now = dayjs.tz(new Date()).format();
|
now = dayjs_1.default.tz(new Date()).format();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
now = dayjs().format();
|
now = (0, dayjs_1.default)().format();
|
||||||
}
|
}
|
||||||
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
|
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
|
||||||
if (level === "INFO") {
|
if (level === "INFO") {
|
||||||
@@ -200,7 +225,7 @@ function polyfill() {
|
|||||||
exports.polyfill = polyfill;
|
exports.polyfill = polyfill;
|
||||||
class TimeLogger {
|
class TimeLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.startTime = dayjs().valueOf();
|
this.startTime = (0, dayjs_1.default)().valueOf();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Output time since start of monitor
|
* Output time since start of monitor
|
||||||
@@ -208,7 +233,7 @@ class TimeLogger {
|
|||||||
*/
|
*/
|
||||||
print(name) {
|
print(name) {
|
||||||
if (exports.isDev && process.env.TIMELOGGER === "1") {
|
if (exports.isDev && process.env.TIMELOGGER === "1") {
|
||||||
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
|
console.log(name + ": " + ((0, dayjs_1.default)().valueOf() - this.startTime) + "ms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,21 +397,21 @@ exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
|
|||||||
* @returns ISO Date time
|
* @returns ISO Date time
|
||||||
*/
|
*/
|
||||||
function isoToUTCDateTime(input) {
|
function isoToUTCDateTime(input) {
|
||||||
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
|
return (0, dayjs_1.default)(input).utc().format(exports.SQL_DATETIME_FORMAT);
|
||||||
}
|
}
|
||||||
exports.isoToUTCDateTime = isoToUTCDateTime;
|
exports.isoToUTCDateTime = isoToUTCDateTime;
|
||||||
/**
|
/**
|
||||||
* @param input
|
* @param input
|
||||||
*/
|
*/
|
||||||
function utcToISODateTime(input) {
|
function utcToISODateTime(input) {
|
||||||
return dayjs.utc(input).toISOString();
|
return dayjs_1.default.utc(input).toISOString();
|
||||||
}
|
}
|
||||||
exports.utcToISODateTime = utcToISODateTime;
|
exports.utcToISODateTime = utcToISODateTime;
|
||||||
/**
|
/**
|
||||||
* For SQL_DATETIME_FORMAT
|
* For SQL_DATETIME_FORMAT
|
||||||
*/
|
*/
|
||||||
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
|
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||||
return dayjs.utc(input).local().format(format);
|
return dayjs_1.default.utc(input).local().format(format);
|
||||||
}
|
}
|
||||||
exports.utcToLocal = utcToLocal;
|
exports.utcToLocal = utcToLocal;
|
||||||
/**
|
/**
|
||||||
@@ -396,6 +421,6 @@ exports.utcToLocal = utcToLocal;
|
|||||||
* @returns Date in requested format
|
* @returns Date in requested format
|
||||||
*/
|
*/
|
||||||
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
|
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||||
return dayjs(input).utc().format(format);
|
return (0, dayjs_1.default)(input).utc().format(format);
|
||||||
}
|
}
|
||||||
exports.localToUTC = localToUTC;
|
exports.localToUTC = localToUTC;
|
||||||
|
26
src/util.ts
26
src/util.ts
@@ -6,7 +6,7 @@
|
|||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
|
|
||||||
import * as dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import * as timezone from "dayjs/plugin/timezone";
|
import * as timezone from "dayjs/plugin/timezone";
|
||||||
import * as utc from "dayjs/plugin/utc";
|
import * as utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
@@ -29,6 +29,26 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
|||||||
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
|
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
|
||||||
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
|
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
|
||||||
|
|
||||||
|
export const badgeConstants = {
|
||||||
|
naColor: "#999",
|
||||||
|
defaultUpColor: "#66c20a",
|
||||||
|
defaultWarnColor: "#eed202",
|
||||||
|
defaultDownColor: "#c2290a",
|
||||||
|
defaultPendingColor: "#f8a306",
|
||||||
|
defaultMaintenanceColor: "#1747f5",
|
||||||
|
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||||
|
defaultStyle: "flat",
|
||||||
|
defaultPingValueSuffix: "ms",
|
||||||
|
defaultPingLabelSuffix: "h",
|
||||||
|
defaultUptimeValueSuffix: "%",
|
||||||
|
defaultUptimeLabelSuffix: "h",
|
||||||
|
defaultCertExpValueSuffix: " days",
|
||||||
|
defaultCertExpLabelSuffix: "h",
|
||||||
|
// Values Come From Default Notification Times
|
||||||
|
defaultCertExpireWarnDays: "14",
|
||||||
|
defaultCertExpireDownDays: "7"
|
||||||
|
};
|
||||||
|
|
||||||
/** Flip the status of s */
|
/** Flip the status of s */
|
||||||
export function flipStatus(s: number) {
|
export function flipStatus(s: number) {
|
||||||
if (s === UP) {
|
if (s === UP) {
|
||||||
@@ -115,6 +135,10 @@ class Logger {
|
|||||||
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
||||||
*/
|
*/
|
||||||
log(module: string, msg: any, level: string) {
|
log(module: string, msg: any, level: string) {
|
||||||
|
if (level === "DEBUG" && !isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -3,42 +3,62 @@ import { currentLocale } from "../../../src/i18n";
|
|||||||
describe("Test i18n.js", () => {
|
describe("Test i18n.js", () => {
|
||||||
|
|
||||||
it("currentLocale()", () => {
|
it("currentLocale()", () => {
|
||||||
const setLanguage = (language) => {
|
const setLanguages = (languages) => {
|
||||||
Object.defineProperty(window.navigator, 'language', {
|
Object.defineProperty(navigator, 'language', {
|
||||||
value: language,
|
value: languages[0],
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
Object.defineProperty(navigator, 'languages', {
|
||||||
|
value: languages,
|
||||||
writable: true
|
writable: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLanguage('en-EN');
|
|
||||||
|
|
||||||
|
setLanguages(['en-EN']);
|
||||||
expect(currentLocale()).equal("en");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
setLanguage('zh-HK');
|
setLanguages(['zh-HK']);
|
||||||
expect(currentLocale()).equal("zh-HK");
|
expect(currentLocale()).equal("zh-HK");
|
||||||
|
|
||||||
// Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
|
// Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
|
||||||
setLanguage('zh-hk');
|
setLanguages(['zh-hk']);
|
||||||
expect(currentLocale()).equal("en");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
setLanguage('en-US');
|
setLanguages(['en-US']);
|
||||||
expect(currentLocale()).equal("en");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
setLanguage('ja-ZZ');
|
setLanguages(['ja-ZZ']);
|
||||||
expect(currentLocale()).equal("ja");
|
expect(currentLocale()).equal("ja");
|
||||||
|
|
||||||
setLanguage('zz-ZZ');
|
setLanguages(['zz-ZZ']);
|
||||||
expect(currentLocale()).equal("en");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
setLanguage('zz-ZZ');
|
setLanguages(['zz-ZZ']);
|
||||||
expect(currentLocale()).equal("en");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
setLanguage('en');
|
setLanguages(['en-US', 'en', 'pl', 'ja']);
|
||||||
localStorage.locale = "en";
|
|
||||||
expect(currentLocale()).equal("en");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
localStorage.locale = "zh-HK";
|
setLanguages(['en-US', 'pl', 'ja']);
|
||||||
expect(currentLocale()).equal("zh-HK");
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
|
setLanguages(['abc', 'en-US', 'pl', 'ja']);
|
||||||
|
expect(currentLocale()).equal("en");
|
||||||
|
|
||||||
|
setLanguages(['fil-PH', 'pl']);
|
||||||
|
expect(currentLocale()).equal("pl");
|
||||||
|
|
||||||
|
setLanguages(['shi-Latn-MA', 'pl']);
|
||||||
|
expect(currentLocale()).equal("pl");
|
||||||
|
|
||||||
|
setLanguages(['pl']);
|
||||||
|
localStorage.locale = "ja-ZZ";
|
||||||
|
expect(currentLocale()).equal("ja");
|
||||||
|
|
||||||
|
setLanguages(['pl']);
|
||||||
|
localStorage.locale = "invalid-lang";
|
||||||
|
expect(currentLocale()).equal("pl");
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@@ -11,7 +11,8 @@
|
|||||||
"removeComments": false,
|
"removeComments": false,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"./src/util.ts"
|
"./src/util.ts"
|
||||||
|
Reference in New Issue
Block a user