mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-04 08:25:57 +00:00
Merge branch 'master' into feature-slow-response-notification
This commit is contained in:
commit
555f09f1e8
39 changed files with 2413 additions and 5297 deletions
17
.eslintrc.js
17
.eslintrc.js
|
@ -150,23 +150,6 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override for jest puppeteer
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"**/*.spec.js",
|
|
||||||
"**/*.spec.jsx"
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
page: true,
|
|
||||||
browser: true,
|
|
||||||
context: true,
|
|
||||||
jestPuppeteer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Override for TypeScript
|
// Override for TypeScript
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
2
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
|
@ -6,7 +6,7 @@ body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: no-duplicate-issues
|
id: no-duplicate-issues
|
||||||
attributes:
|
attributes:
|
||||||
label: "⚠️ Please verify that this bug has NOT been raised before."
|
label: "⚠️ Please verify that this question has NOT been raised before."
|
||||||
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/uptime-kuma/issues?q=)"
|
||||||
options:
|
options:
|
||||||
- label: "I checked and didn't find similar issue"
|
- label: "I checked and didn't find similar issue"
|
||||||
|
|
31
.github/workflows/auto-test.yml
vendored
31
.github/workflows/auto-test.yml
vendored
|
@ -15,7 +15,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-test:
|
auto-test:
|
||||||
needs: [ check-linters ]
|
needs: [ check-linters, e2e-test ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- run: npm install npm@9 -g
|
- run: npm install npm@9 -g
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm run test-backend
|
||||||
env:
|
env:
|
||||||
HEADLESS_TEST: 1
|
HEADLESS_TEST: 1
|
||||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||||
|
@ -78,33 +78,18 @@ jobs:
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run lint:prod
|
- run: npm run lint:prod
|
||||||
|
|
||||||
# TODO: Temporarily disable, as it cannot pass the test in 2.0.0 yet
|
e2e-test:
|
||||||
# e2e-tests:
|
|
||||||
# needs: [ check-linters ]
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - run: git config --global core.autocrlf false # Mainly for Windows
|
|
||||||
# - uses: actions/checkout@v4
|
|
||||||
#
|
|
||||||
# - name: Use Node.js 14
|
|
||||||
# uses: actions/setup-node@v4
|
|
||||||
# with:
|
|
||||||
# node-version: 14
|
|
||||||
# - run: npm install
|
|
||||||
# - run: npm run build
|
|
||||||
# - run: npm run cy:test
|
|
||||||
|
|
||||||
frontend-unit-tests:
|
|
||||||
needs: [ check-linters ]
|
needs: [ check-linters ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ARM64
|
||||||
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@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 14
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 20
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
- run: npx playwright install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:run:unit
|
- run: npm run test-e2e
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -15,9 +15,6 @@ dist-ssr
|
||||||
/tmp
|
/tmp
|
||||||
.env
|
.env
|
||||||
|
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
|
|
||||||
/extra/healthcheck.exe
|
/extra/healthcheck.exe
|
||||||
/extra/healthcheck
|
/extra/healthcheck
|
||||||
/extra/healthcheck-armv7
|
/extra/healthcheck-armv7
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma?style=flat" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||||
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
||||||
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -17,9 +17,9 @@ Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||||
|
|
||||||
Try it!
|
Try it!
|
||||||
|
|
||||||
- Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
|
Demo Server (Location: Frankfurt - Germany): https://demo.kuma.pet/start-demo
|
||||||
|
|
||||||
It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
|
It is a temporary live demo, all data will be deleted after 10 minutes. Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors).
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
|
|
9
compose.yaml
Normal file
9
compose.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
uptime-kuma:
|
||||||
|
image: louislam/uptime-kuma:1
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
ports:
|
||||||
|
# <Host Port>:<Container Port>
|
||||||
|
- 3001:3001
|
||||||
|
restart: unless-stopped
|
60
config/playwright.config.js
Normal file
60
config/playwright.config.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const port = 30001;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Look for test files in the "tests" directory, relative to this configuration file.
|
||||||
|
testDir: "../test/e2e",
|
||||||
|
outputDir: "../private/playwright-test-results",
|
||||||
|
fullyParallel: false,
|
||||||
|
locale: "en-US",
|
||||||
|
|
||||||
|
// Fail the build on CI if you accidentally left test.only in the source code.
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
|
||||||
|
// Retry on CI only.
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
|
||||||
|
// Opt out of parallel tests on CI.
|
||||||
|
workers: 1,
|
||||||
|
|
||||||
|
// Reporter to use
|
||||||
|
reporter: [
|
||||||
|
[
|
||||||
|
"html", {
|
||||||
|
outputFolder: "../private/playwright-report",
|
||||||
|
open: "never",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
use: {
|
||||||
|
// Base URL to use in actions like `await page.goto('/')`.
|
||||||
|
baseURL: url,
|
||||||
|
|
||||||
|
// Collect trace when retrying the failed test.
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configure projects for major browsers.
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: { browserName: "firefox" }
|
||||||
|
},*/
|
||||||
|
],
|
||||||
|
|
||||||
|
// Run your local dev server before starting the tests.
|
||||||
|
webServer: {
|
||||||
|
command: `node extra/remove-playwright-test-data.js && node server/server.js --port=${port} --data-dir=./data/playwright-test`,
|
||||||
|
url,
|
||||||
|
reuseExistingServer: false,
|
||||||
|
cwd: "../",
|
||||||
|
},
|
||||||
|
});
|
24
db/knex_migrations/2023-12-21-0000-stat-ping-min-max.js
Normal file
24
db/knex_migrations/2023-12-21-0000-stat-ping-min-max.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("stat_daily", function (table) {
|
||||||
|
table.float("ping_min").notNullable().defaultTo(0).comment("Minimum ping during this period in milliseconds");
|
||||||
|
table.float("ping_max").notNullable().defaultTo(0).comment("Maximum ping during this period in milliseconds");
|
||||||
|
})
|
||||||
|
.alterTable("stat_minutely", function (table) {
|
||||||
|
table.float("ping_min").notNullable().defaultTo(0).comment("Minimum ping during this period in milliseconds");
|
||||||
|
table.float("ping_max").notNullable().defaultTo(0).comment("Maximum ping during this period in milliseconds");
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("stat_daily", function (table) {
|
||||||
|
table.dropColumn("ping_min");
|
||||||
|
table.dropColumn("ping_max");
|
||||||
|
})
|
||||||
|
.alterTable("stat_minutely", function (table) {
|
||||||
|
table.dropColumn("ping_min");
|
||||||
|
table.dropColumn("ping_max");
|
||||||
|
});
|
||||||
|
};
|
26
db/knex_migrations/2023-12-22-0000-hourly-uptime.js
Normal file
26
db/knex_migrations/2023-12-22-0000-hourly-uptime.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable("stat_hourly", function (table) {
|
||||||
|
table.increments("id");
|
||||||
|
table.comment("This table contains the hourly aggregate statistics for each monitor");
|
||||||
|
table.integer("monitor_id").unsigned().notNullable()
|
||||||
|
.references("id").inTable("monitor")
|
||||||
|
.onDelete("CASCADE")
|
||||||
|
.onUpdate("CASCADE");
|
||||||
|
table.integer("timestamp")
|
||||||
|
.notNullable()
|
||||||
|
.comment("Unix timestamp rounded down to the nearest hour");
|
||||||
|
table.float("ping").notNullable().comment("Average ping in milliseconds");
|
||||||
|
table.float("ping_min").notNullable().defaultTo(0).comment("Minimum ping during this period in milliseconds");
|
||||||
|
table.float("ping_max").notNullable().defaultTo(0).comment("Maximum ping during this period in milliseconds");
|
||||||
|
table.smallint("up").notNullable();
|
||||||
|
table.smallint("down").notNullable();
|
||||||
|
|
||||||
|
table.unique([ "monitor_id", "timestamp" ]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.dropTable("stat_hourly");
|
||||||
|
};
|
|
@ -1,15 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
uptime-kuma:
|
|
||||||
image: louislam/uptime-kuma:1
|
|
||||||
container_name: uptime-kuma
|
|
||||||
volumes:
|
|
||||||
- uptime-kuma:/app/data
|
|
||||||
ports:
|
|
||||||
- "3001:3001" # <Host Port>:<Container Port>
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
uptime-kuma:
|
|
||||||
|
|
6
extra/remove-playwright-test-data.js
Normal file
6
extra/remove-playwright-test-data.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
fs.rmSync("./data/playwright-test", {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
6423
package-lock.json
generated
6423
package-lock.json
generated
File diff suppressed because it is too large
Load diff
34
package.json
34
package.json
|
@ -25,12 +25,15 @@
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"start-server-dev:watch": "cross-env NODE_ENV=development node --watch server/server.js",
|
"start-server-dev:watch": "cross-env NODE_ENV=development node --watch server/server.js",
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "node test/prepare-test-server.js && npm run test-backend",
|
"test": "npm run test-backend && npm run test-e2e",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
"test-backend": "node test/backend-test-entry.js && npm run jest-backend",
|
"test-backend": "node test/backend-test-entry.js",
|
||||||
"test-backend:14": "cross-env TEST_BACKEND=1 NODE_OPTIONS=\"--experimental-abortcontroller --no-warnings\" node--test test/backend-test",
|
"test-backend:14": "cross-env TEST_BACKEND=1 NODE_OPTIONS=\"--experimental-abortcontroller --no-warnings\" node--test test/backend-test",
|
||||||
"test-backend:18": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
"test-backend:18": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
||||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
|
"test-e2e": "playwright test --config ./config/playwright.config.js",
|
||||||
|
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
|
||||||
|
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
|
||||||
|
"playwright-show-report": "playwright show-report ./private/playwright-report",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
"build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim",
|
"build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim",
|
||||||
|
@ -46,7 +49,7 @@
|
||||||
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
|
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
|
||||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --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.10 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.23.11 && 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",
|
||||||
|
@ -62,10 +65,6 @@
|
||||||
"git-remove-tag": "git tag -d",
|
"git-remove-tag": "git tag -d",
|
||||||
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||||
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||||
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
|
|
||||||
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
|
|
||||||
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.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",
|
||||||
|
@ -98,8 +97,8 @@
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "^4.2.0",
|
"gamedig": "^4.2.0",
|
||||||
"http-cookie-agent": "~5.0.4",
|
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
|
"http-cookie-agent": "~5.0.4",
|
||||||
"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",
|
||||||
|
@ -128,7 +127,7 @@
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"pg": "~8.11.3",
|
"pg": "~8.11.3",
|
||||||
"pg-connection-string": "~2.6.2",
|
"pg-connection-string": "~2.6.2",
|
||||||
"playwright-core": "~1.35.1",
|
"playwright-core": "~1.39.0",
|
||||||
"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",
|
"promisify-child-process": "~4.1.2",
|
||||||
|
@ -152,12 +151,14 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
|
"@playwright/test": "~1.39.0",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"@types/bootstrap": "~5.1.9",
|
"@types/bootstrap": "~5.1.9",
|
||||||
|
"@types/node": "^20.8.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||||
"@typescript-eslint/parser": "^6.7.5",
|
"@typescript-eslint/parser": "^6.7.5",
|
||||||
"@vitejs/plugin-vue": "~4.2.3",
|
"@vitejs/plugin-vue": "~5.0.1",
|
||||||
"@vue/compiler-sfc": "~3.3.4",
|
"@vue/compiler-sfc": "~3.4.2",
|
||||||
"@vuepic/vue-datepicker": "~3.4.8",
|
"@vuepic/vue-datepicker": "~3.4.8",
|
||||||
"aedes": "^0.46.3",
|
"aedes": "^0.46.3",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
|
@ -168,7 +169,6 @@
|
||||||
"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": "^13.2.0",
|
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"dompurify": "~2.4.3",
|
"dompurify": "~2.4.3",
|
||||||
|
@ -176,7 +176,7 @@
|
||||||
"eslint-plugin-jsdoc": "~46.4.6",
|
"eslint-plugin-jsdoc": "~46.4.6",
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
"jest": "~29.6.1",
|
"get-port-please": "^3.1.1",
|
||||||
"marked": "~4.2.5",
|
"marked": "~4.2.5",
|
||||||
"node-ssh": "~13.1.0",
|
"node-ssh": "~13.1.0",
|
||||||
"postcss-html": "~1.5.0",
|
"postcss-html": "~1.5.0",
|
||||||
|
@ -193,9 +193,9 @@
|
||||||
"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.0.10",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "~3.3.4",
|
"vue": "~3.4.2",
|
||||||
"vue-chartjs": "~5.2.0",
|
"vue-chartjs": "~5.2.0",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
|
@ -204,7 +204,7 @@
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-prism-editor": "~2.0.0-alpha.2",
|
"vue-prism-editor": "~2.0.0-alpha.2",
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.2.5",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^7.2.0",
|
"wait-on": "^7.2.0",
|
||||||
|
|
|
@ -451,9 +451,7 @@ class Monitor extends BeanModel {
|
||||||
if (this.auth_method === "oauth2-cc") {
|
if (this.auth_method === "oauth2-cc") {
|
||||||
try {
|
try {
|
||||||
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
|
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
|
||||||
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
|
this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest();
|
||||||
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
|
||||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
|
|
||||||
}
|
}
|
||||||
oauth2AuthHeader = {
|
oauth2AuthHeader = {
|
||||||
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
||||||
|
@ -1089,18 +1087,35 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a single attempt to obtain an new access token in the event that
|
||||||
|
* the recent api request failed for authentication purposes
|
||||||
|
*/
|
||||||
|
if (this.auth_method === "oauth2-cc" && error.response.status === 401 && !finalCall) {
|
||||||
|
this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest();
|
||||||
|
let oauth2AuthHeader = {
|
||||||
|
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
||||||
|
};
|
||||||
|
options.headers = { ...(options.headers),
|
||||||
|
...(oauth2AuthHeader)
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.makeAxiosRequest(options, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Fix #2253
|
// Fix #2253
|
||||||
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
|
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
|
||||||
if (!finalCall && typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
|
if (!finalCall && typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) {
|
||||||
log.debug("monitor", "makeAxiosRequest with gzip");
|
log.debug("monitor", "makeAxiosRequest with gzip");
|
||||||
options.headers["Accept-Encoding"] = "gzip, deflate";
|
options.headers["Accept-Encoding"] = "gzip, deflate";
|
||||||
return this.makeAxiosRequest(options, true);
|
return this.makeAxiosRequest(options, true);
|
||||||
} else {
|
} else {
|
||||||
if (typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
|
if (typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) {
|
||||||
e.message = "response timeout: incomplete response within a interval";
|
error.message = "response timeout: incomplete response within a interval";
|
||||||
}
|
}
|
||||||
throw e;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1810,6 +1825,23 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains a new Oidc Token
|
||||||
|
* @returns {Promise<object>} OAuthProvider client
|
||||||
|
*/
|
||||||
|
async makeOidcTokenClientCredentialsRequest() {
|
||||||
|
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new token`);
|
||||||
|
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
||||||
|
if (this.oauthAccessToken?.expires_at) {
|
||||||
|
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken?.expires_at * 1000)}`);
|
||||||
|
} else {
|
||||||
|
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Time until expiry was not provided`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return oAuthAccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
|
|
@ -25,8 +25,14 @@ class User extends BeanModel {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async resetPassword(newPassword) {
|
async resetPassword(newPassword) {
|
||||||
await User.resetPassword(this.id, newPassword);
|
const hashedPassword = passwordHash.generate(newPassword);
|
||||||
this.password = newPassword;
|
|
||||||
|
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||||
|
hashedPassword,
|
||||||
|
this.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.password = hashedPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,7 +38,8 @@ class TailscalePing extends MonitorType {
|
||||||
async runTailscalePing(hostname, interval) {
|
async runTailscalePing(hostname, interval) {
|
||||||
let timeout = interval * 1000 * 0.8;
|
let timeout = interval * 1000 * 0.8;
|
||||||
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||||
timeout: timeout
|
timeout: timeout,
|
||||||
|
encoding: "utf8",
|
||||||
});
|
});
|
||||||
if (res.stderr && res.stderr.toString()) {
|
if (res.stderr && res.stderr.toString()) {
|
||||||
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||||
|
|
|
@ -14,7 +14,9 @@ class Apprise extends NotificationProvider {
|
||||||
args.push("-t");
|
args.push("-t");
|
||||||
args.push(notification.title);
|
args.push(notification.title);
|
||||||
}
|
}
|
||||||
const s = await childProcessAsync.spawn("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";
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ const Monitor = require("./model/monitor");
|
||||||
const User = require("./model/user");
|
const User = require("./model/user");
|
||||||
|
|
||||||
log.debug("server", "Importing Settings");
|
log.debug("server", "Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
|
||||||
} = require("./util-server");
|
} = require("./util-server");
|
||||||
|
|
||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
|
@ -130,7 +130,6 @@ const twoFAVerifyOptions = {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
const testMode = !!args["test"] || false;
|
const testMode = !!args["test"] || false;
|
||||||
const e2eTestMode = !!args["e2e"] || false;
|
|
||||||
|
|
||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
|
||||||
|
@ -1278,6 +1277,7 @@ let needSetup = false;
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
token: User.createJWT(user, server.jwtSecret),
|
||||||
msg: "successAuthChangePassword",
|
msg: "successAuthChangePassword",
|
||||||
msgi18n: true,
|
msgi18n: true,
|
||||||
});
|
});
|
||||||
|
@ -1500,6 +1500,14 @@ let needSetup = false;
|
||||||
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
|
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
|
||||||
|
|
||||||
await R.exec("DELETE FROM heartbeat");
|
await R.exec("DELETE FROM heartbeat");
|
||||||
|
await R.exec("DELETE FROM stat_daily");
|
||||||
|
await R.exec("DELETE FROM stat_hourly");
|
||||||
|
await R.exec("DELETE FROM stat_minutely");
|
||||||
|
|
||||||
|
// Restart all monitors to reset the stats
|
||||||
|
for (let monitorID in server.monitorList) {
|
||||||
|
await restartMonitor(socket.userID, monitorID);
|
||||||
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -1559,10 +1567,6 @@ let needSetup = false;
|
||||||
}
|
}
|
||||||
startMonitors();
|
startMonitors();
|
||||||
checkVersion.startInterval();
|
checkVersion.startInterval();
|
||||||
|
|
||||||
if (e2eTestMode) {
|
|
||||||
startE2eTests();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await initBackgroundJobs();
|
await initBackgroundJobs();
|
||||||
|
|
|
@ -34,16 +34,25 @@ class UptimeCalculator {
|
||||||
*/
|
*/
|
||||||
minutelyUptimeDataList = new LimitQueue(24 * 60);
|
minutelyUptimeDataList = new LimitQueue(24 * 60);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recent 30-day uptime, each item is a 1-hour interval
|
||||||
|
* Key: {number} DivisionKey
|
||||||
|
* @type {LimitQueue<number,string>}
|
||||||
|
*/
|
||||||
|
hourlyUptimeDataList = new LimitQueue(30 * 24);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daily uptime data,
|
* Daily uptime data,
|
||||||
* Key: {number} DailyKey
|
* Key: {number} DailyKey
|
||||||
*/
|
*/
|
||||||
dailyUptimeDataList = new LimitQueue(365);
|
dailyUptimeDataList = new LimitQueue(365);
|
||||||
|
|
||||||
lastDailyUptimeData = null;
|
|
||||||
lastUptimeData = null;
|
lastUptimeData = null;
|
||||||
|
lastHourlyUptimeData = null;
|
||||||
|
lastDailyUptimeData = null;
|
||||||
|
|
||||||
lastDailyStatBean = null;
|
lastDailyStatBean = null;
|
||||||
|
lastHourlyStatBean = null;
|
||||||
lastMinutelyStatBean = null;
|
lastMinutelyStatBean = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,6 +62,10 @@ class UptimeCalculator {
|
||||||
* @returns {Promise<UptimeCalculator>} UptimeCalculator
|
* @returns {Promise<UptimeCalculator>} UptimeCalculator
|
||||||
*/
|
*/
|
||||||
static async getUptimeCalculator(monitorID) {
|
static async getUptimeCalculator(monitorID) {
|
||||||
|
if (!monitorID) {
|
||||||
|
throw new Error("Monitor ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
if (!UptimeCalculator.list[monitorID]) {
|
if (!UptimeCalculator.list[monitorID]) {
|
||||||
UptimeCalculator.list[monitorID] = new UptimeCalculator();
|
UptimeCalculator.list[monitorID] = new UptimeCalculator();
|
||||||
await UptimeCalculator.list[monitorID].init(monitorID);
|
await UptimeCalculator.list[monitorID].init(monitorID);
|
||||||
|
@ -108,13 +121,32 @@ class UptimeCalculator {
|
||||||
up: bean.up,
|
up: bean.up,
|
||||||
down: bean.down,
|
down: bean.down,
|
||||||
avgPing: bean.ping,
|
avgPing: bean.ping,
|
||||||
|
minPing: bean.pingMin,
|
||||||
|
maxPing: bean.pingMax,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load hourly data from database (recent 30 days only)
|
||||||
|
let hourlyStatBeans = await R.find("stat_hourly", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
||||||
|
monitorID,
|
||||||
|
this.getHourlyKey(now.subtract(30, "day")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of hourlyStatBeans) {
|
||||||
|
let key = bean.timestamp;
|
||||||
|
this.hourlyUptimeDataList.push(key, {
|
||||||
|
up: bean.up,
|
||||||
|
down: bean.down,
|
||||||
|
avgPing: bean.ping,
|
||||||
|
minPing: bean.pingMin,
|
||||||
|
maxPing: bean.pingMax,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load daily data from database (recent 365 days only)
|
// Load daily data from database (recent 365 days only)
|
||||||
let dailyStatBeans = await R.find("stat_daily", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
let dailyStatBeans = await R.find("stat_daily", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
||||||
monitorID,
|
monitorID,
|
||||||
this.getDailyKey(now.subtract(365, "day").unix()),
|
this.getDailyKey(now.subtract(365, "day")),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (let bean of dailyStatBeans) {
|
for (let bean of dailyStatBeans) {
|
||||||
|
@ -123,6 +155,8 @@ class UptimeCalculator {
|
||||||
up: bean.up,
|
up: bean.up,
|
||||||
down: bean.down,
|
down: bean.down,
|
||||||
avgPing: bean.ping,
|
avgPing: bean.ping,
|
||||||
|
minPing: bean.pingMin,
|
||||||
|
maxPing: bean.pingMax,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,13 +182,16 @@ class UptimeCalculator {
|
||||||
}
|
}
|
||||||
|
|
||||||
let divisionKey = this.getMinutelyKey(date);
|
let divisionKey = this.getMinutelyKey(date);
|
||||||
let dailyKey = this.getDailyKey(divisionKey);
|
let hourlyKey = this.getHourlyKey(date);
|
||||||
|
let dailyKey = this.getDailyKey(date);
|
||||||
|
|
||||||
let minutelyData = this.minutelyUptimeDataList[divisionKey];
|
let minutelyData = this.minutelyUptimeDataList[divisionKey];
|
||||||
|
let hourlyData = this.hourlyUptimeDataList[hourlyKey];
|
||||||
let dailyData = this.dailyUptimeDataList[dailyKey];
|
let dailyData = this.dailyUptimeDataList[dailyKey];
|
||||||
|
|
||||||
if (flatStatus === UP) {
|
if (flatStatus === UP) {
|
||||||
minutelyData.up += 1;
|
minutelyData.up += 1;
|
||||||
|
hourlyData.up += 1;
|
||||||
dailyData.up += 1;
|
dailyData.up += 1;
|
||||||
|
|
||||||
// Only UP status can update the ping
|
// Only UP status can update the ping
|
||||||
|
@ -163,32 +200,57 @@ class UptimeCalculator {
|
||||||
// The first beat of the minute, the ping is the current ping
|
// The first beat of the minute, the ping is the current ping
|
||||||
if (minutelyData.up === 1) {
|
if (minutelyData.up === 1) {
|
||||||
minutelyData.avgPing = ping;
|
minutelyData.avgPing = ping;
|
||||||
|
minutelyData.minPing = ping;
|
||||||
|
minutelyData.maxPing = ping;
|
||||||
} else {
|
} else {
|
||||||
minutelyData.avgPing = (minutelyData.avgPing * (minutelyData.up - 1) + ping) / minutelyData.up;
|
minutelyData.avgPing = (minutelyData.avgPing * (minutelyData.up - 1) + ping) / minutelyData.up;
|
||||||
|
minutelyData.minPing = Math.min(minutelyData.minPing, ping);
|
||||||
|
minutelyData.maxPing = Math.max(minutelyData.maxPing, ping);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add avg ping
|
||||||
|
// The first beat of the hour, the ping is the current ping
|
||||||
|
if (hourlyData.up === 1) {
|
||||||
|
hourlyData.avgPing = ping;
|
||||||
|
hourlyData.minPing = ping;
|
||||||
|
hourlyData.maxPing = ping;
|
||||||
|
} else {
|
||||||
|
hourlyData.avgPing = (hourlyData.avgPing * (hourlyData.up - 1) + ping) / hourlyData.up;
|
||||||
|
hourlyData.minPing = Math.min(hourlyData.minPing, ping);
|
||||||
|
hourlyData.maxPing = Math.max(hourlyData.maxPing, ping);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add avg ping (daily)
|
// Add avg ping (daily)
|
||||||
// The first beat of the day, the ping is the current ping
|
// The first beat of the day, the ping is the current ping
|
||||||
if (minutelyData.up === 1) {
|
if (dailyData.up === 1) {
|
||||||
dailyData.avgPing = ping;
|
dailyData.avgPing = ping;
|
||||||
|
dailyData.minPing = ping;
|
||||||
|
dailyData.maxPing = ping;
|
||||||
} else {
|
} else {
|
||||||
dailyData.avgPing = (dailyData.avgPing * (dailyData.up - 1) + ping) / dailyData.up;
|
dailyData.avgPing = (dailyData.avgPing * (dailyData.up - 1) + ping) / dailyData.up;
|
||||||
|
dailyData.minPing = Math.min(dailyData.minPing, ping);
|
||||||
|
dailyData.maxPing = Math.max(dailyData.maxPing, ping);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
minutelyData.down += 1;
|
minutelyData.down += 1;
|
||||||
|
hourlyData.down += 1;
|
||||||
dailyData.down += 1;
|
dailyData.down += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dailyData !== this.lastDailyUptimeData) {
|
|
||||||
this.lastDailyUptimeData = dailyData;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minutelyData !== this.lastUptimeData) {
|
if (minutelyData !== this.lastUptimeData) {
|
||||||
this.lastUptimeData = minutelyData;
|
this.lastUptimeData = minutelyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hourlyData !== this.lastHourlyUptimeData) {
|
||||||
|
this.lastHourlyUptimeData = hourlyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dailyData !== this.lastDailyUptimeData) {
|
||||||
|
this.lastDailyUptimeData = dailyData;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't store data in test mode
|
// Don't store data in test mode
|
||||||
if (process.env.TEST_BACKEND) {
|
if (process.env.TEST_BACKEND) {
|
||||||
log.debug("uptime-calc", "Skip storing data in test mode");
|
log.debug("uptime-calc", "Skip storing data in test mode");
|
||||||
|
@ -199,12 +261,24 @@ class UptimeCalculator {
|
||||||
dailyStatBean.up = dailyData.up;
|
dailyStatBean.up = dailyData.up;
|
||||||
dailyStatBean.down = dailyData.down;
|
dailyStatBean.down = dailyData.down;
|
||||||
dailyStatBean.ping = dailyData.avgPing;
|
dailyStatBean.ping = dailyData.avgPing;
|
||||||
|
dailyStatBean.pingMin = dailyData.minPing;
|
||||||
|
dailyStatBean.pingMax = dailyData.maxPing;
|
||||||
await R.store(dailyStatBean);
|
await R.store(dailyStatBean);
|
||||||
|
|
||||||
|
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
|
||||||
|
hourlyStatBean.up = hourlyData.up;
|
||||||
|
hourlyStatBean.down = hourlyData.down;
|
||||||
|
hourlyStatBean.ping = hourlyData.avgPing;
|
||||||
|
hourlyStatBean.pingMin = hourlyData.minPing;
|
||||||
|
hourlyStatBean.pingMax = hourlyData.maxPing;
|
||||||
|
await R.store(hourlyStatBean);
|
||||||
|
|
||||||
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
||||||
minutelyStatBean.up = minutelyData.up;
|
minutelyStatBean.up = minutelyData.up;
|
||||||
minutelyStatBean.down = minutelyData.down;
|
minutelyStatBean.down = minutelyData.down;
|
||||||
minutelyStatBean.ping = minutelyData.avgPing;
|
minutelyStatBean.ping = minutelyData.avgPing;
|
||||||
|
minutelyStatBean.pingMin = minutelyData.minPing;
|
||||||
|
minutelyStatBean.pingMax = minutelyData.maxPing;
|
||||||
await R.store(minutelyStatBean);
|
await R.store(minutelyStatBean);
|
||||||
|
|
||||||
// Remove the old data
|
// Remove the old data
|
||||||
|
@ -214,6 +288,11 @@ class UptimeCalculator {
|
||||||
this.getMinutelyKey(date.subtract(24, "hour")),
|
this.getMinutelyKey(date.subtract(24, "hour")),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
|
||||||
|
this.monitorID,
|
||||||
|
this.getHourlyKey(date.subtract(30, "day")),
|
||||||
|
]);
|
||||||
|
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,6 +321,31 @@ class UptimeCalculator {
|
||||||
return this.lastDailyStatBean;
|
return this.lastDailyStatBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hourly stat bean
|
||||||
|
* @param {number} timestamp milliseconds
|
||||||
|
* @returns {Promise<import("redbean-node").Bean>} stat_hourly bean
|
||||||
|
*/
|
||||||
|
async getHourlyStatBean(timestamp) {
|
||||||
|
if (this.lastHourlyStatBean && this.lastHourlyStatBean.timestamp === timestamp) {
|
||||||
|
return this.lastHourlyStatBean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bean = await R.findOne("stat_hourly", " monitor_id = ? AND timestamp = ?", [
|
||||||
|
this.monitorID,
|
||||||
|
timestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
bean = R.dispense("stat_hourly");
|
||||||
|
bean.monitor_id = this.monitorID;
|
||||||
|
bean.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastHourlyStatBean = bean;
|
||||||
|
return this.lastHourlyStatBean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the minutely stat bean
|
* Get the minutely stat bean
|
||||||
* @param {number} timestamp milliseconds
|
* @param {number} timestamp milliseconds
|
||||||
|
@ -268,11 +372,12 @@ class UptimeCalculator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Convert timestamp to minutely key
|
||||||
* @param {dayjs.Dayjs} date The heartbeat date
|
* @param {dayjs.Dayjs} date The heartbeat date
|
||||||
* @returns {number} Timestamp
|
* @returns {number} Timestamp
|
||||||
*/
|
*/
|
||||||
getMinutelyKey(date) {
|
getMinutelyKey(date) {
|
||||||
// Convert the current date to the nearest minute (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
|
// Truncate value to minutes (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
|
||||||
date = date.startOf("minute");
|
date = date.startOf("minute");
|
||||||
|
|
||||||
// Convert to timestamp in second
|
// Convert to timestamp in second
|
||||||
|
@ -283,6 +388,33 @@ class UptimeCalculator {
|
||||||
up: 0,
|
up: 0,
|
||||||
down: 0,
|
down: 0,
|
||||||
avgPing: 0,
|
avgPing: 0,
|
||||||
|
minPing: 0,
|
||||||
|
maxPing: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return divisionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert timestamp to hourly key
|
||||||
|
* @param {dayjs.Dayjs} date The heartbeat date
|
||||||
|
* @returns {number} Timestamp
|
||||||
|
*/
|
||||||
|
getHourlyKey(date) {
|
||||||
|
// Truncate value to hours (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:00:00)
|
||||||
|
date = date.startOf("hour");
|
||||||
|
|
||||||
|
// Convert to timestamp in second
|
||||||
|
let divisionKey = date.unix();
|
||||||
|
|
||||||
|
if (! (divisionKey in this.hourlyUptimeDataList)) {
|
||||||
|
this.hourlyUptimeDataList.push(divisionKey, {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
avgPing: 0,
|
||||||
|
minPing: 0,
|
||||||
|
maxPing: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,13 +423,11 @@ class UptimeCalculator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert timestamp to daily key
|
* Convert timestamp to daily key
|
||||||
* @param {number} timestamp Timestamp
|
* @param {dayjs.Dayjs} date The heartbeat date
|
||||||
* @returns {number} Timestamp
|
* @returns {number} Timestamp
|
||||||
*/
|
*/
|
||||||
getDailyKey(timestamp) {
|
getDailyKey(date) {
|
||||||
let date = dayjs.unix(timestamp);
|
// Truncate value to start of day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
|
||||||
|
|
||||||
// Convert the date to the nearest day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
|
|
||||||
// Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
|
// Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
|
||||||
date = date.utc().startOf("day");
|
date = date.utc().startOf("day");
|
||||||
let dailyKey = date.unix();
|
let dailyKey = date.unix();
|
||||||
|
@ -307,12 +437,34 @@ class UptimeCalculator {
|
||||||
up: 0,
|
up: 0,
|
||||||
down: 0,
|
down: 0,
|
||||||
avgPing: 0,
|
avgPing: 0,
|
||||||
|
minPing: 0,
|
||||||
|
maxPing: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return dailyKey;
|
return dailyKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert timestamp to key
|
||||||
|
* @param {dayjs.Dayjs} datetime Datetime
|
||||||
|
* @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
|
||||||
|
* @returns {number} Timestamp
|
||||||
|
* @throws {Error} If the type is invalid
|
||||||
|
*/
|
||||||
|
getKey(datetime, type) {
|
||||||
|
switch (type) {
|
||||||
|
case "day":
|
||||||
|
return this.getDailyKey(datetime);
|
||||||
|
case "hour":
|
||||||
|
return this.getHourlyKey(datetime);
|
||||||
|
case "minute":
|
||||||
|
return this.getMinutelyKey(datetime);
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flat status to UP or DOWN
|
* Flat status to UP or DOWN
|
||||||
* @param {number} status the status which schould be turned into a flat status
|
* @param {number} status the status which schould be turned into a flat status
|
||||||
|
@ -333,21 +485,21 @@ class UptimeCalculator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} num the number of data points which are expected to be returned
|
* @param {number} num the number of data points which are expected to be returned
|
||||||
* @param {"day" | "minute"} type the type of data which is expected to be returned
|
* @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
|
||||||
* @returns {UptimeDataResult} UptimeDataResult
|
* @returns {UptimeDataResult} UptimeDataResult
|
||||||
* @throws {Error} The maximum number of minutes greater than 1440
|
* @throws {Error} The maximum number of minutes greater than 1440
|
||||||
*/
|
*/
|
||||||
getData(num, type = "day") {
|
getData(num, type = "day") {
|
||||||
let key;
|
|
||||||
|
|
||||||
if (type === "day") {
|
if (type === "hour" && num > 24 * 30) {
|
||||||
key = this.getDailyKey(this.getCurrentDate().unix());
|
throw new Error("The maximum number of hours is 720");
|
||||||
} else {
|
|
||||||
if (num > 24 * 60) {
|
|
||||||
throw new Error("The maximum number of minutes is 1440");
|
|
||||||
}
|
|
||||||
key = this.getMinutelyKey(this.getCurrentDate());
|
|
||||||
}
|
}
|
||||||
|
if (type === "minute" && num > 24 * 60) {
|
||||||
|
throw new Error("The maximum number of minutes is 1440");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current time period key based on the type
|
||||||
|
let key = this.getKey(this.getCurrentDate(), type);
|
||||||
|
|
||||||
let total = {
|
let total = {
|
||||||
up: 0,
|
up: 0,
|
||||||
|
@ -357,20 +509,37 @@ class UptimeCalculator {
|
||||||
let totalPing = 0;
|
let totalPing = 0;
|
||||||
let endTimestamp;
|
let endTimestamp;
|
||||||
|
|
||||||
if (type === "day") {
|
// Get the eariest timestamp of the required period based on the type
|
||||||
endTimestamp = key - 86400 * (num - 1);
|
switch (type) {
|
||||||
} else {
|
case "day":
|
||||||
endTimestamp = key - 60 * (num - 1);
|
endTimestamp = key - 86400 * (num - 1);
|
||||||
|
break;
|
||||||
|
case "hour":
|
||||||
|
endTimestamp = key - 3600 * (num - 1);
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
endTimestamp = key - 60 * (num - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum up all data in the specified time range
|
// Sum up all data in the specified time range
|
||||||
while (key >= endTimestamp) {
|
while (key >= endTimestamp) {
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
if (type === "day") {
|
switch (type) {
|
||||||
data = this.dailyUptimeDataList[key];
|
case "day":
|
||||||
} else {
|
data = this.dailyUptimeDataList[key];
|
||||||
data = this.minutelyUptimeDataList[key];
|
break;
|
||||||
|
case "hour":
|
||||||
|
data = this.hourlyUptimeDataList[key];
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
data = this.minutelyUptimeDataList[key];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -379,27 +548,53 @@ class UptimeCalculator {
|
||||||
totalPing += data.avgPing * data.up;
|
totalPing += data.avgPing * data.up;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Previous day
|
// Set key to the pervious time period
|
||||||
if (type === "day") {
|
switch (type) {
|
||||||
key -= 86400;
|
case "day":
|
||||||
} else {
|
key -= 86400;
|
||||||
key -= 60;
|
break;
|
||||||
|
case "hour":
|
||||||
|
key -= 3600;
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
key -= 60;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let uptimeData = new UptimeDataResult();
|
let uptimeData = new UptimeDataResult();
|
||||||
|
|
||||||
|
// If there is no data in the previous time ranges, use the last data?
|
||||||
if (total.up === 0 && total.down === 0) {
|
if (total.up === 0 && total.down === 0) {
|
||||||
if (type === "day" && this.lastDailyUptimeData) {
|
switch (type) {
|
||||||
total = this.lastDailyUptimeData;
|
case "day":
|
||||||
totalPing = total.avgPing * total.up;
|
if (this.lastDailyUptimeData) {
|
||||||
} else if (type === "minute" && this.lastUptimeData) {
|
total = this.lastDailyUptimeData;
|
||||||
total = this.lastUptimeData;
|
totalPing = total.avgPing * total.up;
|
||||||
totalPing = total.avgPing * total.up;
|
} else {
|
||||||
} else {
|
return uptimeData;
|
||||||
uptimeData.uptime = 0;
|
}
|
||||||
uptimeData.avgPing = null;
|
break;
|
||||||
return uptimeData;
|
case "hour":
|
||||||
|
if (this.lastHourlyUptimeData) {
|
||||||
|
total = this.lastHourlyUptimeData;
|
||||||
|
totalPing = total.avgPing * total.up;
|
||||||
|
} else {
|
||||||
|
return uptimeData;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
if (this.lastUptimeData) {
|
||||||
|
total = this.lastUptimeData;
|
||||||
|
totalPing = total.avgPing * total.up;
|
||||||
|
} else {
|
||||||
|
return uptimeData;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,6 +611,85 @@ class UptimeCalculator {
|
||||||
return uptimeData;
|
return uptimeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data in form of an array
|
||||||
|
* @param {number} num the number of data points which are expected to be returned
|
||||||
|
* @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
|
||||||
|
* @returns {Array<object>} uptime data
|
||||||
|
* @throws {Error} The maximum number of minutes greater than 1440
|
||||||
|
*/
|
||||||
|
getDataArray(num, type = "day") {
|
||||||
|
if (type === "hour" && num > 24 * 30) {
|
||||||
|
throw new Error("The maximum number of hours is 720");
|
||||||
|
}
|
||||||
|
if (type === "minute" && num > 24 * 60) {
|
||||||
|
throw new Error("The maximum number of minutes is 1440");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current time period key based on the type
|
||||||
|
let key = this.getKey(this.getCurrentDate(), type);
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
let endTimestamp;
|
||||||
|
|
||||||
|
// Get the eariest timestamp of the required period based on the type
|
||||||
|
switch (type) {
|
||||||
|
case "day":
|
||||||
|
endTimestamp = key - 86400 * (num - 1);
|
||||||
|
break;
|
||||||
|
case "hour":
|
||||||
|
endTimestamp = key - 3600 * (num - 1);
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
endTimestamp = key - 60 * (num - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get datapoints in the specified time range
|
||||||
|
while (key >= endTimestamp) {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "day":
|
||||||
|
data = this.dailyUptimeDataList[key];
|
||||||
|
break;
|
||||||
|
case "hour":
|
||||||
|
data = this.hourlyUptimeDataList[key];
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
data = this.minutelyUptimeDataList[key];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
data.timestamp = key;
|
||||||
|
result.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set key to the pervious time period
|
||||||
|
switch (type) {
|
||||||
|
case "day":
|
||||||
|
key -= 86400;
|
||||||
|
break;
|
||||||
|
case "hour":
|
||||||
|
key -= 3600;
|
||||||
|
break;
|
||||||
|
case "minute":
|
||||||
|
key -= 60;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the uptime data by duration
|
* Get the uptime data by duration
|
||||||
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
|
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
|
||||||
|
@ -446,7 +720,7 @@ class UptimeCalculator {
|
||||||
* @returns {UptimeDataResult} UptimeDataResult
|
* @returns {UptimeDataResult} UptimeDataResult
|
||||||
*/
|
*/
|
||||||
get7Day() {
|
get7Day() {
|
||||||
return this.getData(7);
|
return this.getData(168, "hour");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -464,7 +738,7 @@ class UptimeCalculator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {dayjs.Dayjs} Current date
|
* @returns {dayjs.Dayjs} Current datetime in UTC
|
||||||
*/
|
*/
|
||||||
getCurrentDate() {
|
getCurrentDate() {
|
||||||
return dayjs.utc();
|
return dayjs.utc();
|
||||||
|
@ -476,12 +750,12 @@ class UptimeDataResult {
|
||||||
/**
|
/**
|
||||||
* @type {number} Uptime
|
* @type {number} Uptime
|
||||||
*/
|
*/
|
||||||
uptime;
|
uptime = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number} Average ping
|
* @type {number} Average ping
|
||||||
*/
|
*/
|
||||||
avgPing;
|
avgPing = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -4,7 +4,6 @@ const { R } = require("redbean-node");
|
||||||
const { log, genSecret, badgeConstants } = 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 iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
const chardet = require("chardet");
|
const chardet = require("chardet");
|
||||||
const chroma = require("chroma-js");
|
const chroma = require("chroma-js");
|
||||||
|
@ -129,7 +128,7 @@ exports.ping = async (hostname, size = 56) => {
|
||||||
return await exports.pingAsync(hostname, false, size);
|
return await exports.pingAsync(hostname, false, size);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the host cannot be resolved, try again with ipv6
|
// If the host cannot be resolved, try again with ipv6
|
||||||
console.debug("ping", "IPv6 error message: " + e.message);
|
log.debug("ping", "IPv6 error message: " + e.message);
|
||||||
|
|
||||||
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
|
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
|
||||||
if (!e.message) {
|
if (!e.message) {
|
||||||
|
@ -797,29 +796,6 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Start end-to-end tests
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exports.startE2eTests = async () => {
|
|
||||||
console.log("Starting unit test...");
|
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
|
||||||
const child = childProcess.spawn(npm, [ "run", "cy:run" ]);
|
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
console.log(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
console.log(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", function (code) {
|
|
||||||
console.log("Jest exit code: " + code);
|
|
||||||
process.exit(code);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert unknown string to UTF8
|
* Convert unknown string to UTF8
|
||||||
* @param {Uint8Array} body Buffer
|
* @param {Uint8Array} body Buffer
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<span v-if="isNum" ref="output">{{ output }}</span> <span v-if="isNum">{{ unit }}</span>
|
<span v-if="isNum" ref="output">{{ outputFixed }}</span> <span v-if="isNum">{{ unit }}</span>
|
||||||
<span v-else>{{ value }}</span>
|
<span v-else>{{ value }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -37,6 +37,19 @@ export default {
|
||||||
isNum() {
|
isNum() {
|
||||||
return typeof this.value === "number";
|
return typeof this.value === "number";
|
||||||
},
|
},
|
||||||
|
outputFixed() {
|
||||||
|
if (typeof this.output === "number") {
|
||||||
|
if (this.output < 1) {
|
||||||
|
return "<1";
|
||||||
|
} else if (Number.isInteger(this.output)) {
|
||||||
|
return this.output;
|
||||||
|
} else {
|
||||||
|
return this.output.toFixed(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.output;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -177,6 +177,12 @@ export default {
|
||||||
this.password.currentPassword = "";
|
this.password.currentPassword = "";
|
||||||
this.password.newPassword = "";
|
this.password.newPassword = "";
|
||||||
this.password.repeatNewPassword = "";
|
this.password.repeatNewPassword = "";
|
||||||
|
|
||||||
|
// Update token of the current session
|
||||||
|
if (res.token) {
|
||||||
|
this.$root.storage().token = res.token;
|
||||||
|
this.$root.socket.token = res.token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
"disableauth.message2": "It is designed for scenarios {intendThirdPartyAuth} in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
|
"disableauth.message2": "It is designed for scenarios {intendThirdPartyAuth} in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
|
||||||
"where you intend to implement third-party authentication": "where you intend to implement third-party authentication",
|
"where you intend to implement third-party authentication": "where you intend to implement third-party authentication",
|
||||||
"Please use this option carefully!": "Please use this option carefully!",
|
"Please use this option carefully!": "Please use this option carefully!",
|
||||||
"Logout": "Logout",
|
"Logout": "Log out",
|
||||||
"Leave": "Leave",
|
"Leave": "Leave",
|
||||||
"I understand, please disable": "I understand, please disable",
|
"I understand, please disable": "I understand, please disable",
|
||||||
"Confirm": "Confirm",
|
"Confirm": "Confirm",
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
"Username": "Username",
|
"Username": "Username",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Remember me": "Remember me",
|
"Remember me": "Remember me",
|
||||||
"Login": "Login",
|
"Login": "Log in",
|
||||||
"No Monitors, please": "No Monitors, please",
|
"No Monitors, please": "No Monitors, please",
|
||||||
"add one": "add one",
|
"add one": "add one",
|
||||||
"Notification Type": "Notification Type",
|
"Notification Type": "Notification Type",
|
||||||
|
@ -905,5 +905,6 @@
|
||||||
"useRemoteBrowser": "Use a Remote Browser",
|
"useRemoteBrowser": "Use a Remote Browser",
|
||||||
"deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
|
"deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
|
||||||
"GrafanaOncallUrl": "Grafana Oncall URL",
|
"GrafanaOncallUrl": "Grafana Oncall URL",
|
||||||
"Browser Screenshot": "Browser Screenshot"
|
"Browser Screenshot": "Browser Screenshot",
|
||||||
|
"What is a Remote Browser?": "What is a Remote Browser?"
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
<router-link :to=" '/clone/' + monitor.id " class="btn btn-normal">
|
<router-link :to=" '/clone/' + monitor.id " class="btn btn-normal">
|
||||||
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
|
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<button class="btn btn-danger" @click="deleteDialog">
|
<button class="btn btn-normal text-danger" @click="deleteDialog">
|
||||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -78,22 +78,27 @@ test("Test getMinutelyKey", async (t) => {
|
||||||
|
|
||||||
test("Test getDailyKey", async (t) => {
|
test("Test getDailyKey", async (t) => {
|
||||||
let c2 = new UptimeCalculator();
|
let c2 = new UptimeCalculator();
|
||||||
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00").unix());
|
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00"));
|
||||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
c2 = new UptimeCalculator();
|
c2 = new UptimeCalculator();
|
||||||
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30").unix());
|
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30"));
|
||||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
// Edge case 1
|
// Edge case 1
|
||||||
c2 = new UptimeCalculator();
|
c2 = new UptimeCalculator();
|
||||||
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59").unix());
|
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59"));
|
||||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
// Edge case 2
|
// Edge case 2
|
||||||
c2 = new UptimeCalculator();
|
c2 = new UptimeCalculator();
|
||||||
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00").unix());
|
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00"));
|
||||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
|
// Test timezone
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)"));
|
||||||
|
assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Test lastDailyUptimeData", async (t) => {
|
test("Test lastDailyUptimeData", async (t) => {
|
||||||
|
|
|
@ -1,323 +0,0 @@
|
||||||
// ⚠️⚠️⚠️ Deprecated: Jest is not recommended for testing backend code anymore, please create a new test file in ./test/backend-test which are native Node.js test files.
|
|
||||||
|
|
||||||
const { genSecret, DOWN, log} = require("../src/util");
|
|
||||||
const utilServer = require("../server/util-server");
|
|
||||||
const Discord = require("../server/notification-providers/discord");
|
|
||||||
const axios = require("axios");
|
|
||||||
const { UptimeKumaServer } = require("../server/uptime-kuma-server");
|
|
||||||
const Database = require("../server/database");
|
|
||||||
const {Settings} = require("../server/settings");
|
|
||||||
const fs = require("fs");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
dayjs.extend(require("dayjs/plugin/utc"));
|
|
||||||
dayjs.extend(require("dayjs/plugin/timezone"));
|
|
||||||
|
|
||||||
jest.mock("axios");
|
|
||||||
|
|
||||||
describe("Test parseCertificateInfo", () => {
|
|
||||||
it("should handle undefined", async () => {
|
|
||||||
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
|
|
||||||
const info = parseCertificateInfo(undefined);
|
|
||||||
expect(info).toEqual(undefined);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
it("should handle normal cert chain", async () => {
|
|
||||||
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
|
|
||||||
|
|
||||||
const chain1 = {
|
|
||||||
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain2 = {
|
|
||||||
fingerprint: "A0:31:C4:67:82:E6:E6:C6:62:C2:C8:7C:76:DA:9A:A6:2C:CA:BD:8E",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain3 = {
|
|
||||||
fingerprint: "5F:B7:EE:06:33:E2:59:DB:AD:0C:4C:9A:E6:D3:8F:1A:61:C7:DC:25",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
chain1.issuerCertificate = chain2;
|
|
||||||
chain2.issuerCertificate = chain3;
|
|
||||||
chain3.issuerCertificate = chain3;
|
|
||||||
|
|
||||||
const info = parseCertificateInfo(chain1);
|
|
||||||
expect(chain1).toEqual(info);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
it("should handle cert chain with strange circle", async () => {
|
|
||||||
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
|
|
||||||
|
|
||||||
const chain1 = {
|
|
||||||
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain2 = {
|
|
||||||
fingerprint: "A0:31:C4:67:82:E6:E6:C6:62:C2:C8:7C:76:DA:9A:A6:2C:CA:BD:8E",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain3 = {
|
|
||||||
fingerprint: "5F:B7:EE:06:33:E2:59:DB:AD:0C:4C:9A:E6:D3:8F:1A:61:C7:DC:25",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain4 = {
|
|
||||||
fingerprint: "haha",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
chain1.issuerCertificate = chain2;
|
|
||||||
chain2.issuerCertificate = chain3;
|
|
||||||
chain3.issuerCertificate = chain4;
|
|
||||||
chain4.issuerCertificate = chain2;
|
|
||||||
|
|
||||||
const info = parseCertificateInfo(chain1);
|
|
||||||
expect(chain1).toEqual(info);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
it("should handle cert chain with last undefined (should be happen in real, but just in case)", async () => {
|
|
||||||
const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
|
|
||||||
|
|
||||||
const chain1 = {
|
|
||||||
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain2 = {
|
|
||||||
fingerprint: "A0:31:C4:67:82:E6:E6:C6:62:C2:C8:7C:76:DA:9A:A6:2C:CA:BD:8E",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain3 = {
|
|
||||||
fingerprint: "5F:B7:EE:06:33:E2:59:DB:AD:0C:4C:9A:E6:D3:8F:1A:61:C7:DC:25",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chain4 = {
|
|
||||||
fingerprint: "haha",
|
|
||||||
valid_from: "Oct 22 12:00:00 2013 GMT",
|
|
||||||
valid_to: "Oct 22 12:00:00 2028 GMT",
|
|
||||||
subjectaltname: "DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net",
|
|
||||||
};
|
|
||||||
|
|
||||||
chain1.issuerCertificate = chain2;
|
|
||||||
chain2.issuerCertificate = chain3;
|
|
||||||
chain3.issuerCertificate = chain4;
|
|
||||||
chain4.issuerCertificate = undefined;
|
|
||||||
|
|
||||||
const info = parseCertificateInfo(chain1);
|
|
||||||
expect(chain1).toEqual(info);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test genSecret", () => {
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be correct length", () => {
|
|
||||||
let secret = genSecret(-1);
|
|
||||||
expect(secret).toEqual("");
|
|
||||||
|
|
||||||
secret = genSecret(0);
|
|
||||||
expect(secret).toEqual("");
|
|
||||||
|
|
||||||
secret = genSecret(1);
|
|
||||||
expect(secret.length).toEqual(1);
|
|
||||||
|
|
||||||
secret = genSecret(2);
|
|
||||||
expect(secret.length).toEqual(2);
|
|
||||||
|
|
||||||
secret = genSecret(64);
|
|
||||||
expect(secret.length).toEqual(64);
|
|
||||||
|
|
||||||
secret = genSecret(9000);
|
|
||||||
expect(secret.length).toEqual(9000);
|
|
||||||
|
|
||||||
secret = genSecret(90000);
|
|
||||||
expect(secret.length).toEqual(90000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should contain first and last possible chars", () => {
|
|
||||||
let secret = genSecret(90000);
|
|
||||||
expect(secret).toContain("A");
|
|
||||||
expect(secret).toContain("9");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test reset-password", () => {
|
|
||||||
it("should able to run", async () => {
|
|
||||||
await require("../extra/reset-password").main();
|
|
||||||
}, 120000);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test Discord Notification Provider", () => {
|
|
||||||
const hostname = "discord.com";
|
|
||||||
const port = 1337;
|
|
||||||
|
|
||||||
const sendNotification = async (hostname, port, type) => {
|
|
||||||
const discordProvider = new Discord();
|
|
||||||
|
|
||||||
axios.post.mockResolvedValue({});
|
|
||||||
|
|
||||||
await discordProvider.send(
|
|
||||||
{
|
|
||||||
discordUsername: "Uptime Kuma",
|
|
||||||
discordWebhookUrl: "https://discord.com",
|
|
||||||
},
|
|
||||||
"test message",
|
|
||||||
{
|
|
||||||
type,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: DOWN,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should send hostname for ping monitors", async () => {
|
|
||||||
await sendNotification(hostname, null, "ping");
|
|
||||||
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(hostname);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([ "dns", "port", "steam" ])("should send hostname for %p monitors", async (type) => {
|
|
||||||
await sendNotification(hostname, port, type);
|
|
||||||
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(`${hostname}:${port}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("The function filterAndJoin", () => {
|
|
||||||
it("should join and array of strings to one string", () => {
|
|
||||||
const result = utilServer.filterAndJoin([ "one", "two", "three" ]);
|
|
||||||
expect(result).toBe("onetwothree");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should join strings using a given connector", () => {
|
|
||||||
const result = utilServer.filterAndJoin([ "one", "two", "three" ], "-");
|
|
||||||
expect(result).toBe("one-two-three");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should filter null, undefined and empty strings before joining", () => {
|
|
||||||
const result = utilServer.filterAndJoin([ undefined, "", "three" ], "--");
|
|
||||||
expect(result).toBe("three");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return an empty string if all parts are filtered out", () => {
|
|
||||||
const result = utilServer.filterAndJoin([ undefined, "", "" ], "--");
|
|
||||||
expect(result).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test uptimeKumaServer.getClientIP()", () => {
|
|
||||||
it("should able to get a correct client IP", async () => {
|
|
||||||
Database.initDataDir({
|
|
||||||
"data-dir": "./data/test"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (! fs.existsSync(Database.sqlitePath)) {
|
|
||||||
log.info("server", "Copying Database");
|
|
||||||
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Database.connect(true);
|
|
||||||
await Database.patch();
|
|
||||||
|
|
||||||
const fakeSocket = {
|
|
||||||
client: {
|
|
||||||
conn: {
|
|
||||||
remoteAddress: "192.168.10.10",
|
|
||||||
request: {
|
|
||||||
headers: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const server = Object.create(UptimeKumaServer.prototype);
|
|
||||||
let ip = await server.getClientIP(fakeSocket);
|
|
||||||
|
|
||||||
await Settings.set("trustProxy", false);
|
|
||||||
expect(await Settings.get("trustProxy")).toBe(false);
|
|
||||||
expect(ip).toBe("192.168.10.10");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "10.10.10.10";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("192.168.10.10");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-real-ip"] = "20.20.20.20";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("192.168.10.10");
|
|
||||||
|
|
||||||
await Settings.set("trustProxy", true);
|
|
||||||
expect(await Settings.get("trustProxy")).toBe(true);
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "10.10.10.10";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("10.10.10.10");
|
|
||||||
|
|
||||||
// x-real-ip
|
|
||||||
delete fakeSocket.client.conn.request.headers["x-forwarded-for"];
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("20.20.20.20");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "2001:db8:85a3:8d3:1319:8a2e:370:7348";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("2001:db8:85a3:8d3:1319:8a2e:370:7348");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("203.0.113.195");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("203.0.113.195");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("203.0.113.195");
|
|
||||||
|
|
||||||
// Elements are comma-separated, with optional whitespace surrounding the commas.
|
|
||||||
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195 , 2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178";
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("203.0.113.195");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.remoteAddress = "2001:db8::1";
|
|
||||||
fakeSocket.client.conn.request.headers = {};
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("2001:db8::1");
|
|
||||||
|
|
||||||
fakeSocket.client.conn.remoteAddress = "::ffff:127.0.0.1";
|
|
||||||
fakeSocket.client.conn.request.headers = {};
|
|
||||||
ip = await server.getClientIP(fakeSocket);
|
|
||||||
expect(ip).toBe("127.0.0.1");
|
|
||||||
|
|
||||||
await Database.close();
|
|
||||||
}, 120000);
|
|
||||||
});
|
|
|
@ -1,18 +0,0 @@
|
||||||
const actor = require("../support/actors/actor");
|
|
||||||
const userData = require("../support/const/user-data");
|
|
||||||
const dashboardPage = require("../support/pages/dashboard-page");
|
|
||||||
const setupPage = require("../support/pages/setup-page");
|
|
||||||
|
|
||||||
describe("user can create a new account on setup page", () => {
|
|
||||||
before(() => {
|
|
||||||
cy.visit("/setup");
|
|
||||||
});
|
|
||||||
it("user can create new account", () => {
|
|
||||||
cy.url().should("be.equal", setupPage.SetupPage.url);
|
|
||||||
actor.actor.setupTask.fillAndSubmitSetupForm(userData.DEFAULT_USER_DATA.username, userData.DEFAULT_USER_DATA.password, userData.DEFAULT_USER_DATA.password);
|
|
||||||
cy.url().should("be.equal", dashboardPage.DashboardPage.url);
|
|
||||||
cy.get('[role="alert"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.and("contain.text", "Added Successfully.");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,8 +0,0 @@
|
||||||
const setupTask = require("../tasks/setup-task");
|
|
||||||
class Actor {
|
|
||||||
constructor() {
|
|
||||||
this.setupTask = new setupTask.SetupTask();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const actor = new Actor();
|
|
||||||
exports.actor = actor;
|
|
|
@ -1,4 +0,0 @@
|
||||||
exports.DEFAULT_USER_DATA = {
|
|
||||||
username: "testuser",
|
|
||||||
password: "testuser123",
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
require("./commands");
|
|
|
@ -1,3 +0,0 @@
|
||||||
exports.DashboardPage = {
|
|
||||||
url: Cypress.env("baseUrl") + "/dashboard",
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
exports.SetupPage = {
|
|
||||||
url: Cypress.env("baseUrl") + "/setup",
|
|
||||||
usernameInput: '[data-cy="username-input"]',
|
|
||||||
passWordInput: '[data-cy="password-input"]',
|
|
||||||
passwordRepeatInput: '[data-cy="password-repeat-input"]',
|
|
||||||
submitSetupForm: '[data-cy="submit-setup-form"]',
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
const setupPage = require("../pages/setup-page");
|
|
||||||
|
|
||||||
class SetupTask {
|
|
||||||
fillAndSubmitSetupForm(username, password, passwordRepeat) {
|
|
||||||
cy.get(setupPage.SetupPage.usernameInput).type(username);
|
|
||||||
cy.get(setupPage.SetupPage.passWordInput).type(password);
|
|
||||||
cy.get(setupPage.SetupPage.passwordRepeatInput).type(passwordRepeat);
|
|
||||||
cy.get(setupPage.SetupPage.submitSetupForm).click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.SetupTask = SetupTask;
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { currentLocale } from "../../../src/i18n";
|
|
||||||
|
|
||||||
describe("Test i18n.js", () => {
|
|
||||||
|
|
||||||
it("currentLocale()", () => {
|
|
||||||
const setLanguage = (language) => {
|
|
||||||
Object.defineProperty(window.navigator, 'language', {
|
|
||||||
value: language,
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setLanguage('en-EN');
|
|
||||||
|
|
||||||
expect(currentLocale()).equal("en");
|
|
||||||
|
|
||||||
setLanguage('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.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
|
|
||||||
setLanguage('zh-hk');
|
|
||||||
expect(currentLocale()).equal("en");
|
|
||||||
|
|
||||||
setLanguage('en-US');
|
|
||||||
expect(currentLocale()).equal("en");
|
|
||||||
|
|
||||||
setLanguage('ja-ZZ');
|
|
||||||
expect(currentLocale()).equal("ja");
|
|
||||||
|
|
||||||
setLanguage('zz-ZZ');
|
|
||||||
expect(currentLocale()).equal("en");
|
|
||||||
|
|
||||||
setLanguage('zz-ZZ');
|
|
||||||
expect(currentLocale()).equal("en");
|
|
||||||
|
|
||||||
setLanguage('en');
|
|
||||||
localStorage.locale = "en";
|
|
||||||
expect(currentLocale()).equal("en");
|
|
||||||
|
|
||||||
localStorage.locale = "zh-HK";
|
|
||||||
expect(currentLocale()).equal("zh-HK");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { hostNameRegexPattern } from "../../../src/util-frontend";
|
|
||||||
|
|
||||||
describe("Test util-frontend.js", () => {
|
|
||||||
|
|
||||||
describe("hostNameRegexPattern()", () => {
|
|
||||||
it('should return a valid regex for non mqtt hostnames', () => {
|
|
||||||
const regex = new RegExp(hostNameRegexPattern(false));
|
|
||||||
|
|
||||||
expect(regex.test("www.test.com")).to.be.true;
|
|
||||||
expect(regex.test("127.0.0.1")).to.be.true;
|
|
||||||
expect(regex.test("192.168.1.156")).to.be.true;
|
|
||||||
expect(regex.test(" 192.168.1.145")).to.be.false;
|
|
||||||
expect(regex.test("192.168.1.145 ")).to.be.false;
|
|
||||||
expect(regex.test(" fe80::3282:3ff:ae28:592")).to.be.false;
|
|
||||||
expect(regex.test("fe80::3282:3ff:ae28:592 ")).to.be.false;
|
|
||||||
|
|
||||||
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
|
|
||||||
expect(regex.test(`${schema}://www.test.com`)).to.be.false;
|
|
||||||
expect(regex.test(`${schema}://127.0.0.1`)).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should return a valid regex for mqtt hostnames', () => {
|
|
||||||
const hostnameString = hostNameRegexPattern(false);
|
|
||||||
console.log('*********', hostnameString, '***********');
|
|
||||||
const regex = new RegExp(hostNameRegexPattern(true));
|
|
||||||
|
|
||||||
expect(regex.test("www.test.com")).to.be.true;
|
|
||||||
expect(regex.test("127.0.0.1")).to.be.true;
|
|
||||||
expect(regex.test("192.168.1.156")).to.be.true;
|
|
||||||
expect(regex.test(" 192.168.1.145")).to.be.false;
|
|
||||||
expect(regex.test("192.168.1.145 ")).to.be.false;
|
|
||||||
expect(regex.test(" fe80::3282:3ff:ae28:592")).to.be.false;
|
|
||||||
expect(regex.test("fe80::3282:3ff:ae28:592 ")).to.be.false;
|
|
||||||
|
|
||||||
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
|
|
||||||
expect(regex.test(`${schema}://www.test.com`)).to.be.true;
|
|
||||||
expect(regex.test(`${schema}://127.0.0.1`)).to.be.true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
43
test/e2e/setup.spec.js
Normal file
43
test/e2e/setup.spec.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { test } from "@playwright/test";
|
||||||
|
import { login, screenshot } from "./util-test";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
test("setup sqlite", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./");
|
||||||
|
await page.getByText("SQLite").click();
|
||||||
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setup admin", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./");
|
||||||
|
await page.getByPlaceholder("Username").click();
|
||||||
|
await page.getByPlaceholder("Username").fill("admin");
|
||||||
|
await page.getByPlaceholder("Username").press("Tab");
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).fill("admin123");
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).press("Tab");
|
||||||
|
await page.getByPlaceholder("Repeat Password").fill("admin123");
|
||||||
|
await page.getByRole("button", { name: "Create" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All other tests should be run after setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
test("login", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./dashboard");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logout", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./dashboard");
|
||||||
|
await login(page);
|
||||||
|
await page.getByText("A", { exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "Log out" }).click();
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
27
test/e2e/util-test.js
Normal file
27
test/e2e/util-test.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* @param {TestInfo} testInfo Test info
|
||||||
|
* @param {Page} page Page
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function screenshot(testInfo, page) {
|
||||||
|
const screenshot = await page.screenshot();
|
||||||
|
await testInfo.attach("screenshot", {
|
||||||
|
body: screenshot,
|
||||||
|
contentType: "image/png"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Page} page Page
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function login(page) {
|
||||||
|
// Login
|
||||||
|
await page.getByPlaceholder("Username").click();
|
||||||
|
await page.getByPlaceholder("Username").fill("admin");
|
||||||
|
await page.getByPlaceholder("Username").press("Tab");
|
||||||
|
await page.getByPlaceholder("Password").fill("admin123");
|
||||||
|
await page.getByLabel("Remember me").check();
|
||||||
|
await page.getByRole("button", { name: "Log in" }).click();
|
||||||
|
await page.isVisible("text=Add New Monitor");
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue