Merge branch 'master' of https://github.com/louislam/uptime-kuma into feat/add-auth-header-to-webhook-notification-#1919
commit
3c5de1c889
@ -0,0 +1,24 @@
|
|||||||
|
name: 'Automatically close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */6 * * *'
|
||||||
|
#Run every 6 hours
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
||||||
|
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 2 days with no activity.'
|
||||||
|
days-before-stale: 90
|
||||||
|
days-before-close: 2
|
||||||
|
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
|
||||||
|
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
|
||||||
|
exempt-issue-assignees: 'louislam'
|
||||||
|
exempt-pr-assignees: 'louislam'
|
||||||
|
operations-per-run: 200
|
@ -0,0 +1,28 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
projectId: "vyjuem",
|
||||||
|
e2e: {
|
||||||
|
experimentalStudio: true,
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
|
||||||
|
},
|
||||||
|
fixturesFolder: "test/cypress/fixtures",
|
||||||
|
screenshotsFolder: "test/cypress/screenshots",
|
||||||
|
videosFolder: "test/cypress/videos",
|
||||||
|
downloadsFolder: "test/cypress/downloads",
|
||||||
|
supportFile: "test/cypress/support/e2e.js",
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
pageLoadTimeout: 60000,
|
||||||
|
viewportWidth: 1920,
|
||||||
|
viewportHeight: 1080,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/e2e/setup.cy.js",
|
||||||
|
"test/cypress/e2e/**/*.js"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
},
|
||||||
|
});
|
@ -1,33 +0,0 @@
|
|||||||
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
|
||||||
const util = require("util");
|
|
||||||
|
|
||||||
class DebugEnv extends PuppeteerEnvironment {
|
|
||||||
async handleTestEvent(event, state) {
|
|
||||||
const ignoredEvents = [
|
|
||||||
"setup",
|
|
||||||
"add_hook",
|
|
||||||
"start_describe_definition",
|
|
||||||
"add_test",
|
|
||||||
"finish_describe_definition",
|
|
||||||
"run_start",
|
|
||||||
"run_describe_start",
|
|
||||||
"test_start",
|
|
||||||
"hook_start",
|
|
||||||
"hook_success",
|
|
||||||
"test_fn_start",
|
|
||||||
"test_fn_success",
|
|
||||||
"test_done",
|
|
||||||
"run_describe_finish",
|
|
||||||
"run_finish",
|
|
||||||
"teardown",
|
|
||||||
"test_fn_failure",
|
|
||||||
];
|
|
||||||
if (!ignoredEvents.includes(event.name)) {
|
|
||||||
console.log(
|
|
||||||
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DebugEnv;
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"rootDir": "..",
|
|
||||||
"testRegex": "./test/frontend.spec.js",
|
|
||||||
};
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"launch": {
|
|
||||||
"dumpio": true,
|
|
||||||
"slowMo": 500,
|
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
|
||||||
args: [
|
|
||||||
"--disable-setuid-sandbox",
|
|
||||||
"--disable-gpu",
|
|
||||||
"--disable-dev-shm-usage",
|
|
||||||
"--no-default-browser-check",
|
|
||||||
"--no-experiments",
|
|
||||||
"--no-first-run",
|
|
||||||
"--no-pings",
|
|
||||||
"--no-sandbox",
|
|
||||||
"--no-zygote",
|
|
||||||
"--single-process",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,12 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"verbose": true,
|
|
||||||
"preset": "jest-puppeteer",
|
|
||||||
"globals": {
|
|
||||||
"__DEV__": true
|
|
||||||
},
|
|
||||||
"testRegex": "./test/e2e.spec.js",
|
|
||||||
"testEnvironment": "./config/jest-debug-env.js",
|
|
||||||
"rootDir": "..",
|
|
||||||
"testTimeout": 30000,
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE docker_host (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
docker_daemon VARCHAR(255),
|
||||||
|
docker_type VARCHAR(255),
|
||||||
|
name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_host INTEGER REFERENCES docker_host(id);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_container VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,18 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_username VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_password VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_calling_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_called_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_secret VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT
|
@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD resend_interval INTEGER default 0 not null;
|
||||||
|
|
||||||
|
ALTER TABLE heartbeat
|
||||||
|
ADD down_count INTEGER default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,33 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
if (!process.env.UPTIME_KUMA_GH_REPO) {
|
||||||
|
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
|
||||||
|
|
||||||
|
if (inputArray.length !== 2) {
|
||||||
|
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = inputArray[0];
|
||||||
|
let branch = inputArray[1];
|
||||||
|
|
||||||
|
console.log("Checkout pr");
|
||||||
|
|
||||||
|
// Checkout the pr
|
||||||
|
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
||||||
|
|
||||||
|
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
||||||
|
|
||||||
|
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
|
||||||
|
|
||||||
|
console.log(result.stdout.toString());
|
||||||
|
console.error(result.stderr.toString());
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@
|
|||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
const CacheableLookup = require("cacheable-lookup");
|
||||||
|
|
||||||
|
class CacheableDnsHttpAgent {
|
||||||
|
|
||||||
|
static cacheable = new CacheableLookup();
|
||||||
|
|
||||||
|
static httpAgentList = {};
|
||||||
|
static httpsAgentList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register cacheable to global agents
|
||||||
|
*/
|
||||||
|
static registerGlobalAgent() {
|
||||||
|
this.cacheable.install(http.globalAgent);
|
||||||
|
this.cacheable.install(https.globalAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static install(agent) {
|
||||||
|
this.cacheable.install(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {https.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agent}
|
||||||
|
*/
|
||||||
|
static getHttpsAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpsAgentList)) {
|
||||||
|
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpsAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpsAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {http.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agents}
|
||||||
|
*/
|
||||||
|
static getHttpAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpAgentList)) {
|
||||||
|
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CacheableDnsHttpAgent,
|
||||||
|
};
|
@ -0,0 +1,118 @@
|
|||||||
|
const axios = require("axios");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const version = require("../package.json").version;
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
class DockerHost {
|
||||||
|
/**
|
||||||
|
* Save a docker host
|
||||||
|
* @param {Object} dockerHost Docker host to save
|
||||||
|
* @param {?number} dockerHostID ID of the docker host to update
|
||||||
|
* @param {number} userID ID of the user who adds the docker host
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async save(dockerHost, dockerHostID, userID) {
|
||||||
|
let bean;
|
||||||
|
|
||||||
|
if (dockerHostID) {
|
||||||
|
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bean = R.dispense("docker_host");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.user_id = userID;
|
||||||
|
bean.docker_daemon = dockerHost.dockerDaemon;
|
||||||
|
bean.docker_type = dockerHost.dockerType;
|
||||||
|
bean.name = dockerHost.name;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Docker host
|
||||||
|
* @param {number} dockerHostID ID of the Docker host to delete
|
||||||
|
* @param {number} userID ID of the user who created the Docker host
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async delete(dockerHostID, userID) {
|
||||||
|
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed proxy from monitors if exists
|
||||||
|
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
|
||||||
|
|
||||||
|
await R.trash(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the amount of containers on the Docker host
|
||||||
|
* @param {Object} dockerHost Docker host to check for
|
||||||
|
* @returns {number} Total amount of containers on the host
|
||||||
|
*/
|
||||||
|
static async testDockerHost(dockerHost) {
|
||||||
|
const options = {
|
||||||
|
url: "/containers/json?all=true",
|
||||||
|
headers: {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "Uptime-Kuma/" + version
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost.dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost.dockerDaemon;
|
||||||
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
|
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await axios.request(options);
|
||||||
|
|
||||||
|
if (Array.isArray(res.data)) {
|
||||||
|
|
||||||
|
if (res.data.length > 1) {
|
||||||
|
|
||||||
|
if ("ImageID" in res.data[0]) {
|
||||||
|
return res.data.length;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return res.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since axios 0.27.X, it does not accept `tcp://` protocol.
|
||||||
|
* Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
|
||||||
|
*/
|
||||||
|
static patchDockerURL(url) {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
// Replace the first occurrence only with g
|
||||||
|
return url.replace(/tcp:\/\//g, "http://");
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DockerHost,
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class DockerHost extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Returns an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
userID: this.user_id,
|
||||||
|
dockerDaemon: this.docker_daemon,
|
||||||
|
dockerType: this.docker_type,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DockerHost;
|
@ -0,0 +1,24 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class FreeMobile extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "FreeMobile";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||||
|
"user": notification.freemobileUser,
|
||||||
|
"pass": notification.freemobilePass,
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FreeMobile;
|
@ -0,0 +1,35 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class GoAlert extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "GoAlert";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let closeAction = "close";
|
||||||
|
let data = {
|
||||||
|
summary: msg,
|
||||||
|
};
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
data["action"] = closeAction;
|
||||||
|
}
|
||||||
|
let headers = {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
let config = {
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
let msg = (error.response.data) ? error.response.data : "Error without response";
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GoAlert;
|
@ -0,0 +1,38 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const defaultNotificationService = "notify";
|
||||||
|
|
||||||
|
class HomeAssistant extends NotificationProvider {
|
||||||
|
name = "HomeAssistant";
|
||||||
|
|
||||||
|
async send(notification, message, monitor = null, heartbeat = null) {
|
||||||
|
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||||
|
{
|
||||||
|
title: "Uptime Kuma",
|
||||||
|
message,
|
||||||
|
...(notificationService !== "persistent_notification" && { data: {
|
||||||
|
name: monitor?.name,
|
||||||
|
status: heartbeat?.status,
|
||||||
|
} }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${notification.longLivedAccessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return "Sent Successfully.";
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HomeAssistant;
|
@ -0,0 +1,43 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const qs = require("qs");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class LineNotify extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "LineNotify";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let lineAPIUrl = "https://notify-api.line.me/api/notify";
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Authorization": "Bearer " + notification.lineNotifyAccessToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let testMessage = {
|
||||||
|
"message": msg,
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||||
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
let downMessage = {
|
||||||
|
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||||
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
|
let upMessage = {
|
||||||
|
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||||
|
}
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LineNotify;
|
@ -0,0 +1,36 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class ServerChan extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "ServerChan";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
|
||||||
|
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||||
|
"desp": msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStatus(heartbeatJSON, monitorJSON) {
|
||||||
|
let title = "UptimeKuma Message";
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
|
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
|
||||||
|
}
|
||||||
|
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||||
|
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ServerChan;
|
@ -0,0 +1,25 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SMSManager extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "SMSManager";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
let data = {
|
||||||
|
apikey: notification.smsmanagerApiKey,
|
||||||
|
endpoint: "https://http-api.smsmanager.cz/Send",
|
||||||
|
message: msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
to: notification.numbers,
|
||||||
|
messageType: notification.messageType,
|
||||||
|
};
|
||||||
|
await axios.get(`${data.endpoint}?apikey=${data.apikey}&message=${data.message}&number=${data.to}&gateway=${data.messageType}`);
|
||||||
|
return "SMS sent sucessfully.";
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SMSManager;
|
@ -0,0 +1,76 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN } = require("../../src/util");
|
||||||
|
|
||||||
|
class Squadcast extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "squadcast";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
let data = {
|
||||||
|
message: msg,
|
||||||
|
description: "",
|
||||||
|
tags: {},
|
||||||
|
heartbeat: heartbeatJSON,
|
||||||
|
source: "uptime-kuma"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (heartbeatJSON !== null) {
|
||||||
|
data.description = heartbeatJSON["msg"];
|
||||||
|
data.event_id = heartbeatJSON["monitorID"];
|
||||||
|
|
||||||
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
data.message = `${monitorJSON["name"]} is DOWN`;
|
||||||
|
data.status = "trigger";
|
||||||
|
} else {
|
||||||
|
data.message = `${monitorJSON["name"]} is UP`;
|
||||||
|
data.status = "resolve";
|
||||||
|
}
|
||||||
|
|
||||||
|
let address;
|
||||||
|
switch (monitorJSON["type"]) {
|
||||||
|
case "ping":
|
||||||
|
address = monitorJSON["hostname"];
|
||||||
|
break;
|
||||||
|
case "port":
|
||||||
|
case "dns":
|
||||||
|
case "steam":
|
||||||
|
address = monitorJSON["hostname"];
|
||||||
|
if (monitorJSON["port"]) {
|
||||||
|
address += ":" + monitorJSON["port"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
address = monitorJSON["url"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.tags["AlertAddress"] = address;
|
||||||
|
|
||||||
|
monitorJSON["tags"].forEach(tag => {
|
||||||
|
data.tags[tag["name"]] = {
|
||||||
|
value: tag["value"]
|
||||||
|
};
|
||||||
|
if (tag["color"] !== null) {
|
||||||
|
data.tags[tag["name"]]["color"] = tag["color"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(notification.squadcastWebhookURL, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Squadcast;
|
@ -0,0 +1,79 @@
|
|||||||
|
const { sendDockerHostList } = require("../client");
|
||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { DockerHost } = require("../docker");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for docker hosts
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
|
module.exports.dockerSocketHandler = (socket) => {
|
||||||
|
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
|
||||||
|
await sendDockerHostList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved",
|
||||||
|
id: dockerHostBean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteDockerHost", async (dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await DockerHost.delete(dockerHostID, socket.userID);
|
||||||
|
await sendDockerHostList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("testDockerHost", async (dockerHost, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let amount = await DockerHost.testDockerHost(dockerHost);
|
||||||
|
let msg;
|
||||||
|
|
||||||
|
if (amount >= 1) {
|
||||||
|
msg = "Connected Successfully. Amount of containers: " + amount;
|
||||||
|
} else {
|
||||||
|
msg = "Connected Successfully, but there are no containers?";
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.error("docker", e);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
|
{{ $t("Setup Docker Host") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||||
|
<input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
|
||||||
|
<select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
|
||||||
|
<option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
|
||||||
|
<input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("Examples") }}:
|
||||||
|
<ul>
|
||||||
|
<li>/var/run/docker.sock</li>
|
||||||
|
<li>http://localhost:2375</li>
|
||||||
|
<li>https://localhost:2376 (TLS)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
|
{{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||||
|
{{ $t("Test") }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
|
||||||
|
{{ $t("deleteDockerHostMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
emits: [ "added" ],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
processing: false,
|
||||||
|
id: null,
|
||||||
|
connectionTypes: [ "socket", "tcp" ],
|
||||||
|
dockerHost: {
|
||||||
|
name: "",
|
||||||
|
dockerDaemon: "",
|
||||||
|
dockerType: "",
|
||||||
|
// Do not set default value here, please scroll to show()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
deleteConfirm() {
|
||||||
|
this.modal.hide();
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
show(dockerHostID) {
|
||||||
|
if (dockerHostID) {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
this.id = dockerHostID;
|
||||||
|
|
||||||
|
for (let n of this.$root.dockerHostList) {
|
||||||
|
if (n.id === dockerHostID) {
|
||||||
|
this.dockerHost = n;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
toast.error("Docker Host not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.id = null;
|
||||||
|
this.dockerHost = {
|
||||||
|
name: "",
|
||||||
|
dockerType: "socket",
|
||||||
|
dockerDaemon: "/var/run/docker.sock",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
|
||||||
|
// Emit added event, doesn't emit edit.
|
||||||
|
if (! this.id) {
|
||||||
|
this.$emit("added", res.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
test() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDockerHost() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="freemobileUser" class="form-label">{{ $t("Free Mobile User Identifier") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="freemobileUser" v-model="$parent.notification.freemobileUser" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="freemobilePass" class="form-label">{{ $t("Free Mobile API Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="freemobilePass" v-model="$parent.notification.freemobilePass" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="goAlertInfo" class="form-text">
|
||||||
|
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
|
||||||
|
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("goAlertIntegrationKeyInfo") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="homeAssistantUrl" class="form-label">{{ $t("Home Assistant URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="homeAssistantUrl" v-model="$parent.notification.homeAssistantUrl" type="url" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="longLivedAccessToken" class="form-label">{{ $t("Long-Lived Access Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="longLivedAccessToken" v-model="$parent.notification.longLivedAccessToken" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
|
||||||
|
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
|
||||||
|
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
|
||||||
|
<p>
|
||||||
|
{{ $t("Trigger type:") }} <code>Event</code><br />
|
||||||
|
{{ $t("Event type:") }} <code>call_service</code><br />
|
||||||
|
{{ $t("Event data:") }}
|
||||||
|
</p>
|
||||||
|
<pre>domain: notify
|
||||||
|
service: mobile_app_my_phone # change to your device name
|
||||||
|
service_data:
|
||||||
|
title: Uptime Kuma
|
||||||
|
data:
|
||||||
|
status: 0 # 0=down 1=up
|
||||||
|
# name: Optional Uptime Kuma Monitor Name to filter by</pre>
|
||||||
|
<p>
|
||||||
|
{{ $t("Then choose an action, for example switch the scene to where an RGB light is red.") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="line-notify-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||||
|
<input id="line-notify-access-token" v-model="$parent.notification.lineNotifyAccessToken" type="text" class="form-control" :required="true">
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetLineNotifyToken" class="form-text" style="margin-top: 8px;">
|
||||||
|
<a href="https://notify-bot.line.me/" target="_blank">https://notify-bot.line.me/</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsmanager-key" class="form-label">API Key</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("SMSManager API Docs ") }}
|
||||||
|
<a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</div>
|
||||||
|
<input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsmanager-numbers" class="form-label"> {{ $t("Recipients") }}</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("You can divide numbers with") }} <b>,</b> {{ $t("or") }} <b>;</b>
|
||||||
|
</div>
|
||||||
|
<input id="smsmanager-numbers" v-model="$parent.notification.numbers" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsmanager-messageType" class="form-label">{{ $t("Gateway Type") }}</label>
|
||||||
|
<select id="smsmanager-messageType" v-model="$parent.notification.messageType" class="form-select">
|
||||||
|
<option value="economy">Economy</option>
|
||||||
|
<option value="lowcost">Lowcost</option>
|
||||||
|
<option value="high" selected>High</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("checkPrice", [$t("SMSManager")]) }}
|
||||||
|
<a href="https://smsmanager.cz/rozesilani-sms/ceny/ceska-republika/" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
|
||||||
|
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
|
||||||
|
<input id="webhook-url" v-model="$parent.notification.squadcastWebhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="dockerHost-list my-4">
|
||||||
|
<p v-if="$root.dockerHostList.length === 0">
|
||||||
|
{{ $t("Not available, please setup.") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||||
|
<li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item">
|
||||||
|
{{ dockerHost.name }}<br>
|
||||||
|
<a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
|
||||||
|
{{ $t("Setup Docker Host") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DockerHostDialog ref="dockerHostDialog" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DockerHostDialog from "../../components/DockerHostDialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DockerHostDialog,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,203 @@
|
|||||||
|
export default {
|
||||||
|
languageName: "Português (Portugal)",
|
||||||
|
checkEverySecond: "Verificar a cada {0} segundos.",
|
||||||
|
retryCheckEverySecond: "Tentar novamente a cada {0} segundos.",
|
||||||
|
retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
|
||||||
|
ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS",
|
||||||
|
upsideDownModeDescription: "Inverte o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
|
||||||
|
maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Define como 0 para desativar redirecionamentos.",
|
||||||
|
acceptedStatusCodesDescription: "Seleciona os códigos de status que são considerados uma resposta bem-sucedida.",
|
||||||
|
passwordNotMatchMsg: "A senha repetida não corresponde.",
|
||||||
|
notificationDescription: "Atribuir uma notificação ao (s) monitor (es) para que funcione.",
|
||||||
|
keywordDescription: "Pesquisa a palavra-chave em HTML simples ou resposta JSON e diferencia maiúsculas de minúsculas",
|
||||||
|
pauseDashboardHome: "Pausa",
|
||||||
|
deleteMonitorMsg: "Tens a certeza de que queres excluir este monitor?",
|
||||||
|
deleteNotificationMsg: "Tens a certeza de que queres excluir esta notificação para todos os monitores?",
|
||||||
|
resolverserverDescription: "A Cloudflare é o servidor padrão, podes alterar o servidor 'resolvedor' a qualquer momento.",
|
||||||
|
rrtypeDescription: "Seleciona o RR-Type que queres monitorizar",
|
||||||
|
pauseMonitorMsg: "Tens a certeza que queres fazer uma pausa?",
|
||||||
|
enableDefaultNotificationDescription: "Para cada monitor novo esta notificação vai estar activa por padrão. Podes também desativar a notificação separadamente para cada monitor.",
|
||||||
|
clearEventsMsg: "Tens a certeza que queres excluir todos os eventos deste monitor?",
|
||||||
|
clearHeartbeatsMsg: "Tens a certeza de que queres excluir todos os heartbeats deste monitor?",
|
||||||
|
confirmClearStatisticsMsg: "Tens a certeza que queres excluir TODAS as estatísticas?",
|
||||||
|
importHandleDescription: "Escolhe 'Ignorar existente' se quiseres ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.",
|
||||||
|
confirmImportMsg: "Tens a certeza que queres importar o backup? Certifica-te que selecionaste a opção de importação correta.",
|
||||||
|
twoFAVerifyLabel: "Insire o teu token para verificares se o 2FA está a funcionar",
|
||||||
|
tokenValidSettingsMsg: "O token é válido! Agora podes salvar as configurações do 2FA.",
|
||||||
|
confirmEnableTwoFAMsg: "Tens a certeza de que queres habilitar 2FA?",
|
||||||
|
confirmDisableTwoFAMsg: "Tens a certeza de que queres desativar 2FA?",
|
||||||
|
Settings: "Configurações",
|
||||||
|
Dashboard: "Dashboard",
|
||||||
|
"New Update": "Nova Atualização",
|
||||||
|
Language: "Linguagem",
|
||||||
|
Appearance: "Aparência",
|
||||||
|
Theme: "Tema",
|
||||||
|
General: "Geral",
|
||||||
|
Version: "Versão",
|
||||||
|
"Check Update On GitHub": "Verificar atualização no Github",
|
||||||
|
List: "Lista",
|
||||||
|
Add: "Adicionar",
|
||||||
|
"Add New Monitor": "Adicionar novo monitor",
|
||||||
|
"Quick Stats": "Estatísticas rápidas",
|
||||||
|
Up: "On",
|
||||||
|
Down: "Off",
|
||||||
|
Pending: "Pendente",
|
||||||
|
Unknown: "Desconhecido",
|
||||||
|
Pause: "Pausa",
|
||||||
|
Name: "Nome",
|
||||||
|
Status: "Status",
|
||||||
|
DateTime: "Data hora",
|
||||||
|
Message: "Mensagem",
|
||||||
|
"No important events": "Nenhum evento importante",
|
||||||
|
Resume: "Resumo",
|
||||||
|
Edit: "Editar",
|
||||||
|
Delete: "Apagar",
|
||||||
|
Current: "Atual",
|
||||||
|
Uptime: "Tempo de atividade",
|
||||||
|
"Cert Exp.": "Cert Exp.",
|
||||||
|
day: "dia | dias",
|
||||||
|
"-day": "-dia",
|
||||||
|
hour: "hora",
|
||||||
|
"-hour": "-hora",
|
||||||
|
Response: "Resposta",
|
||||||
|
Ping: "Ping",
|
||||||
|
"Monitor Type": "Tipo de Monitor",
|
||||||
|
Keyword: "Palavra-Chave",
|
||||||
|
"Friendly Name": "Nome Amigável",
|
||||||
|
URL: "URL",
|
||||||
|
Hostname: "Hostname",
|
||||||
|
Port: "Porta",
|
||||||
|
"Heartbeat Interval": "Intervalo de Heartbeats",
|
||||||
|
Retries: "Novas tentativas",
|
||||||
|
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeats",
|
||||||
|
Advanced: "Avançado",
|
||||||
|
"Upside Down Mode": "Modo de cabeça para baixo",
|
||||||
|
"Max. Redirects": "Redirecionamento Máx.",
|
||||||
|
"Accepted Status Codes": "Status Code Aceitáveis",
|
||||||
|
Save: "Guardar",
|
||||||
|
Notifications: "Notificações",
|
||||||
|
"Not available, please setup.": "Não disponível, por favor configura.",
|
||||||
|
"Setup Notification": "Configurar Notificação",
|
||||||
|
Light: "Claro",
|
||||||
|
Dark: "Escuro",
|
||||||
|
Auto: "Auto",
|
||||||
|
"Theme - Heartbeat Bar": "Tema - Barra de Heartbeat",
|
||||||
|
Normal: "Normal",
|
||||||
|
Bottom: "Inferior",
|
||||||
|
None: "Nenhum",
|
||||||
|
Timezone: "Fuso horário",
|
||||||
|
"Search Engine Visibility": "Visibilidade do mecanismo de pesquisa",
|
||||||
|
"Allow indexing": "Permitir Indexação",
|
||||||
|
"Discourage search engines from indexing site": "Desencorajar que motores de busca indexem o site",
|
||||||
|
"Change Password": "Mudar senha",
|
||||||
|
"Current Password": "Senha atual",
|
||||||
|
"New Password": "Nova Senha",
|
||||||
|
"Repeat New Password": "Repetir Nova Senha",
|
||||||
|
"Update Password": "Atualizar Senha",
|
||||||
|
"Disable Auth": "Desativar Autenticação",
|
||||||
|
"Enable Auth": "Ativar Autenticação",
|
||||||
|
"disableauth.message1": "Tens a certeza que queres <strong>desativar a autenticação</strong>?",
|
||||||
|
"disableauth.message2": "Isso é para <strong>alguém que tem autenticação de terceiros</strong> em frente ao 'UpTime Kuma' como o Cloudflare Access.",
|
||||||
|
"Please use this option carefully!": "Por favor, utiliza esta opção com cuidado.",
|
||||||
|
Logout: "Logout",
|
||||||
|
Leave: "Sair",
|
||||||
|
"I understand, please disable": "Eu entendo, por favor desativa.",
|
||||||
|
Confirm: "Confirmar",
|
||||||
|
Yes: "Sim",
|
||||||
|
No: "Não",
|
||||||
|
Username: "Utilizador",
|
||||||
|
Password: "Senha",
|
||||||
|
"Remember me": "Lembra-me",
|
||||||
|
Login: "Autenticar",
|
||||||
|
"No Monitors, please": "Nenhum monitor, por favor",
|
||||||
|
"add one": "adicionar um",
|
||||||
|
"Notification Type": "Tipo de Notificação",
|
||||||
|
Email: "Email",
|
||||||
|
Test: "Testar",
|
||||||
|
"Certificate Info": "Info. do Certificado ",
|
||||||
|
"Resolver Server": "Resolver Servidor",
|
||||||
|
"Resource Record Type": "Tipo de registro de aplicação",
|
||||||
|
"Last Result": "Último resultado",
|
||||||
|
"Create your admin account": "Cria a tua conta de admin",
|
||||||
|
"Repeat Password": "Repete a senha",
|
||||||
|
"Import Backup": "Importar Backup",
|
||||||
|
"Export Backup": "Exportar Backup",
|
||||||
|
Export: "Exportar",
|
||||||
|
Import: "Importar",
|
||||||
|
respTime: "Tempo de Resp. (ms)",
|
||||||
|
notAvailableShort: "N/A",
|
||||||
|
"Default enabled": "Padrão habilitado",
|
||||||
|
"Apply on all existing monitors": "Aplicar em todos os monitores existentes",
|
||||||
|
Create: "Criar",
|
||||||
|
"Clear Data": "Limpar Dados",
|
||||||
|
Events: "Eventos",
|
||||||
|
Heartbeats: "Heartbeats",
|
||||||
|
"Auto Get": "Obter Automático",
|
||||||
|
backupDescription: "Podes fazer backup de todos os monitores e todas as notificações num arquivo JSON.",
|
||||||
|
backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.",
|
||||||
|
backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantem-no com cuidado.",
|
||||||
|
alertNoFile: "Seleciona um arquivo para importar.",
|
||||||
|
alertWrongFileType: "Seleciona um arquivo JSON.",
|
||||||
|
"Clear all statistics": "Limpar todas as estatísticas",
|
||||||
|
"Skip existing": "Saltar existente",
|
||||||
|
Overwrite: "Sobrescrever",
|
||||||
|
Options: "Opções",
|
||||||
|
"Keep both": "Manter os dois",
|
||||||
|
"Verify Token": "Verificar Token",
|
||||||
|
"Setup 2FA": "Configurar 2FA",
|
||||||
|
"Enable 2FA": "Ativar 2FA",
|
||||||
|
"Disable 2FA": "Desativar 2FA",
|
||||||
|
"2FA Settings": "Configurações do 2FA ",
|
||||||
|
"Two Factor Authentication": "Autenticação de Dois Fatores",
|
||||||
|
Active: "Ativo",
|
||||||
|
Inactive: "Inativo",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "Mostrar URI",
|
||||||
|
Tags: "Tag",
|
||||||
|
"Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...",
|
||||||
|
"Tag with this name already exist.": "Já existe uma etiqueta com este nome.",
|
||||||
|
"Tag with this value already exist.": "Já existe uma etiqueta com este valor.",
|
||||||
|
color: "cor",
|
||||||
|
"value (optional)": "valor (opcional)",
|
||||||
|
Gray: "Cinza",
|
||||||
|
Red: "Vermelho",
|
||||||
|
Orange: "Laranja",
|
||||||
|
Green: "Verde",
|
||||||
|
Blue: "Azul",
|
||||||
|
Indigo: "Índigo",
|
||||||
|
Purple: "Roxo",
|
||||||
|
Pink: "Rosa",
|
||||||
|
"Search...": "Pesquisa...",
|
||||||
|
"Avg. Ping": "Ping Médio.",
|
||||||
|
"Avg. Response": "Resposta Média. ",
|
||||||
|
"Status Page": "Página de Status",
|
||||||
|
"Status Pages": "Página de Status",
|
||||||
|
"Entry Page": "Página de entrada",
|
||||||
|
statusPageNothing: "Nada aqui, por favor, adiciona um grupo ou monitor.",
|
||||||
|
"No Services": "Nenhum Serviço",
|
||||||
|
"All Systems Operational": "Todos os Serviços Operacionais",
|
||||||
|
"Partially Degraded Service": "Serviço parcialmente degradados",
|
||||||
|
"Degraded Service": "Serviço Degradado",
|
||||||
|
"Add Group": "Adicionar Grupo",
|
||||||
|
"Add a monitor": "Adicionar um monitor",
|
||||||
|
"Edit Status Page": "Editar Página de Status",
|
||||||
|
"Go to Dashboard": "Ir para o dashboard",
|
||||||
|
telegram: "Telegram",
|
||||||
|
webhook: "Webhook",
|
||||||
|
smtp: "Email (SMTP)",
|
||||||
|
discord: "Discord",
|
||||||
|
teams: "Microsoft Teams",
|
||||||
|
signal: "Signal",
|
||||||
|
gotify: "Gotify",
|
||||||
|
slack: "Slack",
|
||||||
|
"rocket.chat": "Rocket.chat",
|
||||||
|
pushover: "Pushover",
|
||||||
|
pushy: "Pushy",
|
||||||
|
octopush: "Octopush",
|
||||||
|
promosms: "PromoSMS",
|
||||||
|
lunasea: "LunaSea",
|
||||||
|
apprise: "Apprise (Support 50+ Notification services)",
|
||||||
|
pushbullet: "Pushbullet",
|
||||||
|
line: "Line Messenger",
|
||||||
|
mattermost: "Mattermost",
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
const actor = require("../support/actors/actor");
|
||||||
|
const userData = require("../support/const/user-data");
|
||||||
|
const dashboardPage = require("../support/pages/dashboard-page");
|
||||||
|
const setupPage = require("../support/pages/setup-page");
|
||||||
|
|
||||||
|
describe("user can create a new account on setup page", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit("/setup");
|
||||||
|
});
|
||||||
|
it("user can create new account", () => {
|
||||||
|
cy.url().should("be.equal", setupPage.SetupPage.url);
|
||||||
|
actor.actor.setupTask.fillAndSubmitSetupForm(userData.DEFAULT_USER_DATA.username, userData.DEFAULT_USER_DATA.password, userData.DEFAULT_USER_DATA.password);
|
||||||
|
cy.url().should("be.equal", dashboardPage.DashboardPage.url);
|
||||||
|
cy.get('[role="alert"]')
|
||||||
|
.should("be.visible")
|
||||||
|
.and("contain.text", "Added Successfully.");
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,8 @@
|
|||||||
|
const setupTask = require("../tasks/setup-task");
|
||||||
|
class Actor {
|
||||||
|
constructor() {
|
||||||
|
this.setupTask = new setupTask.SetupTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const actor = new Actor();
|
||||||
|
exports.actor = actor;
|
@ -0,0 +1,4 @@
|
|||||||
|
exports.DEFAULT_USER_DATA = {
|
||||||
|
username: "testuser",
|
||||||
|
password: "testuser123",
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
require("./commands");
|
@ -0,0 +1,3 @@
|
|||||||
|
exports.DashboardPage = {
|
||||||
|
url: Cypress.env("baseUrl") + "/dashboard",
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
exports.SetupPage = {
|
||||||
|
url: Cypress.env("baseUrl") + "/setup",
|
||||||
|
usernameInput: '[data-cy="username-input"]',
|
||||||
|
passWordInput: '[data-cy="password-input"]',
|
||||||
|
passwordRepeatInput: '[data-cy="password-repeat-input"]',
|
||||||
|
submitSetupForm: '[data-cy="submit-setup-form"]',
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
const setupPage = require("../pages/setup-page");
|
||||||
|
|
||||||
|
class SetupTask {
|
||||||
|
fillAndSubmitSetupForm(username, password, passwordRepeat) {
|
||||||
|
cy.get(setupPage.SetupPage.usernameInput).type(username);
|
||||||
|
cy.get(setupPage.SetupPage.passWordInput).type(password);
|
||||||
|
cy.get(setupPage.SetupPage.passwordRepeatInput).type(passwordRepeat);
|
||||||
|
cy.get(setupPage.SetupPage.submitSetupForm).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.SetupTask = SetupTask;
|
@ -1,329 +0,0 @@
|
|||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { Page, Browser } = require("puppeteer");
|
|
||||||
const { sleep } = require("../src/util");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set back the correct data type for page object
|
|
||||||
* @type {Page}
|
|
||||||
*/
|
|
||||||
page;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Browser}
|
|
||||||
*/
|
|
||||||
browser;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await page.setViewport({
|
|
||||||
width: 1280,
|
|
||||||
height: 720,
|
|
||||||
deviceScaleFactor: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseURL = "http://127.0.0.1:3002";
|
|
||||||
|
|
||||||
describe("Init", () => {
|
|
||||||
const title = "Uptime Kuma";
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await page.goto(baseURL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should be titled "${title}"`, async () => {
|
|
||||||
await expect(page.title()).resolves.toEqual(title);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup Page
|
|
||||||
it("Setup", async () => {
|
|
||||||
// Create an Admin
|
|
||||||
await page.waitForSelector("#floatingInput");
|
|
||||||
await page.waitForSelector("#repeat");
|
|
||||||
await page.click("#floatingInput");
|
|
||||||
await page.type("#floatingInput", "admin");
|
|
||||||
await page.type("#floatingPassword", "admin123");
|
|
||||||
await page.type("#repeat", "admin123");
|
|
||||||
await page.click(".btn-primary[type=submit]");
|
|
||||||
await sleep(3000);
|
|
||||||
|
|
||||||
// Go to /setup again
|
|
||||||
await page.goto(baseURL + "/setup");
|
|
||||||
await sleep(3000);
|
|
||||||
let pathname = await page.evaluate(() => location.pathname);
|
|
||||||
expect(pathname).toEqual("/dashboard");
|
|
||||||
|
|
||||||
// Go to /
|
|
||||||
await page.goto(baseURL);
|
|
||||||
await page.waitForSelector("h1.mb-3");
|
|
||||||
pathname = await page.evaluate(() => location.pathname);
|
|
||||||
expect(pathname).toEqual("/dashboard");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create monitor", async () => {
|
|
||||||
// Create monitor
|
|
||||||
await page.goto(baseURL + "/add");
|
|
||||||
await page.waitForSelector("#name");
|
|
||||||
|
|
||||||
await page.type("#name", "Myself");
|
|
||||||
await page.waitForSelector("#url");
|
|
||||||
await page.click("#url", { clickCount: 3 });
|
|
||||||
await page.keyboard.type(baseURL);
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
|
|
||||||
await page.waitForFunction(() => {
|
|
||||||
const badge = document.querySelector("span.badge");
|
|
||||||
return badge && badge.innerText == "100%";
|
|
||||||
}, { timeout: 5000 });
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Settings Page
|
|
||||||
/*
|
|
||||||
describe("Settings", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await page.goto(baseURL + "/settings");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Change Language", async () => {
|
|
||||||
await page.goto(baseURL + "/settings/appearance");
|
|
||||||
await page.waitForSelector("#language");
|
|
||||||
|
|
||||||
await page.select("#language", "zh-HK");
|
|
||||||
let languageTitle = await page.evaluate(() => document.querySelector("[for=language]").innerText);
|
|
||||||
expect(languageTitle).toEqual("語言");
|
|
||||||
|
|
||||||
await page.select("#language", "en");
|
|
||||||
languageTitle = await page.evaluate(() => document.querySelector("[for=language]").innerText);
|
|
||||||
expect(languageTitle).toEqual("Language");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Change Theme", async () => {
|
|
||||||
await page.goto(baseURL + "/settings/appearance");
|
|
||||||
|
|
||||||
// Dark
|
|
||||||
await click(page, ".btn[for=btncheck2]");
|
|
||||||
await page.waitForSelector("div.dark");
|
|
||||||
|
|
||||||
await page.waitForSelector(".btn[for=btncheck1]");
|
|
||||||
|
|
||||||
// Light
|
|
||||||
await click(page, ".btn[for=btncheck1]");
|
|
||||||
await page.waitForSelector("div.light");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Change Heartbeat Bar Style", async () => {
|
|
||||||
await page.goto(baseURL + "/settings/appearance");
|
|
||||||
|
|
||||||
// Bottom
|
|
||||||
await click(page, ".btn[for=btncheck5]");
|
|
||||||
await page.waitForSelector("div.hp-bar-big");
|
|
||||||
|
|
||||||
// None
|
|
||||||
await click(page, ".btn[for=btncheck6]");
|
|
||||||
await page.waitForSelector("div.hp-bar-big", {
|
|
||||||
hidden: true,
|
|
||||||
timeout: 1000
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Timezone
|
|
||||||
|
|
||||||
it("Search Engine Visibility", async () => {
|
|
||||||
// Default
|
|
||||||
let res = await axios.get(baseURL + "/robots.txt");
|
|
||||||
expect(res.data).toContain("Disallow: /");
|
|
||||||
|
|
||||||
// Yes
|
|
||||||
await click(page, "#searchEngineIndexYes");
|
|
||||||
await click(page, "form > div > .btn[type=submit]");
|
|
||||||
await sleep(1000);
|
|
||||||
res = await axios.get(baseURL + "/robots.txt");
|
|
||||||
expect(res.data).not.toContain("Disallow: /");
|
|
||||||
|
|
||||||
// No
|
|
||||||
await click(page, "#searchEngineIndexNo");
|
|
||||||
await click(page, "form > div > .btn[type=submit]");
|
|
||||||
await sleep(1000);
|
|
||||||
res = await axios.get(baseURL + "/robots.txt");
|
|
||||||
expect(res.data).toContain("Disallow: /");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Entry Page", async () => {
|
|
||||||
const newPage = await browser.newPage();
|
|
||||||
|
|
||||||
// Default
|
|
||||||
await newPage.goto(baseURL);
|
|
||||||
await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
|
|
||||||
let pathname = await newPage.evaluate(() => location.pathname);
|
|
||||||
expect(pathname).toEqual("/dashboard");
|
|
||||||
|
|
||||||
// Status Page
|
|
||||||
await click(page, "#entryPageNo");
|
|
||||||
await click(page, "form > div > .btn[type=submit]");
|
|
||||||
await sleep(1000);
|
|
||||||
await newPage.goto(baseURL);
|
|
||||||
await newPage.waitForSelector("img.logo", { timeout: 3000 });
|
|
||||||
pathname = await newPage.evaluate(() => location.pathname);
|
|
||||||
expect(pathname).toEqual("/status");
|
|
||||||
|
|
||||||
// Back to Dashboard
|
|
||||||
await click(page, "#entryPageYes");
|
|
||||||
await click(page, "form > div > .btn[type=submit]");
|
|
||||||
await sleep(1000);
|
|
||||||
await newPage.goto(baseURL);
|
|
||||||
await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
|
|
||||||
pathname = await newPage.evaluate(() => location.pathname);
|
|
||||||
expect(pathname).toEqual("/dashboard");
|
|
||||||
|
|
||||||
await newPage.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Change Password (wrong current password)", async () => {
|
|
||||||
await page.goto(baseURL + "/settings/security");
|
|
||||||
await page.waitForSelector("#current-password");
|
|
||||||
|
|
||||||
await page.type("#current-password", "wrong_passw$$d");
|
|
||||||
await page.type("#new-password", "new_password123");
|
|
||||||
await page.type("#repeat-new-password", "new_password123");
|
|
||||||
|
|
||||||
// Save
|
|
||||||
await click(page, "form > div > .btn[type=submit]", 0);
|
|
||||||
await sleep(1000);
|
|
||||||
|
|
||||||
await click(page, "#logout-btn");
|
|
||||||
await login("admin", "new_password123");
|
|
||||||
let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
|
|
||||||
expect(elementCount).toEqual(1);
|
|
||||||
|
|
||||||
await login("admin", "admin123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Change Password (wrong repeat)", async () => {
|
|
||||||
await page.goto(baseURL + "/settings/security");
|
|
||||||
await page.waitForSelector("#current-password");
|
|
||||||
|
|
||||||
await page.type("#current-password", "admin123");
|
|
||||||
await page.type("#new-password", "new_password123");
|
|
||||||
await page.type("#repeat-new-password", "new_password1234567898797898");
|
|
||||||
|
|
||||||
await click(page, "form > div > .btn[type=submit]", 0);
|
|
||||||
await sleep(1000);
|
|
||||||
|
|
||||||
await click(page, "#logout-btn");
|
|
||||||
await login("admin", "new_password123");
|
|
||||||
|
|
||||||
let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
|
|
||||||
expect(elementCount).toEqual(1);
|
|
||||||
|
|
||||||
await login("admin", "admin123");
|
|
||||||
await page.waitForSelector("#current-password");
|
|
||||||
let pathname = await page.evaluate(() => location.pathname);
|
|
||||||
expect(pathname).toEqual("/settings/security");
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: 2FA
|
|
||||||
|
|
||||||
// TODO: Export Backup
|
|
||||||
|
|
||||||
// TODO: Import Backup
|
|
||||||
|
|
||||||
it("Should disable & enable auth", async () => {
|
|
||||||
await page.goto(baseURL + "/settings/security");
|
|
||||||
await click(page, "#disableAuth-btn");
|
|
||||||
await click(page, ".btn.btn-danger[data-bs-dismiss='modal']", 2); // Not a good way to do it
|
|
||||||
await page.waitForSelector("#enableAuth-btn", { timeout: 3000 });
|
|
||||||
await page.waitForSelector("#logout-btn", {
|
|
||||||
hidden: true,
|
|
||||||
timeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPage = await browser.newPage();
|
|
||||||
await newPage.goto(baseURL);
|
|
||||||
await newPage.waitForSelector("span.badge", { timeout: 3000 });
|
|
||||||
newPage.close();
|
|
||||||
|
|
||||||
await click(page, "#enableAuth-btn");
|
|
||||||
await login("admin", "admin123");
|
|
||||||
await page.waitForSelector("#disableAuth-btn", { timeout: 3000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
// it("Should clear all statistics", async () => {
|
|
||||||
// await page.goto(baseURL + "/settings/monitor-history");
|
|
||||||
// await click(page, "#clearAllStats-btn");
|
|
||||||
// await click(page, ".btn.btn-danger");
|
|
||||||
// await page.waitForFunction(() => {
|
|
||||||
// const badge = document.querySelector("span.badge");
|
|
||||||
// return badge && badge.innerText == "0%";
|
|
||||||
// }, { timeout: 3000 });
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO
|
|
||||||
* Create Monitor - All type
|
|
||||||
* Edit Monitor
|
|
||||||
* Delete Monitor
|
|
||||||
*
|
|
||||||
* Create Notification (token problem, maybe hard to test)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe("Status Page", () => {
|
|
||||||
const title = "Uptime Kuma";
|
|
||||||
beforeAll(async () => {
|
|
||||||
await page.goto(baseURL + "/status");
|
|
||||||
});
|
|
||||||
it(`should be titled "${title}"`, async () => {
|
|
||||||
await expect(page.title()).resolves.toEqual(title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test login
|
|
||||||
* @param {string} username
|
|
||||||
* @param {string} password
|
|
||||||
*/
|
|
||||||
async function login(username, password) {
|
|
||||||
await input(page, "#floatingInput", username);
|
|
||||||
await input(page, "#floatingPassword", password);
|
|
||||||
await page.click(".btn-primary[type=submit]");
|
|
||||||
await sleep(5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click on an element on the page
|
|
||||||
* @param {Page} page Puppeteer page instance
|
|
||||||
* @param {string} selector
|
|
||||||
* @param {number} elementIndex
|
|
||||||
* @returns {Promise<any>}
|
|
||||||
*/
|
|
||||||
async function click(page, selector, elementIndex = 0) {
|
|
||||||
await page.waitForSelector(selector, {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
return await page.evaluate((s, i) => {
|
|
||||||
return document.querySelectorAll(s)[i].click();
|
|
||||||
}, selector, elementIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Input text into selected field
|
|
||||||
* @param {Page} page Puppeteer page instance
|
|
||||||
* @param {string} selector
|
|
||||||
* @param {string} text Text to input
|
|
||||||
*/
|
|
||||||
async function input(page, selector, text) {
|
|
||||||
await page.waitForSelector(selector, {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
const element = await page.$(selector);
|
|
||||||
await element.click({ clickCount: 3 });
|
|
||||||
await page.keyboard.press("Backspace");
|
|
||||||
await page.type(selector, text);
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// eslint-disable-next-line no-global-assign
|
|
||||||
global.localStorage = {};
|
|
||||||
global.navigator = {
|
|
||||||
language: "en"
|
|
||||||
};
|
|
||||||
|
|
||||||
const { currentLocale } = require("../src/i18n");
|
|
||||||
|
|
||||||
describe("Test i18n.js", () => {
|
|
||||||
|
|
||||||
it("currentLocale()", () => {
|
|
||||||
expect(currentLocale()).toEqual("en");
|
|
||||||
|
|
||||||
navigator.language = "zh-HK";
|
|
||||||
expect(currentLocale()).toEqual("zh-HK");
|
|
||||||
|
|
||||||
// Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
|
|
||||||
navigator.language = "zh-hk";
|
|
||||||
expect(currentLocale()).toEqual("en");
|
|
||||||
|
|
||||||
navigator.language = "en-US";
|
|
||||||
expect(currentLocale()).toEqual("en");
|
|
||||||
|
|
||||||
navigator.language = "ja-ZZ";
|
|
||||||
expect(currentLocale()).toEqual("ja");
|
|
||||||
|
|
||||||
navigator.language = "zz";
|
|
||||||
expect(currentLocale()).toEqual("en");
|
|
||||||
|
|
||||||
navigator.language = "zz-ZZ";
|
|
||||||
expect(currentLocale()).toEqual("en");
|
|
||||||
|
|
||||||
localStorage.locale = "en";
|
|
||||||
expect(currentLocale()).toEqual("en");
|
|
||||||
|
|
||||||
localStorage.locale = "zh-HK";
|
|
||||||
expect(currentLocale()).toEqual("zh-HK");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue