Merge pull request #2 from louislam/master

Update
pull/3878/head
HdroguettA 1 year ago committed by GitHub
commit b57d52fa8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -30,7 +30,6 @@ SECURITY.md
tsconfig.json
.env
/tmp
/babel.config.js
/ecosystem.config.js
/extra/healthcheck.exe
/extra/healthcheck

@ -19,12 +19,13 @@ module.exports = {
],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@babel/eslint-parser",
parser: "@typescript-eslint/parser",
sourceType: "module",
requireConfigFile: false,
},
plugins: [
"jsdoc"
"jsdoc",
"@typescript-eslint",
],
rules: {
"yoda": "error",
@ -163,6 +164,22 @@ module.exports = {
context: true,
jestPuppeteer: true,
},
},
// Override for TypeScript
{
"files": [
"**/*.ts",
],
extends: [
"plugin:@typescript-eslint/recommended",
],
"rules": {
"jsdoc/require-returns-type": "off",
"jsdoc/require-param-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"prefer-const": "off",
}
}
]
};

@ -0,0 +1,43 @@
name: "CodeQL"
on:
push:
branches: [ "master", "1.23.X"]
pull_request:
branches: [ "master", "1.23.X"]
schedule:
- cron: '16 22 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
timeout-minutes: 360
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript-typescript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

@ -46,7 +46,7 @@ docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name upti
Uptime Kuma is now running on http://localhost:3001
> [!WARNING]
> **NFS** (Network File System) are **NOT** supported. Please map to a local directory or volume.
> File Systems like **NFS** (Network File System) are **NOT** supported. Please map to a local directory or volume.
### 💪🏻 Non-Docker

@ -1,7 +0,0 @@
const config = {};
if (process.env.TEST_FRONTEND) {
config.presets = [ "@babel/preset-env" ];
}
module.exports = config;

1954
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -21,6 +21,7 @@
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"start-server-dev:watch": "cross-env NODE_ENV=development node --watch server/server.js",
"build": "vite build --config ./config/vite.config.js",
"test": "node test/prepare-test-server.js && npm run test-backend",
"test-with-build": "npm run build && npm test",
@ -144,14 +145,14 @@
},
"devDependencies": {
"@actions/github": "~5.0.1",
"@babel/eslint-parser": "^7.22.7",
"@babel/preset-env": "^7.15.8",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@popperjs/core": "~2.10.2",
"@types/bootstrap": "~5.1.9",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@vitejs/plugin-vue": "~4.2.3",
"@vue/compiler-sfc": "~3.3.4",
"@vuepic/vue-datepicker": "~3.4.8",

@ -12,22 +12,40 @@ const mysql = require("mysql2/promise");
*/
class Database {
/**
* Boostrap database for SQLite
* @type {string}
*/
static templatePath = "./db/kuma.db";
/**
* Data Dir (Default: ./data)
* @type {string}
*/
static dataDir;
/**
* User Upload Dir (Default: ./data/upload)
* @type {string}
*/
static uploadDir;
/**
* Chrome Screenshot Dir (Default: ./data/screenshots)
* @type {string}
*/
static screenshotDir;
/**
* SQLite file path (Default: ./data/kuma.db)
* @type {string}
*/
static sqlitePath;
/**
* For storing Docker TLS certs (Default: ./data/docker-tls)
* @type {string}
*/
static dockerTLSDir;
/**
@ -84,8 +102,8 @@ class Database {
"patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true, // The last file so far converted to a knex migration file
"patch-notification-config.sql": true,
"patch-add-gamedig-given-port.sql": true,
"patch-notification-config.sql": true, // The last file so far converted to a knex migration file
};
/**
@ -131,7 +149,7 @@ class Database {
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`);
log.info("server", `Data Dir: ${Database.dataDir}`);
}
/**
@ -318,10 +336,10 @@ class Database {
await R.exec("PRAGMA synchronous = NORMAL");
if (!noLog) {
log.info("db", "SQLite config:");
log.info("db", await R.getAll("PRAGMA journal_mode"));
log.info("db", await R.getAll("PRAGMA cache_size"));
log.info("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
log.debug("db", "SQLite config:");
log.debug("db", await R.getAll("PRAGMA journal_mode"));
log.debug("db", await R.getAll("PRAGMA cache_size"));
log.debug("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
}
}
@ -390,13 +408,15 @@ class Database {
version = 0;
}
log.info("db", "Your database version: " + version);
log.info("db", "Latest database version: " + this.latestVersion);
if (version !== this.latestVersion) {
log.info("db", "Your database version: " + version);
log.info("db", "Latest database version: " + this.latestVersion);
}
if (version === this.latestVersion) {
log.info("db", "Database patch not needed");
log.debug("db", "Database patch not needed");
} else if (version > this.latestVersion) {
log.info("db", "Warning: Database version is newer than expected");
log.warn("db", "Warning: Database version is newer than expected");
} else {
log.info("db", "Database patch is needed");
@ -432,7 +452,7 @@ class Database {
* @returns {Promise<void>}
*/
static async patchSqlite2() {
log.info("db", "Database Patch 2.0 Process");
log.debug("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {

@ -72,7 +72,6 @@ class DockerHost {
url: "/containers/json?all=true",
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version
},
};

@ -447,7 +447,6 @@ class Monitor extends BeanModel {
timeout: this.timeout * 1000,
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version,
...(contentType ? { "Content-Type": contentType } : {}),
...(basicAuthHeader),
...(oauth2AuthHeader),
@ -627,7 +626,6 @@ class Monitor extends BeanModel {
timeout: this.timeout * 1000,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
@ -681,7 +679,6 @@ class Monitor extends BeanModel {
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)

@ -49,8 +49,6 @@ if (process.platform === "win32") {
];
}
log.debug("chrome", allowedList);
/**
* Is the executable path allowed?
* @param {string} executablePath Path to executable

@ -0,0 +1,61 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class GrafanaOncall extends NotificationProvider {
name = "GrafanaOncall";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
if (!notification.GrafanaOncallURL) {
throw new Error("GrafanaOncallURL cannot be empty");
}
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON === null) {
let grafanaupdata = {
title: "General notification",
message: msg,
state: "alerting",
};
await axios.post(
notification.GrafanaOncallURL,
grafanaupdata
);
return okMsg;
} else if (heartbeatJSON["status"] === DOWN) {
let grafanadowndata = {
title: monitorJSON["name"] + " is down",
message: heartbeatJSON["msg"],
state: "alerting",
};
await axios.post(
notification.GrafanaOncallURL,
grafanadowndata
);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
let grafanaupdata = {
title: monitorJSON["name"] + " is up",
message: heartbeatJSON["msg"],
state: "ok",
};
await axios.post(
notification.GrafanaOncallURL,
grafanaupdata
);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = GrafanaOncall;

@ -1,6 +1,7 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
const { DOWN } = require("../../src/util");
const { Liquid } = require("liquidjs");
class SMTP extends NotificationProvider {
@ -39,76 +40,86 @@ 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
// default values in case the user does not want to template
let subject = msg;
let body = msg;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
// subject and body are templated
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
let customSubject = "";
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
// 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
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
const engine = new Liquid();
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" || monitorJSON["type"] === "json-query") {
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;
const tpl = engine.parse(customSubject);
subject = await engine.render(tpl, context);
}
if (customBody !== "") {
const tpl = engine.parse(customBody);
body = await engine.render(tpl, context);
}
}
let transporter = nodemailer.createTransport(config);
let bodyTextContent = msg;
if (heartbeatJSON) {
bodyTextContent = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
// send mail with defined transport object
let transporter = nodemailer.createTransport(config);
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
text: bodyTextContent,
text: body,
});
return "Sent Successfully.";
}
/**
* Generate context for LiquidJS
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
*/
generateContext(msg, monitorJSON, heartbeatJSON) {
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
return {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
}
}
module.exports = SMTP;

@ -14,6 +14,7 @@ const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify");
const GrafanaOncall = require("./notification-providers/grafana-oncall");
const HomeAssistant = require("./notification-providers/home-assistant");
const Kook = require("./notification-providers/kook");
const Line = require("./notification-providers/line");
@ -65,7 +66,7 @@ class Notification {
* @throws Duplicate notification providers in list
*/
static init() {
log.info("notification", "Prepare Notification Providers");
log.debug("notification", "Prepare Notification Providers");
this.providerList = {};
@ -84,6 +85,7 @@ class Notification {
new GoogleChat(),
new Gorush(),
new Gotify(),
new GrafanaOncall(),
new HomeAssistant(),
new Kook(),
new Line(),

@ -39,7 +39,6 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
log.info("server", "Welcome to Uptime Kuma");
log.debug("server", "Arguments");
log.debug("server", args);
@ -47,8 +46,13 @@ if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
log.info("server", "Env: " + process.env.NODE_ENV);
log.debug("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
const checkVersion = require("./check-version");
log.info("server", "Uptime Kuma Version: " + checkVersion.version);
log.info("server", "Loading modules");
log.debug("server", "Importing express");
const express = require("express");
@ -61,8 +65,6 @@ log.debug("server", "Importing http-graceful-shutdown");
const gracefulShutdown = require("http-graceful-shutdown");
log.debug("server", "Importing prometheus-api-metrics");
const prometheusAPIMetrics = require("prometheus-api-metrics");
log.debug("server", "Importing compare-versions");
const compareVersions = require("compare-versions");
const { passwordStrength } = require("check-password-strength");
log.debug("server", "Importing 2FA Modules");
@ -75,7 +77,6 @@ const server = UptimeKumaServer.getInstance(args);
const io = module.exports.io = server.io;
const app = server.app;
log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor");
const Monitor = require("./model/monitor");
const User = require("./model/user");
@ -88,9 +89,6 @@ log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
Notification.init();
log.debug("server", "Importing Proxy");
const { Proxy } = require("./proxy");
log.debug("server", "Importing Database");
const Database = require("./database");
@ -102,9 +100,6 @@ const { apiAuth } = require("./auth");
const { login } = require("./auth");
const passwordHash = require("./password-hash");
const checkVersion = require("./check-version");
log.info("server", "Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
@ -195,7 +190,7 @@ let needSetup = false;
server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList();
log.info("server", "Adding route");
log.debug("server", "Adding route");
// ***************************
// Normal Router here
@ -295,7 +290,7 @@ let needSetup = false;
}
});
log.info("server", "Adding socket handler");
log.debug("server", "Adding socket handler");
io.on("connection", async (socket) => {
sendInfo(socket, true);
@ -323,12 +318,12 @@ let needSetup = false;
decoded.username,
]);
// Check if the password changed
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
throw new Error("The token is invalid due to password change or old token");
}
if (user) {
// Check if the password changed
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
throw new Error("The token is invalid due to password change or old token");
}
log.debug("auth", "afterLogin");
afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
@ -1434,212 +1429,6 @@ let needSetup = false;
}
});
socket.on("uploadBackup", async (uploadedJSON, importHandle, callback) => {
try {
checkLogin(socket);
let backupData = JSON.parse(uploadedJSON);
log.info("manage", `Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
let notificationListData = backupData.notificationList;
let proxyListData = backupData.proxyList;
let monitorListData = backupData.monitorList;
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
if (importHandle === "overwrite") {
// Stops every monitor first, so it doesn't execute any heartbeat while importing
for (let id in server.monitorList) {
let monitor = server.monitorList[id];
await monitor.stop();
}
await R.exec("DELETE FROM heartbeat");
await R.exec("DELETE FROM monitor_notification");
await R.exec("DELETE FROM monitor_tls_info");
await R.exec("DELETE FROM notification");
await R.exec("DELETE FROM monitor_tag");
await R.exec("DELETE FROM tag");
await R.exec("DELETE FROM monitor");
await R.exec("DELETE FROM proxy");
}
// Only starts importing if the backup file contains at least one notification
if (notificationListData.length >= 1) {
// Get every existing notification name and puts them in one simple string
let notificationNameList = await R.getAll("SELECT name FROM notification");
let notificationNameListString = JSON.stringify(notificationNameList);
for (let i = 0; i < notificationListData.length; i++) {
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
let notification = JSON.parse(notificationListData[i].config);
await Notification.save(notification, null, socket.userID);
}
}
}
// Only starts importing if the backup file contains at least one proxy
if (proxyListData && proxyListData.length >= 1) {
const proxies = await R.findAll("proxy");
// Loop over proxy list and save proxies
for (const proxy of proxyListData) {
const exists = proxies.find(item => item.id === proxy.id);
// Do not process when proxy already exists in import handle is skip and keep
if ([ "skip", "keep" ].includes(importHandle) && !exists) {
return;
}
// Save proxy as new entry if exists update exists one
await Proxy.save(proxy, exists ? proxy.id : undefined, proxy.userId);
}
}
// Only starts importing if the backup file contains at least one monitor
if (monitorListData.length >= 1) {
// Get every existing monitor name and puts them in one simple string
let monitorNameList = await R.getAll("SELECT name FROM monitor");
let monitorNameListString = JSON.stringify(monitorNameList);
for (let i = 0; i < monitorListData.length; i++) {
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
// --- Start ---
// Define default values
let retryInterval = 0;
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
/*
Only replace the default value with the backup file data for the specific version, where it appears the first time
More information about that where "let version" will be defined
*/
if (version17x) {
retryInterval = monitorListData[i].retryInterval;
}
// --- End ---
let monitor = {
// Define the new variable from earlier here
name: monitorListData[i].name,
description: monitorListData[i].description,
type: monitorListData[i].type,
url: monitorListData[i].url,
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,
timeout,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
resendInterval: monitorListData[i].resendInterval || 0,
hostname: monitorListData[i].hostname,
maxretries: monitorListData[i].maxretries,
port: monitorListData[i].port,
keyword: monitorListData[i].keyword,
invertKeyword: monitorListData[i].invertKeyword,
ignoreTls: monitorListData[i].ignoreTls,
upsideDown: monitorListData[i].upsideDown,
maxredirects: monitorListData[i].maxredirects,
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
dns_resolve_type: monitorListData[i].dns_resolve_type,
dns_resolve_server: monitorListData[i].dns_resolve_server,
notificationIDList: monitorListData[i].notificationIDList,
proxy_id: monitorListData[i].proxy_id || null,
};
if (monitorListData[i].pushToken) {
monitor.pushToken = monitorListData[i].pushToken;
}
let bean = R.dispense("monitor");
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor);
bean.user_id = socket.userID;
await R.store(bean);
// Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented
if (version17x) {
// Only import if the specific monitor has tags assigned
for (const oldTag of monitorListData[i].tags) {
// Check if tag already exists and get data ->
let tag = await R.findOne("tag", " name = ?", [
oldTag.name,
]);
let tagId;
if (!tag) {
// -> If it doesn't exist, create new tag from backup file
let beanTag = R.dispense("tag");
beanTag.name = oldTag.name;
beanTag.color = oldTag.color;
await R.store(beanTag);
tagId = beanTag.id;
} else {
// -> If it already exist, set tagId to value from database
tagId = tag.id;
}
// Assign the new created tag to the monitor
await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [
tagId,
bean.id,
oldTag.value,
]);
}
}
await updateMonitorNotification(bean.id, notificationIDList);
// If monitor was active start it immediately, otherwise pause it
if (monitorListData[i].active === 1) {
await startMonitor(socket.userID, bean.id);
} else {
await pauseMonitor(socket.userID, bean.id);
}
}
}
await sendNotificationList(socket);
await server.sendMonitorList(socket);
}
callback({
ok: true,
msg: "successBackupRestored",
msgi18n: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearEvents", async (monitorID, callback) => {
try {
checkLogin(socket);
@ -1735,11 +1524,12 @@ let needSetup = false;
});
log.info("server", "Init the server");
log.debug("server", "Init the server");
server.httpServer.once("error", async (err) => {
console.error("Cannot listen: " + err.message);
log.error("server", "Cannot listen: " + err.message);
await shutdownFunction();
process.exit(1);
});
server.start();
@ -1851,9 +1641,9 @@ async function afterLogin(socket, user) {
* @returns {Promise<void>}
*/
async function initDatabase(testMode = false) {
log.info("server", "Connecting to the Database");
log.debug("server", "Connecting to the database");
await Database.connect(testMode);
log.info("server", "Connected");
log.info("server", "Connected to the database");
// Patch the database
await Database.patch();
@ -1867,7 +1657,7 @@ async function initDatabase(testMode = false) {
jwtSecretBean = await initJWTSecret();
log.info("server", "Stored JWT secret into database");
} else {
log.info("server", "Load JWT secret from database.");
log.debug("server", "Load JWT secret from database.");
}
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup

@ -48,7 +48,7 @@ class SetupDatabase {
try {
dbConfig = Database.readDBConfig();
log.info("setup-database", "db-config.json is found and is valid");
log.debug("setup-database", "db-config.json is found and is valid");
this.needSetup = false;
} catch (e) {

@ -12,6 +12,7 @@ const { Settings } = require("./settings");
const dayjs = require("dayjs");
const childProcess = require("child_process");
const path = require("path");
const axios = require("axios");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
@ -83,7 +84,10 @@ class UptimeKumaServer {
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
log.info("server", "Creating express and socket.io instance");
// Set axios default user-agent to Uptime-Kuma/version
axios.defaults.headers.common["User-Agent"] = this.getUserAgent();
log.debug("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
@ -411,6 +415,14 @@ class UptimeKumaServer {
}
}
}
/**
* Default User-Agent when making HTTP requests
* @returns {string} User-Agent
*/
getUserAgent() {
return "Uptime-Kuma/" + require("../package.json").version;
}
}
module.exports = {

@ -92,11 +92,9 @@
<script lang="ts">
import { Modal } from "bootstrap";
import { useToast } from "vue-toastification";
import dayjs from "dayjs";
import Datepicker from "@vuepic/vue-datepicker";
import CopyableInput from "./CopyableInput.vue";
const toast = useToast();
export default {
components: {
@ -158,7 +156,7 @@ export default {
this.keymodal.show();
this.clearForm();
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},

@ -62,8 +62,6 @@
<script lang="ts">
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@ -120,7 +118,7 @@ export default {
}
if (!found) {
toast.error("Docker Host not found!");
this.$root.toastError("Docker Host not found!");
}
} else {

@ -35,7 +35,7 @@
</button>
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
{{ res.msg }}
{{ $t(res.msg) }}
</div>
</form>
</div>

@ -119,6 +119,7 @@ export default {
"GoogleChat": "Google Chat (Google Workspace)",
"gorush": "Gorush",
"gotify": "Gotify",
"GrafanaOncall": "Grafana Oncall",
"HomeAssistant": "Home Assistant",
"Kook": "Kook",
"line": "LINE Messenger",

@ -21,11 +21,8 @@ import { BarController, BarElement, Chart, Filler, LinearScale, LineController,
import "chartjs-adapter-dayjs-4";
import dayjs from "dayjs";
import { Line } from "vue-chartjs";
import { useToast } from "vue-toastification";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
const toast = useToast();
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
export default {
@ -231,7 +228,7 @@ export default {
this.$root.getMonitorBeats(this.monitorId, newPeriod, (res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
} else {
this.heartbeatList = res.data;
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;

@ -123,9 +123,7 @@ import Confirm from "./Confirm.vue";
import Tag from "./Tag.vue";
import VueMultiselect from "vue-multiselect";
import { colorOptions } from "../util-frontend";
import { useToast } from "vue-toastification";
import { getMonitorRelativeURL } from "../util.ts";
const toast = useToast();
export default {
components: {
@ -320,7 +318,7 @@ export default {
for (let addId of this.addingMonitor) {
await this.addMonitorTagAsync(this.tag.id, addId, "").then((res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
editResult = false;
}
});
@ -330,7 +328,7 @@ export default {
this.monitors.find(monitor => monitor.id === removeId)?.tags.forEach(async (monitorTag) => {
await this.deleteMonitorTagAsync(this.tag.id, removeId, monitorTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
editResult = false;
}
});

@ -129,10 +129,8 @@
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import { colorOptions } from "../util-frontend";
import Tag from "../components/Tag.vue";
const toast = useToast();
/**
* @typedef Tag
@ -262,7 +260,7 @@ export default {
if (res.ok) {
this.existingTags = res.tags;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@ -399,7 +397,7 @@ export default {
let newTagResult;
await this.addTagAsync(newTag).then((res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
newTagResult = false;
}
newTagResult = res.tag;
@ -424,7 +422,7 @@ export default {
// Assign tag to monitor
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
newMonitorTagResult = false;
}
newMonitorTagResult = true;
@ -440,7 +438,7 @@ export default {
let deleteMonitorTagResult;
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
deleteMonitorTagResult = false;
}
deleteMonitorTagResult = true;

@ -76,8 +76,6 @@
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import VueQrcode from "vue-qrcode";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@ -138,7 +136,7 @@ export default {
if (res.ok) {
this.uri = res.uri;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@ -159,7 +157,7 @@ export default {
this.currentPassword = "";
this.modal.hide();
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@ -180,7 +178,7 @@ export default {
this.currentPassword = "";
this.modal.hide();
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@ -194,7 +192,7 @@ export default {
if (res.ok) {
this.tokenValid = res.valid;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@ -208,7 +206,7 @@ export default {
if (res.ok) {
this.twoFAStatus = res.status;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},

@ -0,0 +1,7 @@
<template>
<div class="mb-3">
<label for="GrafanaOncallURL" class="form-label">{{ $t("GrafanaOncallURL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="GrafanaOncallURL" v-model="$parent.notification.GrafanaOncallURL" type="text" class="form-control" required>
</div>
</template>

@ -59,6 +59,28 @@
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
</div>
<p class="form-text">
<i18n-t tag="div" keypath="smtpLiquidIntroduction" class="form-text mb-3">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<code v-pre>{{name}}</code>: {{ $t("emailTemplateServiceName") }}<br />
<code v-pre>{{msg}}</code>: {{ $t("emailTemplateMsg") }}<br />
<code v-pre>{{status}}</code>: {{ $t("emailTemplateStatus") }}<br />
<code v-pre>{{heartbeatJSON}}</code>: {{ $t("emailTemplateHeartbeatJSON") }}<b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br />
<code v-pre>{{monitorJSON}}</code>: {{ $t("emailTemplateMonitorJSON") }} <b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br />
<code v-pre>{{hostnameOrURL}}</code>: {{ $t("emailTemplateHostnameOrURL") }}<br />
</p>
<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 class="form-text">{{ $t("leave blank for default subject") }}</div>
</div>
<div class="mb-3">
<label for="body-email" class="form-label">{{ $t("emailCustomBody") }}</label>
<textarea id="body-email" v-model="$parent.notification.customBody" type="text" class="form-control" autocomplete="false" placeholder=""></textarea>
<div class="form-text">{{ $t("leave blank for default body") }}</div>
</div>
<ToggleSection :heading="$t('smtpDkimSettings')">
<i18n-t tag="div" keypath="smtpDkimDesc" class="form-text mb-3">
<a href="https://nodemailer.com/dkim/" target="_blank">{{ $t("documentation") }}</a>
@ -89,17 +111,6 @@
<input id="dkim-skip-fields" v-model="$parent.notification.smtpDkimskipFields" type="text" class="form-control" autocomplete="false" placeholder="message-id:date">
</div>
</ToggleSection>
<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 />
{{STATUS}}: Status<br />
</div>
</div>
</div>
</template>

@ -58,8 +58,6 @@
<script>
import HiddenInput from "../HiddenInput.vue";
import axios from "axios";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@ -110,7 +108,7 @@ export default {
}
} catch (error) {
toast.error(error.message);
this.$root.toastError(error.message);
}
},

@ -12,9 +12,7 @@
</div>
<div class="mb-3">
<label for="webhook-request-body" class="form-label">{{
$t("Request Body")
}}</label>
<label for="webhook-request-body" class="form-label">{{ $t("Request Body") }}</label>
<select
id="webhook-request-body"
v-model="$parent.notification.webhookContentType"
@ -26,40 +24,29 @@
<option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
</select>
<div class="form-text">
<div v-if="$parent.notification.webhookContentType == 'json'">
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
</div>
<div v-if="$parent.notification.webhookContentType == 'form-data'">
<i18n-t tag="p" keypath="webhookFormDataDesc">
<template #multipart>multipart/form-data"</template>
<template #decodeFunction>
<strong>json_decode($_POST['data'])</strong>
</template>
</i18n-t>
</div>
<div v-if="$parent.notification.webhookContentType == 'custom'">
<i18n-t tag="p" keypath="webhookCustomBodyDesc">
<template #msg>
<code>msg</code>
</template>
<template #heartbeat>
<code>heartbeatJSON</code>
</template>
<template #monitor>
<code>monitorJSON</code>
</template>
</i18n-t>
</div>
</div>
<div v-if="$parent.notification.webhookContentType == 'json'" class="form-text">{{ $t("webhookJsonDesc", ['"application/json"']) }}</div>
<i18n-t v-else-if="$parent.notification.webhookContentType == 'form-data'" tag="div" keypath="webhookFormDataDesc" class="form-text">
<template #multipart>multipart/form-data"</template>
<template #decodeFunction>
<strong>json_decode($_POST['data'])</strong>
</template>
</i18n-t>
<template v-else-if="$parent.notification.webhookContentType == 'custom'">
<i18n-t tag="div" keypath="liquidIntroduction" class="form-text">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<code v-pre>{{msg}}</code>: {{ $t("templateMsg") }}<br />
<code v-pre>{{heartbeatJSON}}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br />
<code v-pre>{{monitorJSON}}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br />
<textarea
v-if="$parent.notification.webhookContentType == 'custom'"
id="customBody"
v-model="$parent.notification.webhookCustomBody"
class="form-control"
:placeholder="customBodyPlaceholder"
></textarea>
<textarea
id="customBody"
v-model="$parent.notification.webhookCustomBody"
class="form-control"
:placeholder="customBodyPlaceholder"
required
></textarea>
</template>
</div>
<div class="mb-3">
@ -67,15 +54,14 @@
<input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
</div>
<div class="form-text">
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
</div>
<div class="form-text">{{ $t("webhookAdditionalHeadersDesc") }}</div>
<textarea
v-if="showAdditionalHeadersField"
id="additionalHeaders"
v-model="$parent.notification.webhookAdditionalHeaders"
class="form-control"
:placeholder="headersPlaceholder"
:required="showAdditionalHeadersField"
></textarea>
</div>
</template>
@ -90,18 +76,18 @@ export default {
computed: {
headersPlaceholder() {
return this.$t("Example:", [
`
{
`{
"Authorization": "Authorization Token"
}`,
]);
},
customBodyPlaceholder() {
return `Example:
{
"Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
return this.$t("Example:", [
`{
"Title": "Uptime Kuma Alert{% if monitorJSON %} - {{ monitorJSON['name'] }}{% endif %}",
"Body": "{{ msg }}"
}`;
}`
]);
}
},
};

@ -12,6 +12,7 @@ import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue";
import GrafanaOncall from "./GrafanaOncall.vue";
import HomeAssistant from "./HomeAssistant.vue";
import Kook from "./Kook.vue";
import Line from "./Line.vue";
@ -71,6 +72,7 @@ const NotificationFormList = {
"GoogleChat": GoogleChat,
"gorush": Gorush,
"gotify": Gotify,
"GrafanaOncall": GrafanaOncall,
"HomeAssistant": HomeAssistant,
"Kook": Kook,
"line": Line,

@ -72,8 +72,6 @@
<script>
import APIKeyDialog from "../../components/APIKeyDialog.vue";
import Confirm from "../Confirm.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@ -109,11 +107,7 @@ export default {
*/
deleteKey() {
this.$root.deleteAPIKey(this.selectedKeyID, (res) => {
if (res.ok) {
toast.success(res.msg);
} else {
toast.error(res.msg);
}
this.$root.toastRes(res);
});
},

@ -1,232 +0,0 @@
<template>
<div>
<div class="my-4">
<div class="alert alert-warning" role="alert" style="border-radius: 15px;">
{{ $t("backupOutdatedWarning") }}<br />
<br />
{{ $t("backupRecommend") }}
</div>
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
<p>
{{ $t("backupDescription") }} <br />
({{ $t("backupDescription2") }}) <br />
</p>
<div class="mb-2">
<button class="btn btn-primary" @click="downloadBackup">
{{ $t("Export") }}
</button>
</div>
<p>
<strong>{{ $t("backupDescription3") }}</strong>
</p>
</div>
<div class="my-4">
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
<label class="form-label">{{ $t("Options") }}:</label>
<br />
<div class="form-check form-check-inline">
<input
id="radioKeep"
v-model="importHandle"
class="form-check-input"
type="radio"
name="radioImportHandle"
value="keep"
/>
<label class="form-check-label" for="radioKeep">
{{ $t("Keep both") }}
</label>
</div>
<div class="form-check form-check-inline">
<input
id="radioSkip"
v-model="importHandle"
class="form-check-input"
type="radio"
name="radioImportHandle"
value="skip"
/>
<label class="form-check-label" for="radioSkip">
{{ $t("Skip existing") }}
</label>
</div>
<div class="form-check form-check-inline">
<input
id="radioOverwrite"
v-model="importHandle"
class="form-check-input"
type="radio"
name="radioImportHandle"
value="overwrite"
/>
<label class="form-check-label" for="radioOverwrite">
{{ $t("Overwrite") }}
</label>
</div>
<div class="form-text mb-2">
{{ $t("importHandleDescription") }}
</div>
<div class="mb-2">
<input
id="import-backend"
type="file"
class="form-control"
accept="application/json"
/>
</div>
<div class="input-group mb-2 justify-content-end">
<button
type="button"
class="btn btn-outline-primary"
:disabled="processing"
@click="confirmImport"
>
<div
v-if="processing"
class="spinner-border spinner-border-sm me-1"
></div>
{{ $t("Import") }}
</button>
</div>
<div
v-if="importAlert"
class="alert alert-danger mt-3"
style="padding: 6px 16px;"
>
{{ importAlert }}
</div>
</div>
<Confirm
ref="confirmImport"
btn-style="btn-danger"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="importBackup"
>
{{ $t("confirmImportMsg") }}
</Confirm>
</div>
</template>
<script>
import Confirm from "../../components/Confirm.vue";
import dayjs from "dayjs";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
Confirm,
},
data() {
return {
processing: false,
importHandle: "skip",
importAlert: null,
};
},
methods: {
/**
* Show the confimation dialog confirming the configuration
* be imported
* @returns {void}
*/
confirmImport() {
this.$refs.confirmImport.show();
},
/**
* Download a backup of the configuration
* @returns {void}
*/
downloadBackup() {
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
let fileName = `Uptime_Kuma_Backup_${time}.json`;
let monitorList = Object.values(this.$root.monitorList);
let exportData = {
version: this.$root.info.version,
notificationList: this.$root.notificationList,
monitorList: monitorList,
};
exportData = JSON.stringify(exportData, null, 4);
let downloadItem = document.createElement("a");
downloadItem.setAttribute(
"href",
"data:application/json;charset=utf-8," +
encodeURIComponent(exportData)
);
downloadItem.setAttribute("download", fileName);
downloadItem.click();
},
/**
* Import the specified backup file
* @returns {string|void} Error message
*/
importBackup() {
this.processing = true;
let uploadItem = document.getElementById("import-backend").files;
if (uploadItem.length <= 0) {
this.processing = false;
return (this.importAlert = this.$t("alertNoFile"));
}
if (uploadItem.item(0).type !== "application/json") {
this.processing = false;
return (this.importAlert = this.$t("alertWrongFileType"));
}
let fileReader = new FileReader();
fileReader.readAsText(uploadItem.item(0));
fileReader.onload = (item) => {
this.$root.uploadBackup(
item.target.result,
this.importHandle,
(res) => {
this.processing = false;
if (res.ok) {
toast.success(res.msg);
} else {
toast.error(res.msg);
}
}
);
};
},
},
};
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.dark {
#import-backend {
&::file-selector-button {
color: $primary;
background-color: $dark-bg;
}
&:hover:not(:disabled):not([readonly])::file-selector-button {
color: $dark-font-color2;
background-color: $primary;
}
}
}
</style>

@ -57,9 +57,6 @@
<script>
import Confirm from "../../components/Confirm.vue";
import { log } from "../../util.ts";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@ -118,7 +115,7 @@ export default {
this.$root.getSocket().emit("shrinkDatabase", (res) => {
if (res.ok) {
this.loadDatabaseSize();
toast.success("Done");
this.$root.toastSuccess("Done");
} else {
log.debug("monitorhistory", res);
}
@ -142,7 +139,7 @@ export default {
if (res.ok) {
this.$router.go();
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},

@ -28,11 +28,9 @@
</template>
<script>
import { useToast } from "vue-toastification";
import TagEditDialog from "../../components/TagEditDialog.vue";
import Tag from "../Tag.vue";
import Confirm from "../Confirm.vue";
const toast = useToast();
export default {
components: {
@ -86,7 +84,7 @@ export default {
if (res.ok) {
this.tagsList = res.tags;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},

@ -213,7 +213,12 @@
"Content Type": "Content Type",
"webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js",
"webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
"webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.",
"liquidIntroduction": "Templatability is achieved via the Liquid templating language. Please refer to the {0} for usage instructions. These are the available variables:",
"templateMsg": "message of the notification",
"templateHeartbeatJSON": "object describing the heartbeat",
"templateMonitorJSON": "object describing the monitor",
"templateLimitedToUpDownCertNotifications": "only available for UP/DOWN/Certificate expiry notifications",
"templateLimitedToUpDownNotifications": "only available for UP/DOWN notifications",
"webhookAdditionalHeadersTitle": "Additional Headers",
"webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
"webhookBodyPresetOption": "Preset - {0}",
@ -489,7 +494,19 @@
"secureOptionTLS": "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error",
"From Email": "From Email",
"emailCustomisableContent": "Customisable content",
"smtpLiquidIntroduction": "The following two fields are templatable via the Liquid templating Language. Please refer to the {0} for usage instructions. These are the available variables:",
"emailCustomSubject": "Custom Subject",
"leave blank for default subject": "leave blank for default subject",
"emailCustomBody": "Custom Body",
"leave blank for default body": "leave blank for default body",
"emailTemplateServiceName": "Service Name",
"emailTemplateHostnameOrURL": "Hostname or URL",
"emailTemplateStatus": "Status",
"emailTemplateMonitorJSON": "object describing the monitor",
"emailTemplateHeartbeatJSON": "object describing the heartbeat",
"emailTemplateMsg": "message of the notification",
"emailTemplateLimitedToUpDownNotification": "only available for UP/DOWN heartbeats, otherwise null",
"To Email": "To Email",
"smtpCC": "CC",
"smtpBCC": "BCC",
@ -839,5 +856,6 @@
"successDisabled": "Disabled Successfully.",
"successEnabled": "Enabled Successfully.",
"tagNotFound": "Tag not found.",
"foundChromiumVersion": "Found Chromium/Chrome. Version: {0}"
"foundChromiumVersion": "Found Chromium/Chrome. Version: {0}",
"GrafanaOncallUrl": "Grafana Oncall URL"
}

@ -370,12 +370,16 @@ main {
padding: 9px 15px;
width: 48px;
box-shadow: 2px 2px 30px rgba(0, 0, 0, 0.2);
z-index: 100;
.dark & {
box-shadow: 2px 2px 30px rgba(0, 0, 0, 0.5);
}
}
@media (max-width: 770px) {
.clear-all-toast-btn {
bottom: 72px;
z-index: 100;
}
}

@ -342,7 +342,7 @@ export default {
* @returns {void}
*/
toastSuccess(msg) {
toast.success(msg);
toast.success(this.$t(msg));
},
/**
@ -351,7 +351,7 @@ export default {
* @returns {void}
*/
toastError(msg) {
toast.error(msg);
toast.error(this.$t(msg));
},
/**

@ -66,7 +66,7 @@ export default {
} else {
if (res.msg.includes("UNIQUE constraint")) {
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
this.$root.toastError("The slug is already taken. Please choose another slug.");
} else {
this.$root.toastRes(res);
}

@ -437,7 +437,7 @@ export default {
*/
testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id);
toast.success("Test notification is requested.");
this.$root.toastSuccess("Test notification is requested.");
},
/**
@ -498,11 +498,9 @@ export default {
*/
deleteMonitor() {
this.$root.deleteMonitor(this.monitor.id, (res) => {
this.$root.toastRes(res);
if (res.ok) {
toast.success(res.msg);
this.$router.push("/dashboard");
} else {
toast.error(res.msg);
}
});
},

@ -246,14 +246,11 @@
</template>
<script>
import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect";
import Datepicker from "@vuepic/vue-datepicker";
import { timezoneList } from "../util-frontend";
import cronstrue from "cronstrue/i18n";
const toast = useToast();
export default {
components: {
VueMultiselect,
@ -457,7 +454,7 @@ export default {
this.affectedMonitors.push(this.affectedMonitorsOptions.find(item => item.id === monitor.id));
});
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
@ -472,11 +469,11 @@ export default {
this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@ -490,7 +487,7 @@ export default {
this.processing = true;
if (this.affectedMonitors.length === 0) {
toast.error(this.$t("atLeastOneMonitor"));
this.$root.toastError(this.$t("atLeastOneMonitor"));
return this.processing = false;
}
@ -499,14 +496,14 @@ export default {
if (res.ok) {
await this.addMonitorMaintenance(res.maintenanceID, async () => {
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
toast.success(res.msg);
this.$root.toastRes(res);
this.processing = false;
this.$root.getMaintenanceList();
this.$router.push("/maintenance");
});
});
} else {
toast.error(res.msg);
this.$root.toastRes(res);
this.processing = false;
}
@ -524,7 +521,7 @@ export default {
});
} else {
this.processing = false;
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@ -539,7 +536,7 @@ export default {
async addMonitorMaintenance(maintenanceID, callback) {
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
} else {
this.$root.getMonitorList();
}
@ -557,7 +554,7 @@ export default {
async addMaintenanceStatusPage(maintenanceID, callback) {
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
} else {
this.$root.getMaintenanceList();
}

@ -848,7 +848,7 @@ import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../u
import { hostNameRegexPattern } from "../util-frontend";
import { sleep } from "../util";
const toast = useToast();
const toast = useToast;
const pushTokenLength = 32;
@ -1173,7 +1173,7 @@ message HealthCheckResponse {
if (res.ok) {
this.gameList = res.gameList;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@ -1318,7 +1318,7 @@ message HealthCheckResponse {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@ -1412,7 +1412,7 @@ message HealthCheckResponse {
createdNewParent = true;
this.monitor.parent = res.monitorID;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
this.processing = false;
return;
}
@ -1428,17 +1428,14 @@ message HealthCheckResponse {
if (createdNewParent) {
this.startParentGroupMonitor();
}
toast.success(res.msg);
this.processing = false;
this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID);
} else {
toast.error(res.msg);
this.processing = false;
}
this.$root.toastRes(res);
});
} else {
await this.$refs.tagsManager.submit(this.monitor.id);

@ -101,12 +101,8 @@ export default {
*/
deleteMaintenance() {
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
if (res.ok) {
toast.success(res.msg);
this.$router.push("/maintenance");
} else {
toast.error(res.msg);
}
this.$root.toastRes(res);
this.$router.push("/maintenance");
});
},
},

@ -81,8 +81,6 @@ import { getResBaseURL } from "../util-frontend";
import { getMaintenanceRelativeURL } from "../util.ts";
import Confirm from "../components/Confirm.vue";
import MaintenanceTime from "../components/MaintenanceTime.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@ -159,11 +157,9 @@ export default {
*/
deleteMaintenance() {
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
this.$root.toastRes(res);
if (res.ok) {
toast.success(res.msg);
this.$router.push("/maintenance");
} else {
toast.error(res.msg);
}
});
},

@ -113,9 +113,6 @@ export default {
proxies: {
title: this.$t("Proxies"),
},
backup: {
title: this.$t("Backup"),
},
about: {
title: this.$t("About"),
},

@ -46,9 +46,6 @@
</template>
<script>
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
data() {
return {
@ -79,7 +76,7 @@ export default {
this.processing = true;
if (this.password !== this.repeatPassword) {
toast.error(this.$t("PasswordsDoNotMatch"));
this.$root.toastError("PasswordsDoNotMatch");
this.processing = false;
return;
}

@ -179,7 +179,7 @@ export default {
},
test() {
toast.error("not implemented");
this.$root.toastError("not implemented");
}
},
};

@ -614,7 +614,7 @@ export default {
}
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@ -869,7 +869,7 @@ export default {
this.enableEditMode = false;
location.href = "/manage-status-page";
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@ -959,7 +959,7 @@ export default {
*/
postIncident() {
if (this.incident.title === "" || this.incident.content === "") {
toast.error(this.$t("Please input title and content"));
this.$root.toastError("Please input title and content");
return;
}
@ -969,7 +969,7 @@ export default {
this.enableEditIncidentMode = false;
this.incident = res.incident;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});

@ -30,7 +30,6 @@ import Tags from "./components/settings/Tags.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 = [
@ -126,10 +125,6 @@ const routes = [
path: "proxies",
component: Proxies,
},
{
path: "backup",
component: Backup,
},
{
path: "about",
component: About,

@ -1,4 +1,5 @@
"use strict";
/*!
// Common Util for frontend and backend
//
// DOT NOT MODIFY util.js!
@ -6,8 +7,10 @@
//
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = void 0;
const dayjs = require("dayjs");
exports.isDev = process.env.NODE_ENV === "development";
exports.appName = "Uptime Kuma";
@ -22,9 +25,57 @@ exports.STATUS_PAGE_MAINTENANCE = 3;
exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600; // 24 days
exports.MIN_INTERVAL_SECOND = 20; // 20 seconds
/** Flip the status of s */
exports.MAX_INTERVAL_SECOND = 2073600;
exports.MIN_INTERVAL_SECOND = 20;
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
exports.CONSOLE_STYLE_Underscore = "\x1b[4m";
exports.CONSOLE_STYLE_Blink = "\x1b[5m";
exports.CONSOLE_STYLE_Reverse = "\x1b[7m";
exports.CONSOLE_STYLE_Hidden = "\x1b[8m";
exports.CONSOLE_STYLE_FgBlack = "\x1b[30m";
exports.CONSOLE_STYLE_FgRed = "\x1b[31m";
exports.CONSOLE_STYLE_FgGreen = "\x1b[32m";
exports.CONSOLE_STYLE_FgYellow = "\x1b[33m";
exports.CONSOLE_STYLE_FgBlue = "\x1b[34m";
exports.CONSOLE_STYLE_FgMagenta = "\x1b[35m";
exports.CONSOLE_STYLE_FgCyan = "\x1b[36m";
exports.CONSOLE_STYLE_FgWhite = "\x1b[37m";
exports.CONSOLE_STYLE_FgGray = "\x1b[90m";
exports.CONSOLE_STYLE_FgOrange = "\x1b[38;5;208m";
exports.CONSOLE_STYLE_FgLightGreen = "\x1b[38;5;119m";
exports.CONSOLE_STYLE_FgLightBlue = "\x1b[38;5;117m";
exports.CONSOLE_STYLE_FgViolet = "\x1b[38;5;141m";
exports.CONSOLE_STYLE_FgBrown = "\x1b[38;5;130m";
exports.CONSOLE_STYLE_FgPink = "\x1b[38;5;219m";
exports.CONSOLE_STYLE_BgBlack = "\x1b[40m";
exports.CONSOLE_STYLE_BgRed = "\x1b[41m";
exports.CONSOLE_STYLE_BgGreen = "\x1b[42m";
exports.CONSOLE_STYLE_BgYellow = "\x1b[43m";
exports.CONSOLE_STYLE_BgBlue = "\x1b[44m";
exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen,
exports.CONSOLE_STYLE_FgLightGreen,
exports.CONSOLE_STYLE_FgBlue,
exports.CONSOLE_STYLE_FgLightBlue,
exports.CONSOLE_STYLE_FgMagenta,
exports.CONSOLE_STYLE_FgOrange,
exports.CONSOLE_STYLE_FgViolet,
exports.CONSOLE_STYLE_FgBrown,
exports.CONSOLE_STYLE_FgPink,
];
const consoleLevelColors = {
"INFO": exports.CONSOLE_STYLE_FgCyan,
"WARN": exports.CONSOLE_STYLE_FgYellow,
"ERROR": exports.CONSOLE_STYLE_FgRed,
"DEBUG": exports.CONSOLE_STYLE_FgGray,
};
function flipStatus(s) {
if (s === exports.UP) {
return exports.DOWN;
@ -35,18 +86,10 @@ function flipStatus(s) {
return s;
}
exports.flipStatus = flipStatus;
/**
* Delays for specified number of seconds
* @param ms Number of milliseconds to sleep for
*/
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;
@ -55,26 +98,12 @@ function ucfirst(str) {
return firstLetter.toUpperCase() + str.substr(1);
}
exports.ucfirst = ucfirst;
/**
* @deprecated Use log.debug
* @since https://github.com/louislam/uptime-kuma/pull/910
* @param msg
*/
function debug(msg) {
exports.log.log("", msg, "debug");
}
exports.debug = debug;
class Logger {
constructor() {
/**
* UPTIME_KUMA_HIDE_LOG=debug_monitor,info_monitor
*
* Example:
* [
* "debug_monitor", // Hide all logs that level is debug and the module is monitor
* "info_monitor",
* ]
*/
this.hideLog = {
info: [],
warn: [],
@ -82,10 +111,9 @@ class Logger {
debug: [],
};
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
let list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (let pair of list) {
// split first "_" only
let values = pair.split(/_(.*)/s);
const list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (const pair of list) {
const values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
@ -94,12 +122,6 @@ class Logger {
this.debug("server", this.hideLog);
}
}
/**
* Write a message to the log
* @param module The module the log comes from
* @param msg Message to write
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module, msg, level) {
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
@ -113,63 +135,56 @@ class Logger {
else {
now = dayjs().format();
}
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
let timePart = exports.CONSOLE_STYLE_FgCyan + now + exports.CONSOLE_STYLE_Reset;
let modulePart = "[" + moduleColor + module + exports.CONSOLE_STYLE_Reset + "]";
let levelPart = levelColor + `${level}:` + exports.CONSOLE_STYLE_Reset;
if (level === "INFO") {
console.info(formattedMessage);
console.info(timePart, modulePart, levelPart, msg);
}
else if (level === "WARN") {
console.warn(formattedMessage);
console.warn(timePart, modulePart, levelPart, msg);
}
else if (level === "ERROR") {
console.error(formattedMessage);
let msgPart;
if (typeof msg === "string") {
msgPart = exports.CONSOLE_STYLE_FgRed + msg + exports.CONSOLE_STYLE_Reset;
}
else {
msgPart = msg;
}
console.error(timePart, modulePart, levelPart, msgPart);
}
else if (level === "DEBUG") {
if (exports.isDev) {
console.log(formattedMessage);
timePart = exports.CONSOLE_STYLE_FgGray + now + exports.CONSOLE_STYLE_Reset;
let msgPart;
if (typeof msg === "string") {
msgPart = exports.CONSOLE_STYLE_FgGray + msg + exports.CONSOLE_STYLE_Reset;
}
else {
msgPart = msg;
}
console.debug(timePart, modulePart, levelPart, msgPart);
}
}
else {
console.log(formattedMessage);
console.log(timePart, modulePart, msg);
}
}
/**
* Log an INFO message
* @param module Module log comes from
* @param msg Message to write
*/
info(module, msg) {
this.log(module, msg, "info");
}
/**
* Log a WARN message
* @param module Module log comes from
* @param msg Message to write
*/
warn(module, msg) {
this.log(module, msg, "warn");
}
/**
* Log an ERROR message
* @param module Module log comes from
* @param msg Message to write
*/
error(module, msg) {
this.log(module, msg, "error");
}
/**
* Log a DEBUG message
* @param module Module log comes from
* @param msg Message to write
*/
debug(module, msg) {
this.log(module, msg, "debug");
}
/**
* Log an exeption as an ERROR
* @param module Module log comes from
* @param exception The exeption to include
* @param msg The message to write
*/
exception(module, exception, msg) {
let finalMessage = exception;
if (msg) {
@ -179,20 +194,12 @@ class Logger {
}
}
exports.log = new Logger();
/**
* 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
*/
function polyfill() {
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);
};
}
@ -202,10 +209,6 @@ class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
/**
* Output time since start of monitor
* @param name Name of monitor
*/
print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
@ -213,66 +216,42 @@ class TimeLogger {
}
}
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;
/**
* Returns either the NodeJS crypto.randomBytes() function or its
* browser equivalent implemented via window.crypto.getRandomValues()
*/
let getRandomBytes = ((typeof window !== 'undefined' && window.crypto)
// Browsers
const getRandomBytes = ((typeof window !== "undefined" && window.crypto)
? function () {
return (numBytes) => {
let randomBytes = new Uint8Array(numBytes);
const randomBytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
return randomBytes;
};
}
// Node
: function () {
return require("crypto").randomBytes;
})();
/**
* Get a random integer suitable for use in cryptography between upper
* and lower bounds.
* @param min Minimum value of integer
* @param max Maximum value of integer
* @returns Cryptographically suitable random integer
*/
function getCryptoRandomInt(min, max) {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
const range = max - min;
if (range >= Math.pow(2, 32))
if (range >= Math.pow(2, 32)) {
console.log("Warning! Range is too large.");
}
let tmpRange = range;
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0)
if (bitsNeeded % 8 === 0) {
bytesNeeded += 1;
}
bitsNeeded += 1;
mask = mask << 1 | 1;
tmpRange = tmpRange >>> 1;
@ -291,11 +270,6 @@ function getCryptoRandomInt(min, max) {
}
}
exports.getCryptoRandomInt = getCryptoRandomInt;
/**
* Generate a random alphanumeric string of fixed length
* @param length Length of string to generate
* @returns string
*/
function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -306,29 +280,14 @@ function genSecret(length = 64) {
return secret;
}
exports.genSecret = genSecret;
/**
* Get the path of a monitor
* @param id ID of monitor
* @returns Formatted relative path
*/
function getMonitorRelativeURL(id) {
return "/dashboard/" + id;
}
exports.getMonitorRelativeURL = getMonitorRelativeURL;
/**
* Get relative path for maintenance
* @param id ID of maintenance
* @returns Formatted relative path
*/
function getMaintenanceRelativeURL(id) {
return "/maintenance/" + id;
}
exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL;
/**
* Parse to Time Object that used in VueDatePicker
* @param {string} time E.g. 12:00
* @returns object
*/
function parseTimeObject(time) {
if (!time) {
return {
@ -336,11 +295,11 @@ function parseTimeObject(time) {
minutes: 0,
};
}
let array = time.split(":");
const array = time.split(":");
if (array.length < 2) {
throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
}
let obj = {
const obj = {
hours: parseInt(array[0]),
minutes: parseInt(array[1]),
seconds: 0,
@ -351,9 +310,6 @@ function parseTimeObject(time) {
return obj;
}
exports.parseTimeObject = parseTimeObject;
/**
* @returns string e.g. 12:00
*/
function parseTimeFromTimeObject(obj) {
if (!obj) {
return obj;
@ -366,36 +322,27 @@ function parseTimeFromTimeObject(obj) {
return result;
}
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
/**
* Convert ISO date to UTC
* @param input Date
* @returns ISO Date time
*/
function isoToUTCDateTime(input) {
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
}
exports.isoToUTCDateTime = isoToUTCDateTime;
/**
* @param input
*/
function utcToISODateTime(input) {
return dayjs.utc(input).toISOString();
}
exports.utcToISODateTime = utcToISODateTime;
/**
* For SQL_DATETIME_FORMAT
*/
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format);
}
exports.utcToLocal = utcToLocal;
/**
* Convert local datetime to UTC
* @param input Local date
* @param format Format to return
* @returns Date in requested format
*/
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format);
}
exports.localToUTC = localToUTC;
function intHash(str, length = 10) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i);
}
return (hash % length + length) % length;
}
exports.intHash = intHash;

@ -1,3 +1,4 @@
/*!
// Common Util for frontend and backend
//
// DOT NOT MODIFY util.js!
@ -5,9 +6,14 @@
//
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
import * as dayjs from "dayjs";
import * as dayjs from "dayjs";
// For loading dayjs plugins, don't remove event though it is not used in this file
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as timezone from "dayjs/plugin/timezone";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as utc from "dayjs/plugin/utc";
export const isDev = process.env.NODE_ENV === "development";
@ -29,7 +35,66 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
/** Flip the status of s */
// Console colors
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
export const CONSOLE_STYLE_Reset = "\x1b[0m";
export const CONSOLE_STYLE_Bright = "\x1b[1m";
export const CONSOLE_STYLE_Dim = "\x1b[2m";
export const CONSOLE_STYLE_Underscore = "\x1b[4m";
export const CONSOLE_STYLE_Blink = "\x1b[5m";
export const CONSOLE_STYLE_Reverse = "\x1b[7m";
export const CONSOLE_STYLE_Hidden = "\x1b[8m";
export const CONSOLE_STYLE_FgBlack = "\x1b[30m";
export const CONSOLE_STYLE_FgRed = "\x1b[31m";
export const CONSOLE_STYLE_FgGreen = "\x1b[32m";
export const CONSOLE_STYLE_FgYellow = "\x1b[33m";
export const CONSOLE_STYLE_FgBlue = "\x1b[34m";
export const CONSOLE_STYLE_FgMagenta = "\x1b[35m";
export const CONSOLE_STYLE_FgCyan = "\x1b[36m";
export const CONSOLE_STYLE_FgWhite = "\x1b[37m";
export const CONSOLE_STYLE_FgGray = "\x1b[90m";
export const CONSOLE_STYLE_FgOrange = "\x1b[38;5;208m";
export const CONSOLE_STYLE_FgLightGreen = "\x1b[38;5;119m";
export const CONSOLE_STYLE_FgLightBlue = "\x1b[38;5;117m";
export const CONSOLE_STYLE_FgViolet = "\x1b[38;5;141m";
export const CONSOLE_STYLE_FgBrown = "\x1b[38;5;130m";
export const CONSOLE_STYLE_FgPink = "\x1b[38;5;219m";
export const CONSOLE_STYLE_BgBlack = "\x1b[40m";
export const CONSOLE_STYLE_BgRed = "\x1b[41m";
export const CONSOLE_STYLE_BgGreen = "\x1b[42m";
export const CONSOLE_STYLE_BgYellow = "\x1b[43m";
export const CONSOLE_STYLE_BgBlue = "\x1b[44m";
export const CONSOLE_STYLE_BgMagenta = "\x1b[45m";
export const CONSOLE_STYLE_BgCyan = "\x1b[46m";
export const CONSOLE_STYLE_BgWhite = "\x1b[47m";
export const CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
CONSOLE_STYLE_FgCyan,
CONSOLE_STYLE_FgGreen,
CONSOLE_STYLE_FgLightGreen,
CONSOLE_STYLE_FgBlue,
CONSOLE_STYLE_FgLightBlue,
CONSOLE_STYLE_FgMagenta,
CONSOLE_STYLE_FgOrange,
CONSOLE_STYLE_FgViolet,
CONSOLE_STYLE_FgBrown,
CONSOLE_STYLE_FgPink,
];
const consoleLevelColors : Record<string, string> = {
"INFO": CONSOLE_STYLE_FgCyan,
"WARN": CONSOLE_STYLE_FgYellow,
"ERROR": CONSOLE_STYLE_FgRed,
"DEBUG": CONSOLE_STYLE_FgGray,
};
/**
* Flip the status of s
* @param s
*/
export function flipStatus(s: number) {
if (s === UP) {
return DOWN;
@ -64,11 +129,10 @@ export function ucfirst(str: string) {
}
/**
* @deprecated Use log.debug
* @since https://github.com/louislam/uptime-kuma/pull/910
* @deprecated Use log.debug (https://github.com/louislam/uptime-kuma/pull/910)
* @param msg
*/
export function debug(msg: any) {
export function debug(msg: unknown) {
log.log("", msg, "debug");
}
@ -83,20 +147,23 @@ class Logger {
* "info_monitor",
* ]
*/
hideLog : any = {
hideLog : Record<string, string[]> = {
info: [],
warn: [],
error: [],
debug: [],
};
/**
*
*/
constructor() {
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
let list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
const list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (let pair of list) {
for (const pair of list) {
// split first "_" only
let values = pair.split(/_(.*)/s);
const values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
@ -128,20 +195,39 @@ class Logger {
} else {
now = dayjs().format();
}
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
let timePart = CONSOLE_STYLE_FgCyan + now + CONSOLE_STYLE_Reset;
let modulePart = "[" + moduleColor + module + CONSOLE_STYLE_Reset + "]";
let levelPart = levelColor + `${level}:` + CONSOLE_STYLE_Reset;
if (level === "INFO") {
console.info(formattedMessage);
console.info(timePart, modulePart, levelPart, msg);
} else if (level === "WARN") {
console.warn(formattedMessage);
console.warn(timePart, modulePart, levelPart, msg);
} else if (level === "ERROR") {
console.error(formattedMessage);
let msgPart :string;
if (typeof msg === "string") {
msgPart = CONSOLE_STYLE_FgRed + msg + CONSOLE_STYLE_Reset;
} else {
msgPart = msg;
}
console.error(timePart, modulePart, levelPart, msgPart);
} else if (level === "DEBUG") {
if (isDev) {
console.log(formattedMessage);
timePart = CONSOLE_STYLE_FgGray + now + CONSOLE_STYLE_Reset;
let msgPart :string;
if (typeof msg === "string") {
msgPart = CONSOLE_STYLE_FgGray + msg + CONSOLE_STYLE_Reset;
} else {
msgPart = msg;
}
console.debug(timePart, modulePart, levelPart, msgPart);
}
} else {
console.log(formattedMessage);
console.log(timePart, modulePart, msg);
}
}
@ -150,7 +236,7 @@ class Logger {
* @param module Module log comes from
* @param msg Message to write
*/
info(module: string, msg: any) {
info(module: string, msg: unknown) {
this.log(module, msg, "info");
}
@ -159,7 +245,7 @@ class Logger {
* @param module Module log comes from
* @param msg Message to write
*/
warn(module: string, msg: any) {
warn(module: string, msg: unknown) {
this.log(module, msg, "warn");
}
@ -168,8 +254,8 @@ class Logger {
* @param module Module log comes from
* @param msg Message to write
*/
error(module: string, msg: any) {
this.log(module, msg, "error");
error(module: string, msg: unknown) {
this.log(module, msg, "error");
}
/**
@ -177,24 +263,24 @@ class Logger {
* @param module Module log comes from
* @param msg Message to write
*/
debug(module: string, msg: any) {
this.log(module, msg, "debug");
debug(module: string, msg: unknown) {
this.log(module, msg, "debug");
}
/**
* Log an exeption as an ERROR
* Log an exception as an ERROR
* @param module Module log comes from
* @param exception The exeption to include
* @param exception The exception to include
* @param msg The message to write
*/
exception(module: string, exception: any, msg: any) {
let finalMessage = exception
exception(module: string, exception: unknown, msg: unknown) {
let finalMessage = exception;
if (msg) {
finalMessage = `${msg}: ${exception}`
finalMessage = `${msg}: ${exception}`;
}
this.log(module, finalMessage , "error");
this.log(module, finalMessage, "error");
}
}
@ -225,22 +311,28 @@ export function polyfill() {
export class TimeLogger {
startTime: number;
/**
*
*/
constructor() {
this.startTime = dayjs().valueOf();
}
/**
* Output time since start of monitor
* @param name Name of monitor
*/
print(name: string) {
if (isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
}
}
}
/**
* Returns a random number between min (inclusive) and max (exclusive)
* @param min
* @param max
*/
export function getRandomArbitrary(min: number, max: number) {
return Math.random() * (max - min) + min;
@ -254,6 +346,8 @@ export function getRandomArbitrary(min: number, max: number) {
* 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!
* @param min
* @param max
*/
export function getRandomInt(min: number, max: number) {
min = Math.ceil(min);
@ -265,13 +359,13 @@ export function getRandomInt(min: number, max: number) {
* Returns either the NodeJS crypto.randomBytes() function or its
* browser equivalent implemented via window.crypto.getRandomValues()
*/
let getRandomBytes = (
(typeof window !== 'undefined' && window.crypto)
const getRandomBytes = (
(typeof window !== "undefined" && window.crypto)
// Browsers
? function () {
return (numBytes: number) => {
let randomBytes = new Uint8Array(numBytes);
const randomBytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
@ -279,8 +373,9 @@ let getRandomBytes = (
};
}
// Node
: function() {
// Node
: function () {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require("crypto").randomBytes;
}
)();
@ -296,35 +391,38 @@ export function getCryptoRandomInt(min: number, max: number):number {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
const range = max - min
if (range >= Math.pow(2, 32))
console.log("Warning! Range is too large.")
const range = max - min;
if (range >= Math.pow(2, 32)) {
console.log("Warning! Range is too large.");
}
let tmpRange = range
let bitsNeeded = 0
let bytesNeeded = 0
let mask = 1
let tmpRange = range;
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0) bytesNeeded += 1
bitsNeeded += 1
mask = mask << 1 | 1
tmpRange = tmpRange >>> 1
if (bitsNeeded % 8 === 0) {
bytesNeeded += 1;
}
bitsNeeded += 1;
mask = mask << 1 | 1;
tmpRange = tmpRange >>> 1;
}
const randomBytes = getRandomBytes(bytesNeeded)
let randomValue = 0
const randomBytes = getRandomBytes(bytesNeeded);
let randomValue = 0;
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= randomBytes[i] << 8 * i
randomValue |= randomBytes[i] << 8 * i;
}
randomValue = randomValue & mask;
if (randomValue <= range) {
return min + randomValue
return min + randomValue;
} else {
return getCryptoRandomInt(min, max)
return getCryptoRandomInt(min, max);
}
}
@ -374,17 +472,17 @@ export function parseTimeObject(time: string) {
};
}
let array = time.split(":");
const array = time.split(":");
if (array.length < 2) {
throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
}
let obj = {
const obj = {
hours: parseInt(array[0]),
minutes: parseInt(array[1]),
seconds: 0,
}
};
if (array.length >= 3) {
obj.seconds = parseInt(array[2]);
}
@ -392,6 +490,7 @@ export function parseTimeObject(time: string) {
}
/**
* @param obj
* @returns string e.g. 12:00
*/
export function parseTimeFromTimeObject(obj : any) {
@ -401,10 +500,10 @@ export function parseTimeFromTimeObject(obj : any) {
let result = "";
result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0")
result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0");
if (obj.seconds) {
result += ":" + obj.seconds.toString().padStart(2, "0")
result += ":" + obj.seconds.toString().padStart(2, "0");
}
return result;
@ -428,8 +527,11 @@ export function utcToISODateTime(input : string) {
/**
* For SQL_DATETIME_FORMAT
* @param input
* @param format
* @returns A string date of SQL_DATETIME_FORMAT
*/
export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) : string {
return dayjs.utc(input).local().format(format);
}
@ -442,3 +544,19 @@ export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format);
}
/**
* Generate a decimal integer number from a string
* @param str Input
* @param length Default is 10 which means 0 - 9
*/
export function intHash(str : string, length = 10) : number {
// A simple hashing function (you can use more complex hash functions if needed)
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i);
}
// Normalize the hash to the range [0, 10]
return (hash % length + length) % length; // Ensure the result is non-negative
}

@ -8,7 +8,8 @@
"es2020",
"DOM"
],
"removeComments": false,
"declaration": false,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": false,
"strict": true

Loading…
Cancel
Save