Merge branch 'louislam:master' into group-monitors

This commit is contained in:
Peace 2023-02-01 20:44:09 +01:00 committed by GitHub
commit 2c581ade90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 156 additions and 51 deletions

View File

@ -17,8 +17,11 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Directories ## Directories
- config (dev config files)
- data (App data) - data (App data)
- db (Base database and migration scripts)
- dist (Frontend build) - dist (Frontend build)
- docker (Dockerfiles)
- extra (Extra useful scripts) - extra (Extra useful scripts)
- public (Frontend resources for dev only) - public (Frontend resources for dev only)
- server (Server source code) - server (Server source code)
@ -80,13 +83,13 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
## Project Styles ## Project Styles
I personally do not like it when something requires so much learning and configuration before you can finally start the app. I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation could be as easy as like installing a mobile app.
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort required to get it running - Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, no extra effort required to get it running
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go - Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`. - Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`
- Easy to use - Easy to use
- The web UI styling should be consistent and nice. - The web UI styling should be consistent and nice
## Coding Styles ## Coding Styles
@ -95,7 +98,7 @@ I personally do not like it when something requires so much learning and configu
- Follow ESLint - Follow ESLint
- Methods and functions should be documented with JSDoc - Methods and functions should be documented with JSDoc
## Name convention ## Name Conventions
- Javascript/Typescript: camelCaseType - Javascript/Typescript: camelCaseType
- SQLite: snake_case (Underscore) - SQLite: snake_case (Underscore)
@ -109,7 +112,7 @@ I personally do not like it when something requires so much learning and configu
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA) - IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
- A SQLite GUI tool (SQLite Expert Personal is suggested) - A SQLite GUI tool (SQLite Expert Personal is suggested)
## Install dependencies ## Install Dependencies for Development
```bash ```bash
npm ci npm ci
@ -127,6 +130,12 @@ Port `3000` and port `3001` will be used.
npm run dev npm run dev
``` ```
But sometimes, you would like to keep restart the server, but not the frontend, you can run these command in two terminals:
```
npm run start-frontend-dev
npm run start-server-dev
```
## Backend Server ## Backend Server
It binds to `0.0.0.0:3001` by default. It binds to `0.0.0.0:3001` by default.
@ -142,12 +151,15 @@ express.js is used for:
### Structure in /server/ ### Structure in /server/
- jobs/ (Jobs that are running in another process)
- model/ (Object model, auto mapping to the database table name) - model/ (Object model, auto mapping to the database table name)
- modules/ (Modified 3rd-party modules) - modules/ (Modified 3rd-party modules)
- monitor_types (Monitor Types)
- notification-providers/ (individual notification logic) - notification-providers/ (individual notification logic)
- routers/ (Express Routers) - routers/ (Express Routers)
- socket-handler (Socket.io Handlers) - socket-handler (Socket.io Handlers)
- server.js (Server entry point and main logic) - server.js (Server entry point)
- uptime-kuma-server.js (UptimeKumaServer class, main logic should be here, but some still in `server.js`)
## Frontend Dev Server ## Frontend Dev Server
@ -198,18 +210,12 @@ Both frontend and backend share the same package.json. However, the frontend dep
### Update Dependencies ### Update Dependencies
Install `ncu`
https://github.com/raineorshine/npm-check-updates
```bash
ncu -u -t patch
npm install
```
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only. Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
Patch release = the third digit ([Semantic Versioning](https://semver.org/)) Patch release = the third digit ([Semantic Versioning](https://semver.org/))
If for maybe security reasons, a library must be updated. Then you must need to check if there are any breaking changes.
## Translations ## Translations
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages

View File

@ -3,6 +3,6 @@ FROM node:16-alpine3.12
WORKDIR /app WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv # Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
pip3 --no-cache-dir install apprise==1.2.1 && \ pip3 --no-cache-dir install apprise==1.2.1 && \
rm -rf /root/.cache rm -rf /root/.cache

View File

@ -2,7 +2,7 @@
# Build in Golang # Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck # Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
############################################ ############################################
FROM golang:1.19.4-buster FROM golang:1.19-buster
WORKDIR /app WORKDIR /app
ARG TARGETPLATFORM ARG TARGETPLATFORM
COPY ./extra/ ./extra/ COPY ./extra/ ./extra/

View File

@ -10,7 +10,7 @@ WORKDIR /app
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine! # Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
RUN apt update && \ RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \ sqlite3 iputils-ping util-linux dumb-init git && \
pip3 --no-cache-dir install apprise==1.2.1 && \ pip3 --no-cache-dir install apprise==1.2.1 && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove

View File

@ -11,12 +11,17 @@ import (
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"strings"
"time" "time"
) )
func main() { func main() {
isFreeBSD := runtime.GOOS == "freebsd" isFreeBSD := runtime.GOOS == "freebsd"
// Is K8S + uptime-kuma as the container name
// See #2083
isK8s := strings.HasPrefix(os.Getenv("UPTIME_KUMA_PORT"), "tcp://")
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
@ -44,7 +49,11 @@ func main() {
hostname = "127.0.0.1" hostname = "127.0.0.1"
} }
port := os.Getenv("UPTIME_KUMA_PORT") port := ""
// UPTIME_KUMA_PORT is override by K8S unexpectedly,
if !isK8s {
port = os.Getenv("UPTIME_KUMA_PORT")
}
if len(port) == 0 { if len(port) == 0 {
port = os.Getenv("PORT") port = os.Getenv("PORT")
} }

View File

@ -19,17 +19,17 @@ if (sslKey && sslCert) {
// 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. // 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 (::) // Dual-stack support for (::)
let hostname = process.env.UPTIME_KUMA_HOST; let hostname = process.env.UPTIME_KUMA_SERVICE_HOST || process.env.UPTIME_KUMA_HOST || "::";
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD // Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
if (!hostname && !FBSD) { if (!hostname && !FBSD) {
hostname = process.env.HOST; hostname = process.env.HOST;
} }
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001); const port = parseInt(process.env.UPTIME_KUMA_SERVICE_PORT || process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
let options = { let options = {
host: hostname || "127.0.0.1", host: hostname,
port: port, port: port,
timeout: 28 * 1000, timeout: 28 * 1000,
}; };

18
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.19.5", "version": "1.19.6",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.19.5", "version": "1.19.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@grpc/grpc-js": "~1.7.3",
@ -27,6 +27,7 @@
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "~1.7.4", "compression": "~1.7.4",
"dayjs": "~1.11.5", "dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"express": "~4.17.3", "express": "~4.17.3",
"express-basic-auth": "~1.2.1", "express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7", "express-static-gzip": "~2.1.7",
@ -7813,6 +7814,14 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/duplexify": { "node_modules/duplexify": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
@ -24881,6 +24890,11 @@
"domhandler": "^5.0.1" "domhandler": "^5.0.1"
} }
}, },
"dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
},
"duplexify": { "duplexify": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",

View File

@ -84,6 +84,7 @@
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "~1.7.4", "compression": "~1.7.4",
"dayjs": "~1.11.5", "dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"express": "~4.17.3", "express": "~4.17.3",
"express-basic-auth": "~1.2.1", "express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7", "express-static-gzip": "~2.1.7",

View File

@ -5,6 +5,8 @@ const badgeConstants = {
naColor: "#999", naColor: "#999",
defaultUpColor: "#66c20a", defaultUpColor: "#66c20a",
defaultDownColor: "#c2290a", defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue", // as defined by badge-maker / shields.io defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat", defaultStyle: "flat",
defaultPingValueSuffix: "ms", defaultPingValueSuffix: "ms",

View File

@ -72,6 +72,12 @@ class PluginsManager {
* @param {string} name Directory name, also known as plugin unique name * @param {string} name Directory name, also known as plugin unique name
*/ */
downloadPlugin(repoURL, name) { downloadPlugin(repoURL, name) {
if (fs.existsSync(this.pluginsDir + name)) {
log.info("plugin", "Plugin folder already exists? Removing...");
fs.rmSync(this.pluginsDir + name, {
recursive: true
});
}
log.info("plugin", "Installing plugin: " + name + " " + repoURL); log.info("plugin", "Installing plugin: " + name + " " + repoURL);
let result = Git.clone(repoURL, this.pluginsDir, name); let result = Git.clone(repoURL, this.pluginsDir, name);
log.info("plugin", "Install result: " + result); log.info("plugin", "Install result: " + result);
@ -115,13 +121,19 @@ class PluginsManager {
* @returns {Promise<[]>} * @returns {Promise<[]>}
*/ */
async fetchPluginList() { async fetchPluginList() {
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json"); let remotePluginList;
const list = res.data.pluginList; try {
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
remotePluginList = res.data.pluginList;
} catch (e) {
log.error("plugin", "Failed to fetch plugin list: " + e.message);
remotePluginList = [];
}
for (let plugin of this.pluginList) { for (let plugin of this.pluginList) {
let find = false; let find = false;
// Try to merge // Try to merge
for (let remotePlugin of list) { for (let remotePlugin of remotePluginList) {
if (remotePlugin.name === plugin.info.name) { if (remotePlugin.name === plugin.info.name) {
find = true; find = true;
remotePlugin.installed = true; remotePlugin.installed = true;
@ -136,17 +148,17 @@ class PluginsManager {
// Local plugin // Local plugin
if (!find) { if (!find) {
plugin.info.local = true; plugin.info.local = true;
list.push(plugin.info); remotePluginList.push(plugin.info);
} }
} }
// Sort Installed first, then sort by name // Sort Installed first, then sort by name
return list.sort((a, b) => { return remotePluginList.sort((a, b) => {
if (a.installed === b.installed) { if (a.installed === b.installed) {
if ( a.fullName < b.fullName ) { if (a.fullName < b.fullName) {
return -1; return -1;
} }
if ( a.fullName > b.fullName ) { if (a.fullName > b.fullName) {
return 1; return 1;
} }
return 0; return 0;
@ -191,15 +203,24 @@ class PluginWrapper {
let indexFile = this.pluginDir + "/index.js"; let indexFile = this.pluginDir + "/index.js";
let packageJSON = this.pluginDir + "/package.json"; let packageJSON = this.pluginDir + "/package.json";
log.info("plugin", "Installing dependencies");
if (fs.existsSync(indexFile)) { if (fs.existsSync(indexFile)) {
// Install dependencies // Install dependencies
childProcess.execSync("npm install", { let result = childProcess.spawnSync("npm", [ "install" ], {
cwd: this.pluginDir, cwd: this.pluginDir,
env: { env: {
...process.env,
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
} }
}); });
if (result.stdout) {
log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
} else {
log.warn("plugin", "Install dependencies result: no output");
}
this.pluginClass = require(path.join(process.cwd(), indexFile)); this.pluginClass = require(path.join(process.cwd(), indexFile));
let pluginClassType = typeof this.pluginClass; let pluginClassType = typeof this.pluginClass;

View File

@ -4,7 +4,7 @@ const { R } = require("redbean-node");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util"); const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page"); const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const { makeBadge } = require("badge-maker"); const { makeBadge } = require("badge-maker");
@ -111,8 +111,12 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
label, label,
upLabel = "Up", upLabel = "Up",
downLabel = "Down", downLabel = "Down",
pendingLabel = "Pending",
maintenanceLabel = "Maintenance",
upColor = badgeConstants.defaultUpColor, upColor = badgeConstants.defaultUpColor,
downColor = badgeConstants.defaultDownColor, downColor = badgeConstants.defaultDownColor,
pendingColor = badgeConstants.defaultPendingColor,
maintenanceColor = badgeConstants.defaultMaintenanceColor,
style = badgeConstants.defaultStyle, style = badgeConstants.defaultStyle,
value, // for demo purpose only value, // for demo purpose only
} = request.query; } = request.query;
@ -139,11 +143,30 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
badgeValues.color = badgeConstants.naColor; badgeValues.color = badgeConstants.naColor;
} else { } else {
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId); const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1; const state = overrideValue !== undefined ? overrideValue : heartbeat.status;
badgeValues.label = label ? label : ""; badgeValues.label = label ?? "";
badgeValues.color = state ? upColor : downColor; switch (state) {
badgeValues.message = label ?? state ? upLabel : downLabel; case DOWN:
badgeValues.color = downColor;
badgeValues.message = downLabel;
break;
case UP:
badgeValues.color = upColor;
badgeValues.message = upLabel;
break;
case PENDING:
badgeValues.color = pendingColor;
badgeValues.message = pendingLabel;
break;
case MAINTENANCE:
badgeValues.color = maintenanceColor;
badgeValues.message = maintenanceLabel;
break;
default:
badgeValues.color = badgeConstants.naColor;
badgeValues.message = "N/A";
}
} }
// build the svg based on given values // build the svg based on given values

View File

@ -11,6 +11,9 @@ dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("./modules/dayjs/plugin/timezone")); dayjs.extend(require("./modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat")); dayjs.extend(require("dayjs/plugin/customParseFormat"));
// Load environment variables from `.env`
require("dotenv").config();
// Check Node.js Version // Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]); const nodeVersion = parseInt(process.versions.node.split(".")[0]);
const requiredVersion = 14; const requiredVersion = 14;

View File

@ -1,5 +1,6 @@
const { checkLogin } = require("../util-server"); const { checkLogin } = require("../util-server");
const { PluginManager } = require("../plugins-manager"); const { PluginsManager } = require("../plugins-manager");
const { log } = require("../../src/util.js");
/** /**
* Handlers for plugins * Handlers for plugins
@ -15,7 +16,9 @@ module.exports.pluginsHandler = (socket, server) => {
try { try {
checkLogin(socket); checkLogin(socket);
if (PluginManager.disable) { log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
if (PluginsManager.disable) {
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/"); throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
} }
@ -25,6 +28,7 @@ module.exports.pluginsHandler = (socket, server) => {
pluginList, pluginList,
}); });
} catch (error) { } catch (error) {
log.warn("plugin", "Error: " + error.message);
callback({ callback({
ok: false, ok: false,
msg: error.message, msg: error.message,

View File

@ -48,7 +48,7 @@ export default {
this.remotePluginList = res.pluginList; this.remotePluginList = res.pluginList;
this.remotePluginListMsg = ""; this.remotePluginListMsg = "";
} else { } else {
this.remotePluginListMsg = this.$t("loadingError") + " " + res.message; this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg;
} }
}); });
} }

View File

@ -74,12 +74,16 @@
</option> </option>
</optgroup> </optgroup>
<!--
Hidden for now: Reason refer to Setting.vue
<optgroup :label="$t('Custom Monitor Type')"> <optgroup :label="$t('Custom Monitor Type')">
<option value="browser"> <option value="browser">
(Beta) HTTP(s) - Browser Engine (Chrome/Firefox) (Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
</option> </option>
</optgroup> </optgroup>
</select> </select>
-->
</select>
</div> </div>
<!-- Friendly Name --> <!-- Friendly Name -->
@ -425,10 +429,6 @@
<div class="my-3"> <div class="my-3">
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager> <tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
</div> </div>
<div class="mt-5 mb-1">
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -618,6 +618,10 @@
</template> </template>
</template> </template>
</div> </div>
<div class="col-md-12 mt-5 mb-1">
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
</div>
</div> </div>
</div> </div>
</form> </form>
@ -1000,6 +1004,14 @@ message HealthCheckResponse {
this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4); this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4);
} }
if (this.monitor.hostname) {
this.monitor.hostname = this.monitor.hostname.trim();
}
if (this.monitor.url) {
this.monitor.url = this.monitor.url.trim();
}
if (this.isAdd) { if (this.isAdd) {
this.$root.add(this.monitor, async (res) => { this.$root.add(this.monitor, async (res) => {

View File

@ -113,9 +113,12 @@ export default {
backup: { backup: {
title: this.$t("Backup"), title: this.$t("Backup"),
}, },
/*
Hidden for now: Unfortunately, after some test, I found that Playwright requires a lot of libraries to be installed on the Linux host in order to start Chrome or Firefox.
It will be hard to install, so I hide this feature for now. But it still accessible via URL: /settings/plugins.
plugins: { plugins: {
title: this.$tc("plugin", 2), title: this.$tc("plugin", 2),
}, },*/
about: { about: {
title: this.$t("About"), title: this.$t("About"),
}, },

View File

@ -32,7 +32,6 @@ import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue"; import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue"; import About from "./components/settings/About.vue";
const routes = [ const routes = [
{ {
path: "/", path: "/",

View File

@ -88,7 +88,7 @@ export function hostNameRegexPattern(mqtt = false) {
// mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect) // mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect)
const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?"; const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?";
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
const ipRegexPattern = `((^\\s*${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))`; const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`;
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$`; const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$`;

View File

@ -9,6 +9,10 @@ describe("Test util-frontend.js", () => {
expect(regex.test("www.test.com")).to.be.true; expect(regex.test("www.test.com")).to.be.true;
expect(regex.test("127.0.0.1")).to.be.true; expect(regex.test("127.0.0.1")).to.be.true;
expect(regex.test("192.168.1.156")).to.be.true; expect(regex.test("192.168.1.156")).to.be.true;
expect(regex.test(" 192.168.1.145")).to.be.false;
expect(regex.test("192.168.1.145 ")).to.be.false;
expect(regex.test(" fe80::3282:3ff:ae28:592")).to.be.false;
expect(regex.test("fe80::3282:3ff:ae28:592 ")).to.be.false;
["mqtt", "mqtts", "ws", "wss"].forEach(schema => { ["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
expect(regex.test(`${schema}://www.test.com`)).to.be.false; expect(regex.test(`${schema}://www.test.com`)).to.be.false;
@ -23,6 +27,10 @@ describe("Test util-frontend.js", () => {
expect(regex.test("www.test.com")).to.be.true; expect(regex.test("www.test.com")).to.be.true;
expect(regex.test("127.0.0.1")).to.be.true; expect(regex.test("127.0.0.1")).to.be.true;
expect(regex.test("192.168.1.156")).to.be.true; expect(regex.test("192.168.1.156")).to.be.true;
expect(regex.test(" 192.168.1.145")).to.be.false;
expect(regex.test("192.168.1.145 ")).to.be.false;
expect(regex.test(" fe80::3282:3ff:ae28:592")).to.be.false;
expect(regex.test("fe80::3282:3ff:ae28:592 ")).to.be.false;
["mqtt", "mqtts", "ws", "wss"].forEach(schema => { ["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
expect(regex.test(`${schema}://www.test.com`)).to.be.true; expect(regex.test(`${schema}://www.test.com`)).to.be.true;

View File

@ -6,7 +6,7 @@
"module": "commonjs", "module": "commonjs",
"lib": [ "lib": [
"es2020", "es2020",
"DOM", "DOM"
], ],
"removeComments": false, "removeComments": false,
"preserveConstEnums": true, "preserveConstEnums": true,