Compare commits

..

82 Commits

Author SHA1 Message Date
Louis Lam
8abbc9fd15 Update to 1.17.0-beta.0 2022-06-14 11:00:55 +08:00
Louis Lam
af7c905b44 Merge pull request #1759 from MrEddX/bulgarian
Bulgarian
2022-06-14 10:57:14 +08:00
Louis Lam
0e8f6d2f85 Merge pull request #1639 from christopherpickering/ntml-auth
Add NTML Auth Option for HTTP
2022-06-14 10:56:55 +08:00
Louis Lam
d16be6fb7d Fix and use null as authMethod None instead of empty string 2022-06-14 10:49:30 +08:00
Louis Lam
f25ca96308 Update package-lock.json 2022-06-14 10:37:15 +08:00
Louis Lam
6682839ec8 Merge remote-tracking branch 'origin/master' into ntml-auth
# Conflicts:
#	package-lock.json
#	package.json
#	server/database.js
#	server/model/monitor.js
#	server/server.js
#	server/util-server.js
2022-06-14 10:36:29 +08:00
Louis Lam
817f6db4fd Remove SQL Server code (which in another pr already) 2022-06-14 10:32:38 +08:00
Louis Lam
dc2302244f Merge pull request #1754 from SIMULATAN/patch-1
Remove template comment in SECURITY.md
2022-06-14 10:22:08 +08:00
MrEddX
7a27d3752a Merge branch 'louislam:master' into bulgarian 2022-06-13 22:36:00 +03:00
MrEddX
f0e8f34aeb Update bg-BG.js
Added new fields
2022-06-13 22:32:55 +03:00
Louis Lam
69273a6c41 Merge remote-tracking branch 'origin/master' 2022-06-13 21:16:03 +08:00
Louis Lam
6424fe77ab Change successful log from info to debug in order to avoid large log and less disk usage 2022-06-13 21:15:47 +08:00
Louis Lam
fa60672cce Merge pull request #1728 from tamasmagyar/test/simplify-discord-unit-tests
test: simplified discord unit tests
2022-06-13 21:02:12 +08:00
Louis Lam
6e43ef1dd3 Merge remote-tracking branch 'origin/master' into feat/cert-exp-settings
# Conflicts:
#	server/model/monitor.js
#	src/languages/en.js
2022-06-13 20:56:14 +08:00
Louis Lam
f4f2b8ddb8 Try to fix #1658 2022-06-13 19:24:05 +08:00
Louis Lam
436bc13aeb Merge pull request #1598 from gregdev/feature/axios-cached-dns-resolve
add axios cached dns resolve to monitor
2022-06-13 18:58:16 +08:00
Louis Lam
b72a279361 Update package-lock.json 2022-06-13 18:54:02 +08:00
Louis Lam
a28ef56553 Merge remote-tracking branch 'gregdev/feature/axios-cached-dns-resolve' into feature/axios-cached-dns-resolve
# Conflicts:
#	package-lock.json
#	package.json
2022-06-13 18:53:19 +08:00
Louis Lam
7f432bd916 Remove axios-cached-dns-resolve-test 2022-06-13 18:52:08 +08:00
Louis Lam
f570d41142 Merge remote-tracking branch 'origin/master' into feature/axios-cached-dns-resolve
# Conflicts:
#	package-lock.json
#	package.json
2022-06-13 18:50:43 +08:00
Louis Lam
d4485fe62f Make the monitor type list a bit clear 2022-06-13 18:14:47 +08:00
Louis Lam
e1681ce370 Merge pull request #1636 from christopherpickering/master
Add SQLServer Monitor
2022-06-13 18:03:56 +08:00
Jakob
69d6633e6d Remove template comment 2022-06-13 11:34:36 +02:00
Louis Lam
04e22f17a9 Merge remote-tracking branch 'origin/master' into christopherpickering_master
# Conflicts:
#	package-lock.json
#	src/languages/en.js
2022-06-11 20:59:58 +08:00
Louis Lam
11243a6ca1 Merge pull request #1222 from NETivism/issue-1201
Show some pure text body in notification when keyword not found
2022-06-09 19:33:10 +08:00
Louis Lam
87428231ad Merge pull request #1727 from chakflying/patch-2
Fix: Fix error when status page desc. is null
2022-06-07 16:45:28 +08:00
tamasmagyar
a68d945cdc simplified backend unit tests 2022-06-07 09:10:50 +02:00
Nelson Chan
2c0180f323 Fix: Fix error when status page desc. is null 2022-06-07 14:57:23 +08:00
Louis Lam
4fdaa1abb6 [Push API] Response 404 if error, fix #1721 2022-06-06 22:40:26 +08:00
Louis Lam
6ee7b3696a Merge pull request #1633 from domingospanta/bugfix/1451_blank_page_on_unkown_resource
Bugfix/1451 blank page on unkown resource
2022-06-06 21:54:49 +08:00
Louis Lam
cc258dce14 Merge pull request #1674 from philippdormann/feature/ntfy-support
feat: ntfy push support
2022-06-06 21:52:41 +08:00
Louis Lam
fb420fa1b1 Compress SVG when building dist 2022-06-05 23:49:48 +08:00
Louis Lam
a707b51053 Page Loading Speed Optimization (#1711)
* Update Vite.js to 2.9.9 and add Rollup Plugin Visualizer
* Prebuild gzip and brotli for assets

Original: ~1.2MB
Optimized: ~370KB
2022-06-05 23:43:25 +08:00
Louis Lam
c6c1bb5b5c Merge pull request #1710 from AnnAngela/master
Update zh-CN translation
2022-06-01 16:05:23 +08:00
Louis Lam
3210264e28 Update PULL_REQUEST_TEMPLATE.md 2022-06-01 14:20:49 +08:00
Louis Lam
54e948c2ca Update CONTRIBUTING.md 2022-06-01 14:08:10 +08:00
Louis Lam
80094ec4e1 Merge pull request #1513 from louislam/status-page-inject-html
[Status Page] Render title, meta tag or favicon etc. in server side
2022-06-01 13:25:41 +08:00
Louis Lam
091158cfe7 [Status Page] Preload data 2022-06-01 13:05:12 +08:00
AnnAngela-work
abb6ce2366 Update zhCN translation 2022-06-01 11:28:10 +08:00
Louis Lam
e4ad8cbfc8 Remove unused variables 2022-05-31 23:06:43 +08:00
Louis Lam
a674caa520 [Status Page] Add og meta tags 2022-05-31 22:53:48 +08:00
Nelson Chan
179e3569b5 Chore: Add code comments 2022-05-31 16:24:39 +08:00
Nelson Chan
43527f2f40 Chore: Update remaining languages 2022-05-30 18:05:28 +08:00
Nelson Chan
26ff6f45a0 Feat: Use i18n pluralization 2022-05-30 17:53:32 +08:00
Louis Lam
c095767f4a [Status Page] SSR 2022-05-30 15:45:44 +08:00
Louis Lam
ffb7ba176c Merge remote-tracking branch 'origin/master' into status-page-inject-html 2022-05-30 14:00:39 +08:00
Nelson Chan
cfa5b551a5 Feat: Make the expiry days sorted 2022-05-26 12:17:24 +08:00
Nelson Chan
46ee149b70 Chore: Slightly improve design 2022-05-26 12:12:29 +08:00
Philipp Dormann
54184350a4 fix: missing semicolons 2022-05-23 21:13:57 +02:00
Philipp Dormann
14dbe7c334 clean up + default ntfs.sh server url 2022-05-23 21:11:01 +02:00
Philipp Dormann
122e6a842b clean up ntfy topic input 2022-05-23 21:08:56 +02:00
Philipp Dormann
77ef22bdb4 set proper ntfy priorities 2022-05-23 21:08:11 +02:00
Philipp Dormann
59f983d506 fix: unused import "HiddenInput" 2022-05-23 20:57:10 +02:00
Philipp Dormann
71f031c14e add ntfy support
ref https://github.com/louislam/uptime-kuma/issues/1622
2022-05-23 10:55:03 +02:00
Domingos F. Panta Jr
73b965c867 Merge branch 'master' into bugfix/1451_blank_page_on_unkown_resource 2022-05-19 19:35:58 +01:00
Nelson Chan
b7ba6330db Feat: Add cert exp. settings 2022-05-19 16:49:34 +08:00
Christopher Pickering
ef73af391f added option for ntlm authorization 2022-05-13 12:58:23 -05:00
Christopher Pickering
44f6fca945 added finally to close connection pool 2022-05-13 09:34:31 -05:00
Christopher Pickering
23ce7c6623 started db update script 2022-05-13 09:06:41 -05:00
Christopher Pickering
c346ea7864 updated name on export 2022-05-13 08:57:06 -05:00
Christopher Pickering
f0ad32a252 merged 2022-05-13 08:41:31 -05:00
Christopher Pickering
5720017fb4 updated name on import 2022-05-13 08:40:46 -05:00
Christopher Pickering
b7dc8e3ef8 started ui update 2022-05-13 07:22:52 -05:00
sur.la.route
5bba19f866 updated format
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2022-05-12 19:54:12 -05:00
sur.la.route
e198f2f1ab updated format
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2022-05-12 19:54:02 -05:00
Christopher Pickering
f91e5b98f9 [empty commit] pull request for Add NTML Auth Option for HTTP 2022-05-12 13:23:41 -05:00
Christopher Pickering
87f933df4f added sqlserver monitor 2022-05-12 12:48:03 -05:00
Christopher Pickering
332b9ab248 [empty commit] pull request for Add SQLServer Monitor 2022-05-12 10:39:12 -05:00
Domingos Panta
7cc89979f0 Moving change from axios interceptor to specific request. 2022-05-12 15:52:21 +01:00
Domingos Panta
668e97c5a9 fix: lint errors. 2022-05-12 13:39:10 +01:00
Domingos Panta
fdd781b081 Added a link to the home page on the lists of actions.
s new link was necessary to prevent a loop when the user tries to access a unknown resource, like status/123, and was redirected to /page-not-found. In this case going back to the last page would be /status/123 which does not exists.
2022-05-11 17:58:52 +01:00
Domingos Panta
373bd9b962 Added response interceptor to axios response.
This interceptor checks for response code 404 and redicts the user if that is the case.
2022-05-11 17:56:31 +01:00
Louis Lam
59be9bb971 working 2022-05-11 00:51:11 +08:00
Louis Lam
201a25c659 Draft 2022-05-09 00:26:49 +08:00
Greg Smith
cbfecab850 switch to the more-up-to-date esm-wallaby
https://github.com/wallabyjs/esm
2022-05-04 15:45:18 +09:30
Louis Lam
25cc54bf72 Try to give more time for axios-cached-dns-resolve test 2022-05-04 13:24:18 +08:00
Louis Lam
3700b16c5b Copy and add axios-cached-dns-resolve test 2022-05-04 13:16:22 +08:00
Greg Smith
d0546afe71 fix esm require: no ugly warnings 2022-05-01 10:22:16 +09:30
Greg Smith
f4515ad8c5 add axios cached dns resolve to monitor 2022-04-30 21:40:47 +09:30
Jimmy Huang
a4be651118 Update server/model/monitor.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2022-04-01 15:26:50 +08:00
Jimmy Huang
244a7b3671 Update server/model/monitor.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2022-02-07 18:46:16 +08:00
Jimmy Huang
ee90d2713f refs issue-1201 in upstream.
Add 100 characters from response body to bean.msg after keyword not match.
2022-01-25 17:39:19 +08:00
60 changed files with 4688 additions and 770 deletions

View File

@@ -1,3 +1,5 @@
👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
# Description # Description
Fixes #(issue) Fixes #(issue)

View File

@@ -27,17 +27,30 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma? ## Can I create a pull request for Uptime Kuma?
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first. Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
✅ Accept: ✅ Accept:
- Bug/Security fix - Bug/Security fix
- Translations - Translations
- Adding notification providers - Adding notification providers
⚠️ Discuss First ⚠️ Discussion First
- Large pull requests - Large pull requests
- New features - New features
❌ Won't Merge
- Do not pass auto test
- Any breaking changes
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted for no reason
- A function that is completely out of scope
### Recommended Pull Request Guideline ### Recommended Pull Request Guideline
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended. Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
@@ -53,22 +66,15 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
1. Click "Change to draft" 1. Click "Change to draft"
1. Discussion 1. Discussion
#### ❌ Won't Merge
- Any breaking changes
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted
- A function that is completely out of scope
## Project Styles ## Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app. I personally do not like something need to learn so much and need to config so much before you can finally start the app.
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run - Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go - Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
- Settings should be configurable in the frontend. Env var is not encouraged. - Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
- Easy to use - Easy to use
- The web UI styling should be consistent and nice.
## Coding Styles ## Coding Styles

View File

@@ -8,9 +8,6 @@ Do not use the issue tracker or discuss it in the public as it will cause more d
## Supported Versions ## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
### Uptime Kuma Versions ### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version. You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.

View File

@@ -1,10 +1,14 @@
import legacy from "@vitejs/plugin-legacy"; import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
const postCssScss = require("postcss-scss"); const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss"); const postcssRTLCSS = require("postcss-rtlcss");
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@@ -12,7 +16,18 @@ export default defineConfig({
legacy({ legacy({
targets: [ "ie > 11" ], targets: [ "ie > 11" ],
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ] additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
}) }),
visualizer({
filename: "tmp/dist-stats.html"
}),
viteCompression({
algorithm: "gzip",
filter: viteCompressionFilter,
}),
viteCompression({
algorithm: "brotliCompress",
filter: viteCompressionFilter,
}),
], ],
css: { css: {
postcss: { postcss: {
@@ -21,4 +36,13 @@ export default defineConfig({
"plugins": [ postcssRTLCSS ] "plugins": [ postcssRTLCSS ]
} }
}, },
build: {
rollupOptions: {
output: {
manualChunks(id, { getModuleInfo, getModuleIds }) {
}
}
},
}
}); });

View File

@@ -0,0 +1,18 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD auth_method VARCHAR(250);
ALTER TABLE monitor
ADD auth_domain TEXT;
ALTER TABLE monitor
ADD auth_workstation TEXT;
COMMIT;
BEGIN TRANSACTION;
UPDATE monitor
SET auth_method = 'basic'
WHERE basic_auth_user is not null;
COMMIT;

View File

@@ -0,0 +1,10 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD database_connection_string VARCHAR(2000);
ALTER TABLE monitor
ADD database_query TEXT;
COMMIT

4256
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.16.1", "version": "1.17.0-beta.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -57,7 +57,8 @@
"ncu-patch": "npm-check-updates -u -t patch", "ncu-patch": "npm-check-updates -u -t patch",
"release-final": "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/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts", "release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"git-remove-tag": "git tag -d" "git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36", "@fortawesome/fontawesome-svg-core": "~1.2.36",
@@ -68,6 +69,8 @@
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.26.1", "axios": "~0.26.1",
"axios-cached-dns-resolve": "^3.0.6",
"axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1", "badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
@@ -76,12 +79,16 @@
"chart.js": "~3.6.2", "chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2", "chroma-js": "^2.1.2",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "^1.7.4",
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
"esm-wallaby": "^3.2.26",
"express": "~4.17.3", "express": "~4.17.3",
"express-basic-auth": "~1.2.1", "express-basic-auth": "~1.2.1",
"express-static-gzip": "^2.1.7",
"favico.js": "^0.3.10", "favico.js": "^0.3.10",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7", "http-graceful-shutdown": "~3.1.7",
@@ -92,6 +99,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"limiter": "^2.1.0", "limiter": "^2.1.0",
"mqtt": "^4.2.8", "mqtt": "^4.2.8",
"mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
@@ -129,27 +137,31 @@
"@babel/eslint-parser": "~7.17.0", "@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4", "@vitejs/plugin-legacy": "~1.8.2",
"@vitejs/plugin-vue": "~1.9.4", "@vitejs/plugin-vue": "~2.3.3",
"@vue/compiler-sfc": "~3.2.31", "@vue/compiler-sfc": "~3.2.36",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"concurrently": "^7.1.0", "concurrently": "^7.1.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1", "dns2": "~2.0.1",
"eslint": "~8.14.0", "eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1", "eslint-plugin-vue": "~8.7.1",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.3", "jest-puppeteer": "~6.0.3",
"lru-cache": "^7.7.1",
"npm-check-updates": "^12.5.9", "npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1", "postcss-html": "^1.3.1",
"puppeteer": "~13.1.3", "puppeteer": "~13.1.3",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~14.7.1", "stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"vite": "~2.6.14", "vite": "~2.9.9",
"vite-plugin-compression": "^0.5.1",
"wait-on": "^6.0.1" "wait-on": "^6.0.1"
} }
} }

View File

@@ -58,6 +58,8 @@ class Database {
"patch-monitor-expiry-notification.sql": true, "patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true, "patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true, "patch-added-mqtt-monitor.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
}; };
/** /**

View File

@@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
@@ -17,6 +17,12 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const axiosCachedDnsResolve = require("esm-wallaby")(module)("axios-cached-dns-resolve");
// create an axios client instance with the cached DNS resolve interceptor
const axiosClient = axios.create();
axiosCachedDnsResolve.registerInterceptor(axiosClient);
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
@@ -87,7 +93,12 @@ class Monitor extends BeanModel {
mqttUsername: this.mqttUsername, mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword, mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic, mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
}; };
if (includeSensitiveData) { if (includeSensitiveData) {
@@ -213,7 +224,7 @@ class Monitor extends BeanModel {
// HTTP basic auth // HTTP basic auth
let basicAuthHeader = {}; let basicAuthHeader = {};
if (this.basic_auth_user) { if (this.auth_method === "basic") {
basicAuthHeader = { basicAuthHeader = {
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass), "Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
}; };
@@ -264,7 +275,21 @@ class Monitor extends BeanModel {
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`);
let res = await axios.request(options); let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation,
});
} else {
res = await axiosClient.request(options);
}
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@@ -312,7 +337,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found"; bean.msg += ", keyword is found";
bean.status = UP; bean.status = UP;
} else { } else {
throw new Error(bean.msg + ", but keyword is not found"); data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
} }
} }
@@ -405,7 +434,7 @@ class Monitor extends BeanModel {
throw new Error("Steam API Key not found"); throw new Error("Steam API Key not found");
} }
let res = await axios.get(steamApiUrl, { let res = await axiosClient.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
@@ -443,6 +472,14 @@ class Monitor extends BeanModel {
interval: this.interval, interval: this.interval,
}); });
bean.status = UP; bean.status = UP;
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else { } else {
bean.msg = "Unknown Monitor Type"; bean.msg = "Unknown Monitor Type";
bean.status = PENDING; bean.status = PENDING;
@@ -493,7 +530,7 @@ class Monitor extends BeanModel {
} }
if (bean.status === UP) { if (bean.status === UP) {
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`); log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) { } else if (bean.status === PENDING) {
if (this.retryInterval > 0) { if (this.retryInterval > 0) {
beatInterval = this.retryInterval; beatInterval = this.retryInterval;
@@ -837,10 +874,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this); const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays"); let notifyDays = await setting("tlsExpiryNotifyDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList); if (notifyDays == null || !Array.isArray(notifyDays)) {
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList); // Reset Default
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList); setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
if (notifyDays != null && Array.isArray(notifyDays)) {
for (const day of notifyDays) {
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
}
}
} }
} }

View File

@@ -1,10 +1,104 @@
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class StatusPage extends BeanModel { class StatusPage extends BeanModel {
/**
* Like this: { "test-uptime.kuma.pet": "default" }
* @type {{}}
*/
static domainMappingList = { }; static domainMappingList = { };
/**
*
* @param {Response} response
* @param {string} indexHTML
* @param {string} slug
*/
static async handleStatusPageResponse(response, indexHTML, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (statusPage) {
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}
/**
* SSR for status pages
* @param {string} indexHTML
* @param {StatusPage} statusPage
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155);
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
if (statusPage.icon) {
$("link[rel=icon]")
.attr("href", statusPage.icon)
.removeAttr("type");
}
const head = $("head");
// OG Meta Tags
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
head.append(`<meta property="og:description" content="${description155}" />`);
// Preload data
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
head.append(`
<script>
window.preloadData = ${json}
</script>
`);
return $.root().html();
}
/**
* Get all status page data in one call
* @param {StatusPage} statusPage
*/
static async getStatusPageData(statusPage) {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
return {
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
};
}
/** /**
* Loads domain mapping from DB * Loads domain mapping from DB
* Return object like this: { "test-uptime.kuma.pet": "default" } * Return object like this: { "test-uptime.kuma.pet": "default" }

View File

@@ -0,0 +1,26 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Ntfy extends NotificationProvider {
name = "ntfy";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`${notification.ntfyserverurl}`, {
"topic": notification.ntfytopic,
"message": msg,
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Ntfy;

View File

@@ -2,6 +2,7 @@ const { R } = require("redbean-node");
const Apprise = require("./notification-providers/apprise"); const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord"); const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify"); const Gotify = require("./notification-providers/gotify");
const Ntfy = require("./notification-providers/ntfy");
const Line = require("./notification-providers/line"); const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea"); const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost"); const Mattermost = require("./notification-providers/mattermost");
@@ -52,6 +53,7 @@ class Notification {
new Discord(), new Discord(),
new Teams(), new Teams(),
new Gotify(), new Gotify(),
new Ntfy(),
new Line(), new Line(),
new LunaSea(), new LunaSea(),
new Feishu(), new Feishu(),

View File

@@ -1,5 +1,5 @@
let express = require("express"); let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server"); const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = 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");
@@ -92,115 +92,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
} }
} catch (e) { } catch (e) {
response.json({ response.status(404).json({
ok: false, ok: false,
msg: e.message msg: e.message
}); });
} }
}); });
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
try {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
response.json({
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => { router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response); allowAllOrigin(response);
@@ -377,16 +275,4 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
} }
}); });
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
function send403(res, msg = "") {
res.status(403).json({
"status": "fail",
"msg": msg,
});
}
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,110 @@
let express = require("express");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, send403 } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
let router = express.Router();
let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status-page", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
try {
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
return null;
}
let statusPageData = await StatusPage.getStatusPageData(statusPage);
if (!statusPageData) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
// Response
response.json(statusPageData);
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
module.exports = router;

View File

@@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
} }
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util"); const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config"); const config = require("./config");
log.info("server", "Welcome to Uptime Kuma"); log.info("server", "Welcome to Uptime Kuma");
@@ -35,6 +35,7 @@ const fs = require("fs");
log.info("server", "Importing 3rd-party libraries"); log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express"); log.debug("server", "Importing express");
const express = require("express"); const express = require("express");
const expressStaticGzip = require("express-static-gzip");
log.debug("server", "Importing redbean-node"); log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken"); log.debug("server", "Importing jsonwebtoken");
@@ -148,22 +149,6 @@ let jwtSecret = null;
*/ */
let needSetup = false; let needSetup = false;
/**
* Cache Index HTML
* @type {string}
*/
let indexHTML = "";
try {
indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
(async () => { (async () => {
Database.init(args); Database.init(args);
await initDatabase(testMode); await initDatabase(testMode);
@@ -179,13 +164,17 @@ try {
// Entry Page // Entry Page
app.get("/", async (request, response) => { app.get("/", async (request, response) => {
debug(`Request Domain: ${request.hostname}`); log.debug("entry", `Request Domain: ${request.hostname}`);
if (request.hostname in StatusPage.domainMappingList) { if (request.hostname in StatusPage.domainMappingList) {
debug("This is a status page domain"); log.debug("entry", "This is a status page domain");
response.send(indexHTML);
let slug = StatusPage.domainMappingList[request.hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { } else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", "")); response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else { } else {
response.redirect("/dashboard"); response.redirect("/dashboard");
} }
@@ -214,7 +203,9 @@ try {
// With Basic Auth using the first user's username/password // With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics()); app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist")); app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload // ./data/upload
app.use("/upload", express.static(Database.uploadDir)); app.use("/upload", express.static(Database.uploadDir));
@@ -227,12 +218,16 @@ try {
const apiRouter = require("./routers/api-router"); const apiRouter = require("./routers/api-router");
app.use(apiRouter); app.use(apiRouter);
// Status Page Router
const statusPageRouter = require("./routers/status-page-router");
app.use(statusPageRouter);
// Universal Route Handler, must be at the end of all express routes. // Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => { app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) { if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found."); response.status(404).send("File not found.");
} else { } else {
response.send(indexHTML); response.send(server.indexHTML);
} }
}); });
@@ -674,6 +669,11 @@ try {
bean.mqttPassword = monitor.mqttPassword; bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic; bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage; bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
await R.store(bean); await R.store(bean);
@@ -1247,8 +1247,11 @@ try {
method: monitorListData[i].method || "GET", method: monitorListData[i].method || "GET",
body: monitorListData[i].body, body: monitorListData[i].body,
headers: monitorListData[i].headers, headers: monitorListData[i].headers,
authMethod: monitorListData[i].authMethod,
basic_auth_user: monitorListData[i].basic_auth_user, basic_auth_user: monitorListData[i].basic_auth_user,
basic_auth_pass: monitorListData[i].basic_auth_pass, basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
hostname: monitorListData[i].hostname, hostname: monitorListData[i].hostname,

View File

@@ -29,6 +29,12 @@ class UptimeKumaServer {
httpServer = undefined; httpServer = undefined;
io = undefined; io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
static getInstance(args) { static getInstance(args) {
if (UptimeKumaServer.instance == null) { if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args); UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -55,6 +61,16 @@ class UptimeKumaServer {
this.httpServer = http.createServer(this.app); this.httpServer = http.createServer(this.app);
} }
try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }

View File

@@ -10,6 +10,8 @@ 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 { badgeConstants } = require("./config");
const mssql = require("mssql");
const { NtlmClient } = require("axios-ntlm");
// From ping-lite // From ping-lite
exports.WIN = /^win/.test(process.platform); exports.WIN = /^win/.test(process.platform);
@@ -172,6 +174,26 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
}); });
}; };
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
* @param {Object} ntlmOptions The auth options
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.httpNtlm = function (options, ntlmOptions) {
return new Promise((resolve, reject) => {
let client = NtlmClient(ntlmOptions);
client(options)
.then((resp) => {
resolve(resp);
})
.catch((err) => {
reject(err);
});
});
};
/** /**
* Resolves a given record using the specified DNS server * Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup * @param {string} hostname The hostname of the record to lookup
@@ -185,7 +207,7 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
// Remove brackets from IPv6 addresses so we can re-add them to // Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300) // prevent issues with ::1:5300 (::1 port 5300)
resolverServer = resolverServer.replace("[", "").replace("]", ""); resolverServer = resolverServer.replace("[", "").replace("]", "");
resolver.setServers([`[${resolverServer}]:${resolverPort}`]); resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (rrtype === "PTR") { if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => { resolver.reverse(hostname, (err, records) => {
@@ -207,6 +229,31 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
}); });
}; };
/**
* Run a query on SQL Server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
};
/** /**
* Retrieve value of setting based on key * Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve * @param {string} key Key of setting to retrieve
@@ -558,3 +605,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
exports.filterAndJoin = (parts, connector = "") => { exports.filterAndJoin = (parts, connector = "") => {
return parts.filter((part) => !!part && part !== "").join(connector); return parts.filter((part) => !!part && part !== "").join(connector);
}; };
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
module.exports.send403 = (res, msg = "") => {
res.status(403).json({
"status": "fail",
"msg": msg,
});
};

View File

@@ -0,0 +1,86 @@
<template>
<div class="input-group mb-3">
<input
ref="input"
v-model="model"
class="form-control"
:type="type"
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</div>
</template>
<script>
/**
* Generic input field with a customizable action on the right.
* Action is passed in as a function.
*/
export default {
props: {
/**
* The value of the input field.
*/
modelValue: {
type: String,
default: ""
},
/**
* Whether the input field is enabled / disabled.
*/
enabled: {
type: Boolean,
default: true
},
/**
* Placeholder text for the input field.
*/
placeholder: {
type: String,
default: ""
},
/**
* The icon displayed in the right button of the input field.
* Accepts a Font Awesome icon string identifier.
* @example "plus"
*/
icon: {
type: String,
required: true,
},
/**
* The input type of the input field.
* @example "email"
*/
type: {
type: String,
default: "text",
},
/**
* The action to be performed when the button is clicked.
* Action is passed in as a function.
*/
action: {
type: Function,
default: () => {},
}
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="mb-3">
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
<div class="input-group mb-3">
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
<div class="input-group mb-3">
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<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">
</div>
</template>
<script>
export default {
mounted() {
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
this.$parent.notification.ntfyPriority = 5;
}
},
};
</script>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="promosms-login" class="form-label">{{ $("promosmsLogin") }}</label> <label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required> <input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">{{ $("promosmsPassword") }}</label> <label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@@ -4,6 +4,7 @@ import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue"; import Webhook from "./Webhook.vue";
import Signal from "./Signal.vue"; import Signal from "./Signal.vue";
import Gotify from "./Gotify.vue"; import Gotify from "./Gotify.vue";
import Ntfy from "./Ntfy.vue";
import Slack from "./Slack.vue"; import Slack from "./Slack.vue";
import RocketChat from "./RocketChat.vue"; import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue"; import Teams from "./Teams.vue";
@@ -46,6 +47,7 @@ const NotificationFormList = {
"teams": Teams, "teams": Teams,
"signal": Signal, "signal": Signal,
"gotify": Gotify, "gotify": Gotify,
"ntfy": Ntfy,
"slack": Slack, "slack": Slack,
"rocket.chat": RocketChat, "rocket.chat": RocketChat,
"pushover": Pushover, "pushover": Pushover,

View File

@@ -20,16 +20,91 @@
</button> </button>
</div> </div>
<div class="my-4">
<h4>{{ $t("settingsCertificateExpiry") }}</h4>
<p>{{ $t("certificationExpiryDescription") }}</p>
<div class="mt-2 mb-4 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">
<span>{{ day }} {{ $tc("day", day) }}</span>
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
<font-awesome-icon class="" icon="times" />
</button>
</div>
</div>
<div class="col-12 col-xl-6">
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
</div>
<div>
<button class="btn btn-primary" type="button" @click="saveSettings()">
{{ $t("Save") }}
</button>
</div>
</div>
<NotificationDialog ref="notificationDialog" /> <NotificationDialog ref="notificationDialog" />
</div> </div>
</template> </template>
<script> <script>
import NotificationDialog from "../../components/NotificationDialog.vue"; import NotificationDialog from "../../components/NotificationDialog.vue";
import ActionInput from "../ActionInput.vue";
export default { export default {
components: { components: {
NotificationDialog NotificationDialog,
ActionInput,
},
data() {
return {
/**
* Variable to store the input for new certificate expiry day.
*/
expiryNotifInput: null,
};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
methods: {
/**
* Remove a day from expiry notification days.
* @param {number} day The day to remove.
*/
removeExpiryNotifDay(day) {
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
},
/**
* Add a new expiry notification day.
* Will verify:
* - day is not null or empty string.
* - day is a number.
* - day is > 0.
* - The day is not already in the list.
* @param {number} day The day number to add.
*/
addExpiryNotifDay(day) {
if (day != null && day !== "") {
const parsedDay = parseInt(day);
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
this.settings.tlsExpiryNotifyDays.push(parseInt(day));
this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b);
this.expiryNotifInput = null;
}
}
}
},
}, },
}; };
</script> </script>
@@ -37,10 +112,27 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../assets/vars.scss"; @import "../../assets/vars.scss";
.btn-rm-expiry {
padding-left: 11px;
padding-right: 11px;
}
.dark { .dark {
.list-group-item { .list-group-item {
background-color: $dark-bg2; background-color: $dark-bg2;
color: $dark-font-color; color: $dark-font-color;
} }
} }
.cert-exp-days .cert-exp-day-row {
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
.dark & {
border-bottom: 1px solid $dark-border-color;
}
}
.cert-exp-days .cert-exp-day-row:last-child {
border: none;
}
</style> </style>

View File

@@ -55,8 +55,7 @@ export default {
Current: "Текущ", Current: "Текущ",
Uptime: "Достъпност", Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат", "Cert Exp.": "Вал. сертификат",
days: "дни", day: "ден | дни",
day: "ден",
"-day": "-дни", "-day": "-дни",
hour: "час", hour: "час",
"-hour": "-часa", "-hour": "-часa",
@@ -515,4 +514,18 @@ export default {
"Go back to the previous page.": "Да се върнете към предишната страница.", "Go back to the previous page.": "Да се върнете към предишната страница.",
"Coming Soon": "Очаквайте скоро", "Coming Soon": "Очаквайте скоро",
wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .", wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .",
dnsPortDescription: "DNS порт на сървъра. По подразбиране е 53, но може да бъде променен по всяко време.",
error: "грешка",
critical: "критична",
wayToGetPagerDutyKey: "Може да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук може да потърсите \"Events API V2\". Повече информация {0}",
"Integration Key": "Ключ за интегриране",
"Integration URL": "URL адрес за интеграция",
"Auto resolve or acknowledged": "Автоматично разрешаване или потвърждаване",
"do nothing": "не прави нищо",
"auto acknowledged": "автоматично потвърждаване",
"auto resolve": "автоматично потвърждаване",
"Connection String": "Стринг за връзка",
Query: "Заявка",
settingsCertificateExpiry: "Изтичане валидността на TLS сертификата",
certificationExpiryDescription: "HTTPS мониторите задействат известие при изтичане на TLS сертификата в:",
}; };

View File

@@ -56,8 +56,7 @@ export default {
Current: "Aktuální", Current: "Aktuální",
Uptime: "Doba provozu", Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu", "Cert Exp.": "Platnost certifikátu",
days: "dny/í", day: "den | dny/í",
day: "den",
"-day": "-dní", "-day": "-dní",
hour: "hodina", hour: "hodina",
"-hour": "-hodin", "-hour": "-hodin",

View File

@@ -30,8 +30,7 @@ export default {
Current: "Aktuelt", Current: "Aktuelt",
Uptime: "Oppetid", Uptime: "Oppetid",
"Cert Exp.": "Certifikatets udløb", "Cert Exp.": "Certifikatets udløb",
days: "Dage", day: "Dag | Dage",
day: "Dag",
"-day": "-Dage", "-day": "-Dage",
hour: "Timer", hour: "Timer",
"-hour": "-Timer", "-hour": "-Timer",

View File

@@ -30,8 +30,7 @@ export default {
Current: "Aktuell", Current: "Aktuell",
Uptime: "Verfügbarkeit", Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf", "Cert Exp.": "Zertifikatsablauf",
days: "Tage", day: "Tag | Tage",
day: "Tag",
"-day": "-Tage", "-day": "-Tage",
hour: "Stunde", hour: "Stunde",
"-hour": "-Stunden", "-hour": "-Stunden",

View File

@@ -57,8 +57,7 @@ export default {
Current: "Current", Current: "Current",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "days", day: "day | days",
day: "day",
"-day": "-day", "-day": "-day",
hour: "hour", hour: "hour",
"-hour": "-hour", "-hour": "-hour",
@@ -525,4 +524,8 @@ export default {
"Go back to the previous page.": "Go back to the previous page.", "Go back to the previous page.": "Go back to the previous page.",
"Coming Soon": "Coming Soon", "Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .", wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
"Connection String": "Connection String",
"Query": "Query",
settingsCertificateExpiry: "TLS Certificate Expiry",
certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
}; };

View File

@@ -44,8 +44,7 @@ export default {
Current: "Actual", Current: "Actual",
Uptime: "Tiempo activo", Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.", "Cert Exp.": "Caducidad cert.",
days: "días", day: "día | días",
day: "día",
"-day": "-día", "-day": "-día",
hour: "hora", hour: "hora",
"-hour": "-hora", "-hour": "-hora",

View File

@@ -47,8 +47,7 @@ export default {
Current: "Hetkeseisund", Current: "Hetkeseisund",
Uptime: "Eluiga", Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine", "Cert Exp.": "Sert. aegumine",
days: "päeva", day: "päev | päeva",
day: "päev",
"-day": "-päev", "-day": "-päev",
hour: "tund", hour: "tund",
"-hour": "-tund", "-hour": "-tund",

View File

@@ -55,7 +55,6 @@ export default {
Current: "فعلی", Current: "فعلی",
Uptime: "آپتایم", Uptime: "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL", "Cert Exp.": "تاریخ انقضای SSL",
days: "روز",
day: "روز", day: "روز",
"-day": "-روز", "-day": "-روز",
hour: "ساعت", hour: "ساعت",

View File

@@ -55,8 +55,7 @@ export default {
Current: "Actuellement", Current: "Actuellement",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Expiration SSL", "Cert Exp.": "Expiration SSL",
days: "jours", day: "jour | jours",
day: "jour",
"-day": "-jours", "-day": "-jours",
hour: "-heure", hour: "-heure",
"-hour": "-heures", "-hour": "-heures",

View File

@@ -56,8 +56,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Dostupnost", Uptime: "Dostupnost",
"Cert Exp.": "Istek cert.", "Cert Exp.": "Istek cert.",
days: "dana", day: "dan | dana",
day: "dan",
"-day": "-dnevno", "-day": "-dnevno",
hour: "sat", hour: "sat",
"-hour": "-satno", "-hour": "-satno",

View File

@@ -55,7 +55,6 @@ export default {
Current: "Aktuális", Current: "Aktuális",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "SSL lejárat", "Cert Exp.": "SSL lejárat",
days: "nap",
day: "nap", day: "nap",
"-day": " nap", "-day": " nap",
hour: "óra", hour: "óra",

View File

@@ -55,8 +55,7 @@ export default {
Current: "Saat ini", Current: "Saat ini",
Uptime: "Waktu aktif", Uptime: "Waktu aktif",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "hari-hari", day: "hari | hari-hari",
day: "hari",
"-day": "-hari", "-day": "-hari",
hour: "Jam", hour: "Jam",
"-hour": "-Jam", "-hour": "-Jam",

View File

@@ -56,8 +56,7 @@ export default {
Current: "Corrente", Current: "Corrente",
Uptime: "Tempo di attività", Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato", "Cert Exp.": "Scadenza certificato",
days: "giorni", day: "giorno | giorni",
day: "giorno",
"-day": "-giorni", "-day": "-giorni",
hour: "ora", hour: "ora",
"-hour": "-ore", "-hour": "-ore",

View File

@@ -44,8 +44,7 @@ export default {
Current: "現在", Current: "現在",
Uptime: "起動時間", Uptime: "起動時間",
"Cert Exp.": "証明書有効期限", "Cert Exp.": "証明書有効期限",
days: "日間", day: "日 | 日間",
day: "日",
"-day": "-日", "-day": "-日",
hour: "時間", hour: "時間",
"-hour": "-時間", "-hour": "-時間",

View File

@@ -55,7 +55,6 @@ export default {
Current: "현재", Current: "현재",
Uptime: "업타임", Uptime: "업타임",
"Cert Exp.": "인증서 만료", "Cert Exp.": "인증서 만료",
days: "일",
day: "일", day: "일",
"-day": "-일", "-day": "-일",
hour: "시간", hour: "시간",

View File

@@ -55,8 +55,7 @@ export default {
Current: "Nåværende", Current: "Nåværende",
Uptime: "Oppetid", Uptime: "Oppetid",
"Cert Exp.": "Sertifikat utløper", "Cert Exp.": "Sertifikat utløper",
days: "dager", day: "dag | dager",
day: "dag",
"-day": "-dag", "-day": "-dag",
hour: "time", hour: "time",
"-hour": "-time", "-hour": "-time",

View File

@@ -52,8 +52,7 @@ export default {
Current: "Huidig", Current: "Huidig",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert. verl.", "Cert Exp.": "Cert. verl.",
days: "dagen", day: "dag | dagen",
day: "dag",
"-day": "-dag", "-day": "-dag",
hour: "uur", hour: "uur",
"-hour": "-uur", "-hour": "-uur",

View File

@@ -55,8 +55,7 @@ export default {
Current: "Aktualny", Current: "Aktualny",
Uptime: "Czas pracy", Uptime: "Czas pracy",
"Cert Exp.": "Certyfikat wygasa", "Cert Exp.": "Certyfikat wygasa",
days: "dni", day: "dzień | dni",
day: "dzień",
"-day": " dni", "-day": " dni",
hour: "godzina", hour: "godzina",
"-hour": " godzin", "-hour": " godzin",

View File

@@ -55,8 +55,7 @@ export default {
Current: "Atual", Current: "Atual",
Uptime: "Tempo de atividade", Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "dias", day: "dia | dias",
day: "dia",
"-day": "-dia", "-day": "-dia",
hour: "hora", hour: "hora",
"-hour": "-hora", "-hour": "-hora",

View File

@@ -44,8 +44,7 @@ export default {
Current: "Текущий", Current: "Текущий",
Uptime: "Аптайм", Uptime: "Аптайм",
"Cert Exp.": "Сертификат истекает", "Cert Exp.": "Сертификат истекает",
days: "дней", day: "день | дней",
day: "день",
"-day": " дней", "-day": " дней",
hour: "час", hour: "час",
"-hour": " часа", "-hour": " часа",

View File

@@ -56,8 +56,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Potek certifikata", "Cert Exp.": "Potek certifikata",
days: "dni", day: "dan | dni",
day: "dan",
"-day": "-dni", "-day": "-dni",
hour: "ura", hour: "ura",
"-hour": "-ur", "-hour": "-ur",

View File

@@ -44,8 +44,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Vreme rada", Uptime: "Vreme rada",
"Cert Exp.": "Istek sert.", "Cert Exp.": "Istek sert.",
days: "dana", day: "dan | dana",
day: "dan",
"-day": "-dana", "-day": "-dana",
hour: "sat", hour: "sat",
"-hour": "-sata", "-hour": "-sata",

View File

@@ -44,8 +44,7 @@ export default {
Current: "Тренутно", Current: "Тренутно",
Uptime: "Време рада", Uptime: "Време рада",
"Cert Exp.": "Истек серт.", "Cert Exp.": "Истек серт.",
days: "дана", day: "дан | дана",
day: "дан",
"-day": "-дана", "-day": "-дана",
hour: "сат", hour: "сат",
"-hour": "-сата", "-hour": "-сата",

View File

@@ -44,8 +44,7 @@ export default {
Current: "Nuvarande", Current: "Nuvarande",
Uptime: "Drifttid", Uptime: "Drifttid",
"Cert Exp.": "Certifikat utgår", "Cert Exp.": "Certifikat utgår",
days: "dagar", day: "dag | dagar",
day: "dag",
"-day": " dagar", "-day": " dagar",
hour: "timme", hour: "timme",
"-hour": " timmar", "-hour": " timmar",

View File

@@ -57,8 +57,7 @@ export default {
Current: "Şu anda", Current: "Şu anda",
Uptime: "Çalışma zamanı", Uptime: "Çalışma zamanı",
"Cert Exp.": "Sertifika Süresi", "Cert Exp.": "Sertifika Süresi",
days: "günler", day: "gün | günler",
day: "gün",
"-day": "-gün", "-day": "-gün",
hour: "saat", hour: "saat",
"-hour": "-saat", "-hour": "-saat",

View File

@@ -44,8 +44,7 @@ export default {
Current: "Поточний", Current: "Поточний",
Uptime: "Аптайм", Uptime: "Аптайм",
"Cert Exp.": "Сертифікат спливає", "Cert Exp.": "Сертифікат спливає",
days: "днів", day: "день | днів",
day: "день",
"-day": " днів", "-day": " днів",
hour: "година", hour: "година",
"-hour": " години", "-hour": " години",

View File

@@ -56,7 +56,6 @@ export default {
Current: "Hiện tại", Current: "Hiện tại",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert hết hạn", "Cert Exp.": "Cert hết hạn",
days: "ngày",
day: "ngày", day: "ngày",
"-day": "-ngày", "-day": "-ngày",
hour: "giờ", hour: "giờ",

View File

@@ -57,7 +57,6 @@ export default {
Current: "当前", Current: "当前",
Uptime: "在线时间", Uptime: "在线时间",
"Cert Exp.": "证书有效期", "Cert Exp.": "证书有效期",
days: "天",
day: "天", day: "天",
"-day": " 天", "-day": " 天",
hour: "小时", hour: "小时",
@@ -520,4 +519,14 @@ export default {
wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。", wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。",
signedInDisp: "当前用户: {0}", signedInDisp: "当前用户: {0}",
signedInDispDisabled: "已禁用身份验证", signedInDispDisabled: "已禁用身份验证",
dnsPortDescription: "DNS 服务器端口,默认为 53你可以在任何时候更改此端口.",
error: "错误",
critical: "关键",
wayToGetPagerDutyKey: "你可以在 Service -> Service Directory -> (Select a service) -> Integrations -> Add integration 页面中搜索 \"Events API V2\" 以获取此 Integration Key更多信息请参见 {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "自动标记为已解决或已读",
"do nothing": "不做任何操作",
"auto acknowledged": "自动标记为已读",
"auto resolve": "自动标记为已解决",
}; };

View File

@@ -30,7 +30,6 @@ export default {
Current: "目前", Current: "目前",
Uptime: "上線率", Uptime: "上線率",
"Cert Exp.": "証書期限", "Cert Exp.": "証書期限",
days: "日",
day: "日", day: "日",
"-day": "日", "-day": "日",
hour: "小時", hour: "小時",

View File

@@ -56,7 +56,6 @@ export default {
Current: "目前", Current: "目前",
Uptime: "運作率", Uptime: "運作率",
"Cert Exp.": "憑證期限", "Cert Exp.": "憑證期限",
days: "天",
day: "天", day: "天",
"-day": "天", "-day": "天",
hour: "小時", hour: "小時",

View File

@@ -77,7 +77,7 @@
<h4>{{ $t("Cert Exp.") }}</h4> <h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p> <p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $t("days") }}</a> <a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -11,6 +11,7 @@
<div class="my-3"> <div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label> <label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select"> <select id="type" v-model="monitor.type" class="form-select">
<optgroup label="General Monitor Type">
<option value="http"> <option value="http">
HTTP(s) HTTP(s)
</option> </option>
@@ -26,15 +27,25 @@
<option value="dns"> <option value="dns">
DNS DNS
</option> </option>
</optgroup>
<optgroup label="Passive Monitor Type">
<option value="push"> <option value="push">
Push Push
</option> </option>
</optgroup>
<optgroup label="Specific Monitor Type">
<option value="steam"> <option value="steam">
{{ $t("Steam Game Server") }} {{ $t("Steam Game Server") }}
</option> </option>
<option value="mqtt"> <option value="mqtt">
MQTT MQTT
</option> </option>
<option value="sqlserver">
SQL Server
</option>
</optgroup>
</select> </select>
</div> </div>
@@ -157,6 +168,18 @@
</div> </div>
</template> </template>
<!-- SQL Server -->
<template v-if="monitor.type === 'sqlserver'">
<div class="my-3">
<label for="sqlserverConnectionString" class="form-label">SQL Server {{ $t("Connection String") }}</label>
<input id="sqlserverConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</div>
<div class="my-3">
<label for="sqlserverQuery" class="form-label">SQL Server {{ $t("Query") }}</label>
<textarea id="sqlserverQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div>
</template>
<!-- Interval --> <!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@@ -345,9 +368,25 @@
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea> <textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div> </div>
<!-- HTTP Basic Auth --> <!-- HTTP Auth -->
<h4 class="mt-5 mb-2">{{ $t("HTTP Basic Auth") }}</h4> <h4 class="mt-5 mb-2">{{ $t("HTTP Authentication") }}</h4>
<!-- Method -->
<div class="my-3">
<label for="method" class="form-label">{{ $t("Method") }}</label>
<select id="method" v-model="monitor.authMethod" class="form-select">
<option :value="null">
None
</option>
<option value="basic">
Basic
</option>
<option value="ntlm">
NTLM
</option>
</select>
</div>
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
<div class="my-3"> <div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Username") }}</label> <label for="basicauth" class="form-label">{{ $t("Username") }}</label>
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')"> <input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
@@ -357,6 +396,18 @@
<label for="basicauth" class="form-label">{{ $t("Password") }}</label> <label for="basicauth" class="form-label">{{ $t("Password") }}</label>
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')"> <input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
</div> </div>
<template v-if="monitor.authMethod === 'ntlm' ">
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Domain") }}</label>
<input id="basicauth-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
</div>
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Workstation") }}</label>
<input id="basicauth-workstation" v-model="monitor.authWorkstation" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Workstation')">
</div>
</template>
</template>
</template> </template>
</div> </div>
</div> </div>
@@ -546,6 +597,7 @@ export default {
mqttPassword: "", mqttPassword: "",
mqttTopic: "", mqttTopic: "",
mqttSuccessMessage: "", mqttSuccessMessage: "",
authMethod: null,
}; };
if (this.$root.proxyList && !this.monitor.proxyId) { if (this.$root.proxyList && !this.monitor.proxyId) {

View File

@@ -32,6 +32,7 @@
<ul> <ul>
<li>{{ $t("Retype the address.") }}</li> <li>{{ $t("Retype the address.") }}</li>
<li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li> <li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li>
<li><a href="/" class="go-back">Go back to home page.</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -145,6 +145,10 @@ export default {
this.settings.keepDataPeriodDays = 180; this.settings.keepDataPeriodDays = 180;
} }
if (this.settings.tlsExpiryNotifyDays === undefined) {
this.settings.tlsExpiryNotifyDays = [];
}
this.settingsLoaded = true; this.settingsLoaded = true;
}); });
}, },

View File

@@ -98,7 +98,7 @@
<h1 class="mb-4 title-flex"> <h1 class="mb-4 title-flex">
<!-- Logo --> <!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod"> <span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" @load="statusPageLogoLoaded" /> <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span> </span>
@@ -538,7 +538,7 @@ export default {
this.slug = "default"; this.slug = "default";
} }
axios.get("/api/status-page/" + this.slug).then((res) => { this.getData().then((res) => {
this.config = res.data.config; this.config = res.data.config;
if (!this.config.domainNameList) { if (!this.config.domainNameList) {
@@ -551,6 +551,11 @@ export default {
this.incident = res.data.incident; this.incident = res.data.incident;
this.$root.publicGroupList = res.data.publicGroupList; this.$root.publicGroupList = res.data.publicGroupList;
}).catch( function (error) {
if (error.response.status === 404) {
location.href = "/page-not-found";
}
console.log(error);
}); });
// 5mins a loop // 5mins a loop
@@ -567,6 +572,21 @@ export default {
}, },
methods: { methods: {
/**
* Get status page data
* It should be preloaded in window.preloadData
* @returns {Promise<any>}
*/
getData: function () {
if (window.preloadData) {
return new Promise(resolve => resolve({
data: window.preloadData
}));
} else {
return axios.get("/api/status-page/" + this.slug);
}
},
highlighter(code) { highlighter(code) {
return highlight(code, languages.css); return highlight(code, languages.css);
}, },
@@ -604,6 +624,9 @@ export default {
this.$root.initSocketIO(true); this.$root.initSocketIO(true);
this.enableEditMode = true; this.enableEditMode = true;
this.clickedEditButton = true; this.clickedEditButton = true;
// Try to fix #1658
this.loadedData = true;
} }
}, },
@@ -687,11 +710,6 @@ export default {
} }
}, },
statusPageLogoLoaded(eventPayload) {
// Remark: may not work in dev, due to CORS
favicon.image(eventPayload.target);
},
createIncident() { createIncident() {
this.enableEditIncidentMode = true; this.enableEditIncidentMode = true;

View File

@@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import EmptyLayout from "./layouts/EmptyLayout.vue"; import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue"; import Layout from "./layouts/Layout.vue";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
@@ -8,22 +9,23 @@ import EditMonitor from "./pages/EditMonitor.vue";
import List from "./pages/List.vue"; import List from "./pages/List.vue";
const Settings = () => import("./pages/Settings.vue"); const Settings = () => import("./pages/Settings.vue");
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
const StatusPage = () => import("./pages/StatusPage.vue"); import StatusPage from "./pages/StatusPage.vue";
import Entry from "./pages/Entry.vue"; import Entry from "./pages/Entry.vue";
import Appearance from "./components/settings/Appearance.vue";
import General from "./components/settings/General.vue";
import Notifications from "./components/settings/Notifications.vue";
import ReverseProxy from "./components/settings/ReverseProxy.vue";
import MonitorHistory from "./components/settings/MonitorHistory.vue";
import Security from "./components/settings/Security.vue";
import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
import ManageStatusPage from "./pages/ManageStatusPage.vue"; import ManageStatusPage from "./pages/ManageStatusPage.vue";
import AddStatusPage from "./pages/AddStatusPage.vue"; import AddStatusPage from "./pages/AddStatusPage.vue";
import NotFound from "./pages/NotFound.vue"; import NotFound from "./pages/NotFound.vue";
// Settings - Sub Pages
import Appearance from "./components/settings/Appearance.vue";
import General from "./components/settings/General.vue";
const Notifications = () => import("./components/settings/Notifications.vue");
import ReverseProxy from "./components/settings/ReverseProxy.vue";
import MonitorHistory from "./components/settings/MonitorHistory.vue";
const Security = () => import("./components/settings/Security.vue");
import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
const routes = [ const routes = [
{ {
path: "/", path: "/",

View File

@@ -159,7 +159,6 @@ describe("Test genSecret", () => {
expect(secret).toContain("A"); expect(secret).toContain("A");
expect(secret).toContain("9"); expect(secret).toContain("9");
}); });
}); });
describe("Test reset-password", () => { describe("Test reset-password", () => {
@@ -169,6 +168,9 @@ describe("Test reset-password", () => {
}); });
describe("Test Discord Notification Provider", () => { describe("Test Discord Notification Provider", () => {
const hostname = "discord.com";
const port = 1337;
const sendNotification = async (hostname, port, type) => { const sendNotification = async (hostname, port, type) => {
const discordProvider = new Discord(); const discordProvider = new Discord();
@@ -191,63 +193,35 @@ describe("Test Discord Notification Provider", () => {
); );
}; };
it("should send hostname for dns monitors", async () => {
const hostname = "discord.com";
await sendNotification(hostname, null, "dns");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
hostname
);
});
it("should send hostname for ping monitors", async () => { it("should send hostname for ping monitors", async () => {
const hostname = "discord.com";
await sendNotification(hostname, null, "ping"); await sendNotification(hostname, null, "ping");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(hostname);
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
hostname
);
}); });
it("should send hostname for port monitors", async () => { it.each([ "dns", "port", "steam" ])("should send hostname for %p monitors", async (type) => {
const hostname = "discord.com"; await sendNotification(hostname, port, type);
const port = 1337; expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(`${hostname}:${port}`);
await sendNotification(hostname, port, "port");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
`${hostname}:${port}`
);
});
it("should send hostname for steam monitors", async () => {
const hostname = "discord.com";
const port = 1337;
await sendNotification(hostname, port, "steam");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
`${hostname}:${port}`
);
}); });
}); });
describe("The function filterAndJoin", () => { describe("The function filterAndJoin", () => {
it("should join and array of strings to one string", () => { it("should join and array of strings to one string", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"]); const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree"); expect(result).toBe("onetwothree");
}); });
it("should join strings using a given connector", () => { it("should join strings using a given connector", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"], "-"); const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ], "-");
expect(result).toBe("one-two-three"); expect(result).toBe("one-two-three");
}); });
it("should filter null, undefined and empty strings before joining", () => { it("should filter null, undefined and empty strings before joining", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", "three"], "--"); const result = utilServerRewire.filterAndJoin([ undefined, "", "three" ], "--");
expect(result).toBe("three"); expect(result).toBe("three");
}); });
it("should return an empty string if all parts are filtered out", () => { it("should return an empty string if all parts are filtered out", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", ""], "--"); const result = utilServerRewire.filterAndJoin([ undefined, "", "" ], "--");
expect(result).toBe(""); expect(result).toBe("");
}); });
}); });