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
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?
(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:
- Bug/Security fix
- Translations
- Adding notification providers
⚠️ Discuss First
⚠️ Discussion First
- Large pull requests
- 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
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. 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
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
- 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
- The web UI styling should be consistent and nice.
## 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
Use this section to tell people about which versions of your project are
currently being supported with security updates.
### 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.

View File

@@ -1,10 +1,14 @@
import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
@@ -12,7 +16,18 @@ export default defineConfig({
legacy({
targets: [ "ie > 11" ],
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
})
}),
visualizer({
filename: "tmp/dist-stats.html"
}),
viteCompression({
algorithm: "gzip",
filter: viteCompressionFilter,
}),
viteCompression({
algorithm: "brotliCompress",
filter: viteCompressionFilter,
}),
],
css: {
postcss: {
@@ -21,4 +36,13 @@ export default defineConfig({
"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",
"version": "1.16.1",
"version": "1.17.0-beta.0",
"license": "MIT",
"repository": {
"type": "git",
@@ -57,7 +57,8 @@
"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-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": {
"@fortawesome/fontawesome-svg-core": "~1.2.36",
@@ -68,6 +69,8 @@
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.26.1",
"axios-cached-dns-resolve": "^3.0.6",
"axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3",
"bootstrap": "5.1.3",
@@ -76,12 +79,16 @@
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "^1.7.4",
"dayjs": "^1.11.0",
"esm-wallaby": "^3.2.26",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "^2.1.7",
"favico.js": "^0.3.10",
"form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7",
@@ -92,6 +99,7 @@
"jwt-decode": "^3.1.2",
"limiter": "^2.1.0",
"mqtt": "^4.2.8",
"mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
@@ -129,27 +137,31 @@
"@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4",
"@vitejs/plugin-vue": "~1.9.4",
"@vue/compiler-sfc": "~3.2.31",
"@vitejs/plugin-legacy": "~1.8.2",
"@vitejs/plugin-vue": "~2.3.3",
"@vue/compiler-sfc": "~3.2.36",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"concurrently": "^7.1.0",
"core-js": "~3.18.3",
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"lru-cache": "^7.7.1",
"npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1",
"puppeteer": "~13.1.3",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
"stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0",
"typescript": "~4.4.4",
"vite": "~2.6.14",
"vite": "~2.9.9",
"vite-plugin-compression": "^0.5.1",
"wait-on": "^6.0.1"
}
}

View File

@@ -58,6 +58,8 @@ class Database {
"patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.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 { Prometheus } = require("../prometheus");
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 { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@@ -17,6 +17,12 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
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:
* 0 = DOWN
@@ -87,7 +93,12 @@ class Monitor extends BeanModel {
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
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) {
@@ -213,7 +224,7 @@ class Monitor extends BeanModel {
// HTTP basic auth
let basicAuthHeader = {};
if (this.basic_auth_user) {
if (this.auth_method === "basic") {
basicAuthHeader = {
"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 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.ping = dayjs().valueOf() - startTime;
@@ -312,7 +337,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found";
bean.status = UP;
} 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");
}
let res = await axios.get(steamApiUrl, {
let res = await axiosClient.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
@@ -443,6 +472,14 @@ class Monitor extends BeanModel {
interval: this.interval,
});
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 {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
@@ -493,7 +530,7 @@ class Monitor extends BeanModel {
}
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) {
if (this.retryInterval > 0) {
beatInterval = this.retryInterval;
@@ -837,10 +874,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
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 { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class StatusPage extends BeanModel {
/**
* Like this: { "test-uptime.kuma.pet": "default" }
* @type {{}}
*/
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
* 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 Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify");
const Ntfy = require("./notification-providers/ntfy");
const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost");
@@ -52,6 +53,7 @@ class Notification {
new Discord(),
new Teams(),
new Gotify(),
new Ntfy(),
new Line(),
new LunaSea(),
new Feishu(),

View File

@@ -1,5 +1,5 @@
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 apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
@@ -92,115 +92,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
}
} catch (e) {
response.json({
response.status(404).json({
ok: false,
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) => {
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;

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 { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
log.info("server", "Welcome to Uptime Kuma");
@@ -35,6 +35,7 @@ const fs = require("fs");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken");
@@ -148,22 +149,6 @@ let jwtSecret = null;
*/
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 () => {
Database.init(args);
await initDatabase(testMode);
@@ -179,13 +164,17 @@ try {
// Entry Page
app.get("/", async (request, response) => {
debug(`Request Domain: ${request.hostname}`);
log.debug("entry", `Request Domain: ${request.hostname}`);
if (request.hostname in StatusPage.domainMappingList) {
debug("This is a status page domain");
response.send(indexHTML);
log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[request.hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
}
@@ -214,7 +203,9 @@ try {
// With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist"));
app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload
app.use("/upload", express.static(Database.uploadDir));
@@ -227,12 +218,16 @@ try {
const apiRouter = require("./routers/api-router");
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.
app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found.");
} else {
response.send(indexHTML);
response.send(server.indexHTML);
}
});
@@ -674,6 +669,11 @@ try {
bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic;
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);
@@ -1247,8 +1247,11 @@ try {
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
authMethod: monitorListData[i].authMethod,
basic_auth_user: monitorListData[i].basic_auth_user,
basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
hostname: monitorListData[i].hostname,

View File

@@ -29,6 +29,12 @@ class UptimeKumaServer {
httpServer = undefined;
io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -55,6 +61,16 @@ class UptimeKumaServer {
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);
}

View File

@@ -10,6 +10,8 @@ const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { NtlmClient } = require("axios-ntlm");
// From ping-lite
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
* @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
// prevent issues with ::1:5300 (::1 port 5300)
resolverServer = resolverServer.replace("[", "").replace("]", "");
resolver.setServers([`[${resolverServer}]:${resolverPort}`]);
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
return new Promise((resolve, reject) => {
if (rrtype === "PTR") {
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
* @param {string} key Key of setting to retrieve
@@ -558,3 +605,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
exports.filterAndJoin = (parts, 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>
<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>
<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>
</div>
<div class="mb-3">

View File

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

View File

@@ -20,16 +20,91 @@
</button>
</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" />
</div>
</template>
<script>
import NotificationDialog from "../../components/NotificationDialog.vue";
import ActionInput from "../ActionInput.vue";
export default {
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>
@@ -37,10 +112,27 @@ export default {
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.btn-rm-expiry {
padding-left: 11px;
padding-right: 11px;
}
.dark {
.list-group-item {
background-color: $dark-bg2;
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>

View File

@@ -55,8 +55,7 @@ export default {
Current: "Текущ",
Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат",
days: "дни",
day: "ден",
day: "ден | дни",
"-day": "-дни",
hour: "час",
"-hour": "-часa",
@@ -515,4 +514,18 @@ export default {
"Go back to the previous page.": "Да се върнете към предишната страница.",
"Coming Soon": "Очаквайте скоро",
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í",
Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu",
days: "dny/í",
day: "den",
day: "den | dny/í",
"-day": "-dní",
hour: "hodina",
"-hour": "-hodin",

View File

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

View File

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

View File

@@ -57,8 +57,7 @@ export default {
Current: "Current",
Uptime: "Uptime",
"Cert Exp.": "Cert Exp.",
days: "days",
day: "day",
day: "day | days",
"-day": "-day",
hour: "hour",
"-hour": "-hour",
@@ -525,4 +524,8 @@ export default {
"Go back to the previous page.": "Go back to the previous page.",
"Coming Soon": "Coming Soon",
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",
Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.",
days: "días",
day: "día",
day: "día | días",
"-day": "-día",
hour: "hora",
"-hour": "-hora",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,7 +57,6 @@ export default {
Current: "当前",
Uptime: "在线时间",
"Cert Exp.": "证书有效期",
days: "天",
day: "天",
"-day": " 天",
hour: "小时",
@@ -520,4 +519,14 @@ export default {
wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。",
signedInDisp: "当前用户: {0}",
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: "目前",
Uptime: "上線率",
"Cert Exp.": "証書期限",
days: "日",
day: "日",
"-day": "日",
hour: "小時",

View File

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

View File

@@ -77,7 +77,7 @@
<h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<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>
</div>
</div>

View File

@@ -11,30 +11,41 @@
<div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
<option value="dns">
DNS
</option>
<option value="push">
Push
</option>
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="mqtt">
MQTT
</option>
<optgroup label="General Monitor Type">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
<option value="dns">
DNS
</option>
</optgroup>
<optgroup label="Passive Monitor Type">
<option value="push">
Push
</option>
</optgroup>
<optgroup label="Specific Monitor Type">
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="mqtt">
MQTT
</option>
<option value="sqlserver">
SQL Server
</option>
</optgroup>
</select>
</div>
@@ -157,6 +168,18 @@
</div>
</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 -->
<div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@@ -345,18 +368,46 @@
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div>
<!-- HTTP Basic Auth -->
<h4 class="mt-5 mb-2">{{ $t("HTTP Basic Auth") }}</h4>
<!-- HTTP Auth -->
<h4 class="mt-5 mb-2">{{ $t("HTTP Authentication") }}</h4>
<!-- Method -->
<div class="my-3">
<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')">
<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">
<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')">
</div>
<div class="my-3">
<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')">
</div>
<div class="my-3">
<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')">
</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>
</div>
</div>
@@ -546,6 +597,7 @@ export default {
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
authMethod: null,
};
if (this.$root.proxyList && !this.monitor.proxyId) {

View File

@@ -32,6 +32,7 @@
<ul>
<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">Go back to home page.</a></li>
</ul>
</div>
</div>

View File

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

View File

@@ -98,7 +98,7 @@
<h1 class="mb-4 title-flex">
<!-- Logo -->
<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" />
</span>
@@ -538,7 +538,7 @@ export default {
this.slug = "default";
}
axios.get("/api/status-page/" + this.slug).then((res) => {
this.getData().then((res) => {
this.config = res.data.config;
if (!this.config.domainNameList) {
@@ -551,6 +551,11 @@ export default {
this.incident = res.data.incident;
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
@@ -567,6 +572,21 @@ export default {
},
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) {
return highlight(code, languages.css);
},
@@ -604,6 +624,9 @@ export default {
this.$root.initSocketIO(true);
this.enableEditMode = 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() {
this.enableEditIncidentMode = true;

View File

@@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from "vue-router";
import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue";
import Dashboard from "./pages/Dashboard.vue";
@@ -8,22 +9,23 @@ import EditMonitor from "./pages/EditMonitor.vue";
import List from "./pages/List.vue";
const Settings = () => import("./pages/Settings.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 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 AddStatusPage from "./pages/AddStatusPage.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 = [
{
path: "/",

View File

@@ -159,7 +159,6 @@ describe("Test genSecret", () => {
expect(secret).toContain("A");
expect(secret).toContain("9");
});
});
describe("Test reset-password", () => {
@@ -169,6 +168,9 @@ describe("Test reset-password", () => {
});
describe("Test Discord Notification Provider", () => {
const hostname = "discord.com";
const port = 1337;
const sendNotification = async (hostname, port, type) => {
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 () => {
const hostname = "discord.com";
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 () => {
const hostname = "discord.com";
const port = 1337;
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}`
);
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 = utilServerRewire.filterAndJoin(["one", "two", "three"]);
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree");
});
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");
});
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");
});
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("");
});
});