mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-11 07:02:36 +02:00
Merge branch 'louislam:master' into master
This commit is contained in:
commit
a1a17a0d25
23 changed files with 722 additions and 227 deletions
93
package-lock.json
generated
93
package-lock.json
generated
|
@ -19,6 +19,7 @@
|
|||
"axios": "~0.21.4",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "~5.1.1",
|
||||
"chardet": "^1.3.0",
|
||||
"chart.js": "~3.5.1",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"command-exists": "~1.2.9",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"express-basic-auth": "~1.2.0",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
|
@ -64,7 +66,7 @@
|
|||
"@vitejs/plugin-legacy": "~1.6.1",
|
||||
"@vitejs/plugin-vue": "~1.9.2",
|
||||
"@vue/compiler-sfc": "~3.2.19",
|
||||
"babel-plugin-rewire": "^1.2.0",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
|
@ -3551,6 +3553,17 @@
|
|||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -3813,6 +3826,11 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-1.3.0.tgz",
|
||||
"integrity": "sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
|
||||
|
@ -6303,11 +6321,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
@ -9527,6 +9545,17 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body/node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
|
@ -11978,6 +12007,18 @@
|
|||
"iconv-lite": "0.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
|
||||
|
@ -14905,6 +14946,14 @@
|
|||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -15090,6 +15139,11 @@
|
|||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
|
||||
"dev": true
|
||||
},
|
||||
"chardet": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-1.3.0.tgz",
|
||||
"integrity": "sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
|
||||
|
@ -16967,11 +17021,11 @@
|
|||
"dev": true
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
|
@ -19393,6 +19447,16 @@
|
|||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
|
@ -21251,6 +21315,17 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"iconv-lite": "0.4.24"
|
||||
},
|
||||
"dependencies": {
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"whatwg-mimetype": {
|
||||
|
|
|
@ -60,9 +60,9 @@
|
|||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.21.4",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "~5.1.1",
|
||||
"chardet": "^1.3.0",
|
||||
"chart.js": "~3.5.1",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"command-exists": "~1.2.9",
|
||||
|
@ -72,6 +72,7 @@
|
|||
"express-basic-auth": "~1.2.0",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
|
@ -108,6 +109,7 @@
|
|||
"@vitejs/plugin-legacy": "~1.6.1",
|
||||
"@vitejs/plugin-vue": "~1.9.2",
|
||||
"@vue/compiler-sfc": "~3.2.19",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
|
|
|
@ -292,54 +292,13 @@ class Monitor extends BeanModel {
|
|||
|
||||
let beatInterval = this.interval;
|
||||
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat.status, bean.status);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
// Send only if the first beat is DOWN
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
@ -546,6 +505,53 @@ class Monitor extends BeanModel {
|
|||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
|
||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||
return isImportant;
|
||||
}
|
||||
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
|
108
server/notification-providers/aliyun-sms.js
Normal file
108
server/notification-providers/aliyun-sms.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
const qs = require("qs");
|
||||
|
||||
class AliyunSMS extends NotificationProvider {
|
||||
name = "AliyunSMS";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let msgBody = JSON.stringify({
|
||||
name: monitorJSON["name"],
|
||||
time: heartbeatJSON["time"],
|
||||
status: this.statusToString(heartbeatJSON["status"]),
|
||||
msg: heartbeatJSON["msg"],
|
||||
});
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
let msgBody = JSON.stringify({
|
||||
name: "",
|
||||
time: "",
|
||||
status: "",
|
||||
msg: msg,
|
||||
});
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
PhoneNumbers: notification.phonenumber,
|
||||
TemplateCode: notification.templateCode,
|
||||
SignName: notification.signName,
|
||||
TemplateParam: msgbody,
|
||||
AccessKeyId: notification.accessKeyId,
|
||||
Format: "JSON",
|
||||
SignatureMethod: "HMAC-SHA1",
|
||||
SignatureVersion: "1.0",
|
||||
SignatureNonce: Math.random().toString(),
|
||||
Timestamp: new Date().toISOString(),
|
||||
Action: "SendSms",
|
||||
Version: "2017-05-25",
|
||||
};
|
||||
|
||||
params.Signature = this.sign(params, notification.secretAccessKey);
|
||||
let config = {
|
||||
method: "POST",
|
||||
url: "http://dysmsapi.aliyuncs.com/",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: qs.stringify(params),
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.Message == "OK") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Aliyun request sign */
|
||||
sign(param, AccessKeySecret) {
|
||||
let param2 = {};
|
||||
let data = [];
|
||||
|
||||
let oa = Object.keys(param).sort();
|
||||
|
||||
for (let i = 0; i < oa.length; i++) {
|
||||
let key = oa[i];
|
||||
param2[key] = param[key];
|
||||
}
|
||||
|
||||
for (let key in param2) {
|
||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||
}
|
||||
|
||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||
return Crypto
|
||||
.createHmac("sha1", `${AccessKeySecret}&`)
|
||||
.update(Buffer.from(StringToSign))
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
case UP:
|
||||
return "UP";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AliyunSMS;
|
79
server/notification-providers/dingding.js
Normal file
79
server/notification-providers/dingding.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
|
||||
class DingDing extends NotificationProvider {
|
||||
name = "DingDing";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let params = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: monitorJSON["name"],
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
let params = {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content: msg
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
|
||||
let config = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
|
||||
data: JSON.stringify(params),
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.errmsg == "ok") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** DingDing sign */
|
||||
sign(timestamp, secretKey) {
|
||||
return Crypto
|
||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
case UP:
|
||||
return "UP";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DingDing;
|
|
@ -39,8 +39,9 @@ class Slack extends NotificationProvider {
|
|||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
|
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
|
|||
pass: notification.smtpPassword,
|
||||
};
|
||||
}
|
||||
// Lets start with default subject and empty string for custom one
|
||||
let subject = msg;
|
||||
|
||||
// Change the subject if:
|
||||
// - The msg ends with "Testing" or
|
||||
// - Actual Up/Down Notification
|
||||
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
|
||||
let customSubject = "";
|
||||
|
||||
// Our subject cannot end with whitespace it's often raise spam score
|
||||
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
|
||||
if (notification.customSubject) {
|
||||
customSubject = notification.customSubject.trim();
|
||||
}
|
||||
|
||||
// If custom subject is not empty, change subject for notification
|
||||
if (customSubject !== "") {
|
||||
|
||||
// Replace "MACROS" with corresponding variable
|
||||
let replaceName = new RegExp("{{NAME}}", "g");
|
||||
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
|
||||
let replaceStatus = new RegExp("{{STATUS}}", "g");
|
||||
|
||||
// Lets start with dummy values to simplify code
|
||||
let monitorName = "Test";
|
||||
let monitorHostnameOrURL = "testing.hostname";
|
||||
let serviceStatus = "⚠️ Test";
|
||||
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
}
|
||||
}
|
||||
|
||||
if (heartbeatJSON !== null) {
|
||||
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
|
||||
}
|
||||
|
||||
// Break replace to one by line for better readability
|
||||
customSubject = customSubject.replace(replaceStatus, serviceStatus);
|
||||
customSubject = customSubject.replace(replaceName, monitorName);
|
||||
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
|
||||
|
||||
subject = customSubject;
|
||||
}
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
|
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
|
|||
cc: notification.smtpCC,
|
||||
bcc: notification.smtpBCC,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
subject: subject,
|
||||
text: bodyTextContent,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
|
|
|
@ -19,6 +19,8 @@ const Teams = require("./notification-providers/teams");
|
|||
const Telegram = require("./notification-providers/telegram");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
|
||||
class Notification {
|
||||
|
||||
|
@ -31,6 +33,8 @@ class Notification {
|
|||
|
||||
const list = [
|
||||
new Apprise(),
|
||||
new AliyunSms(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Teams(),
|
||||
new Gotify(),
|
||||
|
|
|
@ -4,10 +4,7 @@ const net = require("net");
|
|||
const spawn = require("child_process").spawn;
|
||||
const events = require("events");
|
||||
const fs = require("fs");
|
||||
const WIN = /^win/.test(process.platform);
|
||||
const LIN = /^linux/.test(process.platform);
|
||||
const MAC = /^darwin/.test(process.platform);
|
||||
const FBSD = /^freebsd/.test(process.platform);
|
||||
const util = require("./util-server");
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
|
@ -23,12 +20,12 @@ function Ping(host, options) {
|
|||
|
||||
const timeout = 10;
|
||||
|
||||
if (WIN) {
|
||||
if (util.WIN) {
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||
|
||||
} else if (LIN) {
|
||||
} else if (util.LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||
|
@ -40,7 +37,7 @@ function Ping(host, options) {
|
|||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (MAC) {
|
||||
} else if (util.MAC) {
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
this._bin = "/sbin/ping6";
|
||||
|
@ -51,7 +48,7 @@ function Ping(host, options) {
|
|||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (FBSD) {
|
||||
} else if (util.FBSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
|
@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
|
|||
});
|
||||
|
||||
this._ping.stdout.on("data", function (data) { // log stdout
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
|
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
|
|||
});
|
||||
|
||||
this._ping.stderr.on("data", function (data) { // log stderr
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
|
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
|
|||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||
* Thank @pemassi
|
||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||
* @param data
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertOutput(data) {
|
||||
if (util.WIN) {
|
||||
if (data) {
|
||||
return util.convertToUTF8(data);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const server = require("../server");
|
|||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP } = require("../../src/util");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
|
@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => {
|
|||
|
||||
router.get("/api/push/:pushToken", async (request, response) => {
|
||||
try {
|
||||
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping;
|
||||
let ping = request.query.ping || null;
|
||||
|
||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||
pushToken
|
||||
|
@ -30,12 +31,40 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||
throw new Error("Monitor not found or not active.");
|
||||
}
|
||||
|
||||
const previousHeartbeat = await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
monitor.id
|
||||
]);
|
||||
|
||||
let status = UP;
|
||||
if (monitor.isUpsideDown()) {
|
||||
status = flipStatus(status);
|
||||
}
|
||||
|
||||
let isFirstBeat = true;
|
||||
let previousStatus = status;
|
||||
let duration = 0;
|
||||
|
||||
let bean = R.dispense("heartbeat");
|
||||
bean.monitor_id = monitor.id;
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
bean.status = UP;
|
||||
|
||||
if (previousHeartbeat) {
|
||||
isFirstBeat = false;
|
||||
previousStatus = previousHeartbeat.status;
|
||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||
}
|
||||
|
||||
debug("PreviousStatus: " + previousStatus);
|
||||
debug("Current Status: " + status);
|
||||
|
||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
||||
bean.monitor_id = monitor.id;
|
||||
bean.status = status;
|
||||
bean.msg = msg;
|
||||
bean.ping = ping;
|
||||
bean.duration = duration;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
|
@ -45,6 +74,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||
response.json({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
if (bean.important) {
|
||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
response.json({
|
||||
ok: false,
|
||||
|
|
|
@ -43,7 +43,7 @@ console.log("Importing this project modules");
|
|||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
|
@ -61,12 +61,22 @@ console.info("Version: " + checkVersion.version);
|
|||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
const hostname = process.env.HOST || args.host;
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
if (!hostname && !FBSD) {
|
||||
hostname = process.env.HOST;
|
||||
}
|
||||
|
||||
if (hostname) {
|
||||
console.log("Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
|
||||
|
||||
// SSL
|
||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
|
||||
/**
|
||||
* Run unit test after the server is ready
|
||||
|
|
|
@ -6,6 +6,14 @@ const passwordHash = require("./password-hash");
|
|||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
exports.LIN = /^linux/.test(process.platform);
|
||||
exports.MAC = /^darwin/.test(process.platform);
|
||||
exports.FBSD = /^freebsd/.test(process.platform);
|
||||
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
|
@ -312,3 +320,14 @@ exports.startUnitTest = async () => {
|
|||
process.exit(code);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param body : Buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.convertToUTF8 = (body) => {
|
||||
const guessEncoding = chardet.detect(body);
|
||||
debug("Guess Encoding: " + guessEncoding);
|
||||
const str = iconv.decode(body, guessEncoding);
|
||||
return str.toString();
|
||||
};
|
||||
|
|
25
src/components/notifications/AliyunSms.vue
Normal file
25
src/components/notifications/AliyunSms.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
|
||||
|
||||
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
||||
|
||||
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
||||
|
||||
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
|
||||
|
||||
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
16
src/components/notifications/DingDing.vue
Normal file
16
src/components/notifications/DingDing.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
|
||||
|
||||
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>For safety, must use secret key</p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -5,7 +5,7 @@
|
|||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<a
|
||||
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
||||
target="_blank"
|
||||
|
|
|
@ -57,6 +57,18 @@
|
|||
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||
<div v-pre class="form-text">
|
||||
(leave blank for default one)<br />
|
||||
{{NAME}}: Service Name<br />
|
||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||
{{URL}}: URL<br />
|
||||
{{STATUS}}: Status<br />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -18,6 +18,8 @@ import Pushbullet from "./Pushbullet.vue";
|
|||
import Line from "./Line.vue";
|
||||
import Mattermost from "./Mattermost.vue";
|
||||
import Matrix from "./Matrix.vue";
|
||||
import AliyunSMS from "./AliyunSms.vue";
|
||||
import DingDing from "./DingDing.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
|
@ -40,11 +42,13 @@ const NotificationFormList = {
|
|||
"promosms": PromoSMS,
|
||||
"lunasea": LunaSea,
|
||||
"Feishu": Feishu,
|
||||
"AliyunSMS": AliyunSMS,
|
||||
"apprise": Apprise,
|
||||
"pushbullet": Pushbullet,
|
||||
"line": Line,
|
||||
"mattermost": Mattermost,
|
||||
"matrix": Matrix,
|
||||
"DingDing": DingDing
|
||||
}
|
||||
|
||||
export default NotificationFormList
|
||||
|
|
|
@ -201,6 +201,7 @@ export default {
|
|||
secureOptionTLS: "TLS (465)",
|
||||
"Ignore TLS Error": "Ignore TLS Error",
|
||||
"From Email": "From Email",
|
||||
emailCustomSubject: "Custom Subject",
|
||||
"To Email": "To Email",
|
||||
smtpCC: "CC",
|
||||
smtpBCC: "BCC",
|
||||
|
|
|
@ -116,39 +116,39 @@ export default {
|
|||
Events: "Olaylar",
|
||||
Heartbeats: "Sağlık Durumları",
|
||||
"Auto Get": "Otomatik Al",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
"Default enabled": "Default enabled",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
retryCheckEverySecond: "{0} Saniyede bir dene.",
|
||||
enableDefaultNotificationDescription: "Bu bildirim her yeni serviste aktif olacaktır. Bildirimi servisler için ayrı ayrı deaktive edebilirsiniz. ",
|
||||
importHandleDescription: "Aynı isimdeki bütün servisleri ve bildirimleri atlamak için 'Var olanı atla' seçiniz. 'Üzerine yaz' var olan bütün servisleri ve bildirimleri silecektir. ",
|
||||
confirmImportMsg: "Yedeği içeri aktarmak istediğinize emin misiniz? Lütfen doğru içeri aktarma seçeneğini seçtiğinizden emin olunuz. ",
|
||||
twoFAVerifyLabel: "Lütfen tokeni yazarak 2FA doğrulamanın çalıştığından emin olunuz.",
|
||||
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
|
||||
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
|
||||
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
|
||||
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
|
||||
"Import Backup": "Yedeği içe aktar",
|
||||
"Export Backup": "Yedeği dışa aktar",
|
||||
Export: "Dışa aktar",
|
||||
Import: "İçe aktar",
|
||||
"Default enabled": "Varsayılan etkinleştirilmiş",
|
||||
"Apply on all existing monitors": "Var olan bütün servislere uygula",
|
||||
backupDescription: "Bütün servisleri ve bildirimleri JSON dosyasına yedekleyebilirsiniz.",
|
||||
backupDescription2: "Not: Geçmiş ve etkinlik verileri içinde değildir.",
|
||||
backupDescription3: "Dışa aktarma dosyasında bildirim tokeni gibi hassas veriler bulunur, dikkatli bir şekilde saklayınız.",
|
||||
alertNoFile: "İçeri aktarmak için bir dosya seçiniz.",
|
||||
alertWrongFileType: "Lütfen bir JSON dosyası seçiniz.",
|
||||
"Clear all statistics": "Bütün istatistikleri temizle",
|
||||
"Skip existing": "Var olanı atla",
|
||||
Overwrite: "Üzerine yaz",
|
||||
Options: "Seçenekler",
|
||||
"Keep both": "İkisini sakla",
|
||||
"Verify Token": "Tokeni doğrula",
|
||||
"Setup 2FA": "2FA Kur",
|
||||
"Enable 2FA": "2FA Etkinleştir",
|
||||
"Disable 2FA": "2FA Devre dışı bırak",
|
||||
"2FA Settings": "2FA Ayarları",
|
||||
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
||||
Active: "Aktif",
|
||||
Inactive: "İnaktif",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
Tags: "Tags",
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hostname -->
|
||||
<!-- TCP Port / Ping / DNS only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
|
@ -140,7 +141,7 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type !== 'push'" class="my-3 form-check">
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
{{ $t("Upside Down Mode") }}
|
||||
|
|
236
src/util.js
236
src/util.js
|
@ -1,118 +1,118 @@
|
|||
"use strict";
|
||||
// Common Util for frontend and backend
|
||||
//
|
||||
// DOT NOT MODIFY util.js!
|
||||
// Need to run "tsc" to compile if there are any changes.
|
||||
//
|
||||
// Backend uses the compiled file util.js
|
||||
// Frontend uses util.ts
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getMonitorRelativeURL = exports.genSecret = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||
const _dayjs = require("dayjs");
|
||||
const dayjs = _dayjs;
|
||||
exports.isDev = process.env.NODE_ENV === "development";
|
||||
exports.appName = "Uptime Kuma";
|
||||
exports.DOWN = 0;
|
||||
exports.UP = 1;
|
||||
exports.PENDING = 2;
|
||||
exports.STATUS_PAGE_ALL_DOWN = 0;
|
||||
exports.STATUS_PAGE_ALL_UP = 1;
|
||||
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||
function flipStatus(s) {
|
||||
if (s === exports.UP) {
|
||||
return exports.DOWN;
|
||||
}
|
||||
if (s === exports.DOWN) {
|
||||
return exports.UP;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
exports.flipStatus = flipStatus;
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
exports.sleep = sleep;
|
||||
/**
|
||||
* PHP's ucfirst
|
||||
* @param str
|
||||
*/
|
||||
function ucfirst(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
const firstLetter = str.substr(0, 1);
|
||||
return firstLetter.toUpperCase() + str.substr(1);
|
||||
}
|
||||
exports.ucfirst = ucfirst;
|
||||
function debug(msg) {
|
||||
if (exports.isDev) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
exports.debug = debug;
|
||||
function polyfill() {
|
||||
/**
|
||||
* String.prototype.replaceAll() polyfill
|
||||
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||
* @author Chris Ferdinandi
|
||||
* @license MIT
|
||||
*/
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function (str, newStr) {
|
||||
// If a regex pattern
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||
return this.replace(str, newStr);
|
||||
}
|
||||
// If a string
|
||||
return this.replace(new RegExp(str, "g"), newStr);
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.polyfill = polyfill;
|
||||
class TimeLogger {
|
||||
constructor() {
|
||||
this.startTime = dayjs().valueOf();
|
||||
}
|
||||
print(name) {
|
||||
if (exports.isDev && process.env.TIMELOGGER === "1") {
|
||||
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.TimeLogger = TimeLogger;
|
||||
/**
|
||||
* Returns a random number between min (inclusive) and max (exclusive)
|
||||
*/
|
||||
function getRandomArbitrary(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
exports.getRandomArbitrary = getRandomArbitrary;
|
||||
/**
|
||||
* From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
|
||||
*
|
||||
* Returns a random integer between min (inclusive) and max (inclusive).
|
||||
* The value is no lower than min (or the next integer greater than min
|
||||
* if min isn't an integer) and no greater than max (or the next integer
|
||||
* lower than max if max isn't an integer).
|
||||
* Using Math.round() will give you a non-uniform distribution!
|
||||
*/
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
exports.getRandomInt = getRandomInt;
|
||||
function genSecret(length = 64) {
|
||||
let secret = "";
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charsLength = chars.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
exports.genSecret = genSecret;
|
||||
function getMonitorRelativeURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
}
|
||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
||||
"use strict";
|
||||
// Common Util for frontend and backend
|
||||
//
|
||||
// DOT NOT MODIFY util.js!
|
||||
// Need to run "tsc" to compile if there are any changes.
|
||||
//
|
||||
// Backend uses the compiled file util.js
|
||||
// Frontend uses util.ts
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getMonitorRelativeURL = exports.genSecret = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||
const _dayjs = require("dayjs");
|
||||
const dayjs = _dayjs;
|
||||
exports.isDev = process.env.NODE_ENV === "development";
|
||||
exports.appName = "Uptime Kuma";
|
||||
exports.DOWN = 0;
|
||||
exports.UP = 1;
|
||||
exports.PENDING = 2;
|
||||
exports.STATUS_PAGE_ALL_DOWN = 0;
|
||||
exports.STATUS_PAGE_ALL_UP = 1;
|
||||
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||
function flipStatus(s) {
|
||||
if (s === exports.UP) {
|
||||
return exports.DOWN;
|
||||
}
|
||||
if (s === exports.DOWN) {
|
||||
return exports.UP;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
exports.flipStatus = flipStatus;
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
exports.sleep = sleep;
|
||||
/**
|
||||
* PHP's ucfirst
|
||||
* @param str
|
||||
*/
|
||||
function ucfirst(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
const firstLetter = str.substr(0, 1);
|
||||
return firstLetter.toUpperCase() + str.substr(1);
|
||||
}
|
||||
exports.ucfirst = ucfirst;
|
||||
function debug(msg) {
|
||||
if (exports.isDev) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
exports.debug = debug;
|
||||
function polyfill() {
|
||||
/**
|
||||
* String.prototype.replaceAll() polyfill
|
||||
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||
* @author Chris Ferdinandi
|
||||
* @license MIT
|
||||
*/
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function (str, newStr) {
|
||||
// If a regex pattern
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||
return this.replace(str, newStr);
|
||||
}
|
||||
// If a string
|
||||
return this.replace(new RegExp(str, "g"), newStr);
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.polyfill = polyfill;
|
||||
class TimeLogger {
|
||||
constructor() {
|
||||
this.startTime = dayjs().valueOf();
|
||||
}
|
||||
print(name) {
|
||||
if (exports.isDev && process.env.TIMELOGGER === "1") {
|
||||
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.TimeLogger = TimeLogger;
|
||||
/**
|
||||
* Returns a random number between min (inclusive) and max (exclusive)
|
||||
*/
|
||||
function getRandomArbitrary(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
exports.getRandomArbitrary = getRandomArbitrary;
|
||||
/**
|
||||
* From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
|
||||
*
|
||||
* Returns a random integer between min (inclusive) and max (inclusive).
|
||||
* The value is no lower than min (or the next integer greater than min
|
||||
* if min isn't an integer) and no greater than max (or the next integer
|
||||
* lower than max if max isn't an integer).
|
||||
* Using Math.round() will give you a non-uniform distribution!
|
||||
*/
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
exports.getRandomInt = getRandomInt;
|
||||
function genSecret(length = 64) {
|
||||
let secret = "";
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charsLength = chars.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
exports.genSecret = genSecret;
|
||||
function getMonitorRelativeURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
}
|
||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
||||
|
|
|
@ -1,10 +1,37 @@
|
|||
const { genSecret } = require("../src/util");
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
});
|
||||
|
||||
describe("", () => {
|
||||
describe("Test genSecret", () => {
|
||||
|
||||
it("should ", () => {
|
||||
it("should be correct length", () => {
|
||||
let secret = genSecret(-1);
|
||||
expect(secret).toEqual("");
|
||||
|
||||
secret = genSecret(0);
|
||||
expect(secret).toEqual("");
|
||||
|
||||
secret = genSecret(1);
|
||||
expect(secret.length).toEqual(1);
|
||||
|
||||
secret = genSecret(2);
|
||||
expect(secret.length).toEqual(2);
|
||||
|
||||
secret = genSecret(64);
|
||||
expect(secret.length).toEqual(64);
|
||||
|
||||
secret = genSecret(9000);
|
||||
expect(secret.length).toEqual(9000);
|
||||
|
||||
secret = genSecret(90000);
|
||||
expect(secret.length).toEqual(90000);
|
||||
});
|
||||
|
||||
it("should contain first and last possible chars", () => {
|
||||
let secret = genSecret(90000);
|
||||
expect(secret).toContain("A");
|
||||
expect(secret).toContain("9");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"newLine": "LF",
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
|
|
Loading…
Add table
Reference in a new issue