mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-16 17:36:47 +02:00
Merge branch 'louislam:master' into master
This commit is contained in:
commit
da3499a463
37 changed files with 4879 additions and 575 deletions
|
@ -196,14 +196,13 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
||||||
### Release Procedures
|
### Release Procedures
|
||||||
|
|
||||||
1. Draft a release note
|
1. Draft a release note
|
||||||
1. Make sure the repo is cleared
|
2. Make sure the repo is cleared
|
||||||
1. `npm run update-version 1.X.X`
|
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
1. `npm run build`
|
4. Wait until the `Press any key to continue`
|
||||||
1. `npm run build-docker`
|
5. `git push`
|
||||||
1. `git push`
|
6. Publish the release note as 1.X.X
|
||||||
1. Publish the release note as 1.X.X
|
7. Press any key to continue
|
||||||
1. `npm run upload-artifacts` with env vars VERSION=1.X.X;GITHUB_TOKEN=XXXX
|
8. SSH to demo site server and update to 1.X.X
|
||||||
1. SSH to demo site server and update to 1.X.X
|
|
||||||
|
|
||||||
Checking:
|
Checking:
|
||||||
|
|
||||||
|
@ -211,6 +210,15 @@ Checking:
|
||||||
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
|
||||||
- Try clean installation with Node.js
|
- Try clean installation with Node.js
|
||||||
|
|
||||||
|
### Release Beta Procedures
|
||||||
|
|
||||||
|
1. Draft a release note, check "This is a pre-release"
|
||||||
|
2. Make sure the repo is cleared
|
||||||
|
3. `npm run release-beta` with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
|
4. Wait until the `Press any key to continue`
|
||||||
|
5. Publish the release note as 1.X.X-beta.X
|
||||||
|
6. Press any key to continue
|
||||||
|
|
||||||
### Release Wiki
|
### Release Wiki
|
||||||
|
|
||||||
#### Setup Repo
|
#### Setup Repo
|
||||||
|
|
|
@ -61,8 +61,14 @@ npm run setup
|
||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (Recommended) Option 2. Run in background using PM2
|
||||||
# Install PM2 if you don't have it: npm install pm2 -g
|
# Install PM2 if you don't have it:
|
||||||
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
|
|
||||||
|
# Start Server
|
||||||
pm2 start server/server.js --name uptime-kuma
|
pm2 start server/server.js --name uptime-kuma
|
||||||
|
|
||||||
|
# If you want to see the current console output
|
||||||
|
pm2 monit
|
||||||
```
|
```
|
||||||
|
|
||||||
Browse to http://localhost:3001 after starting.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
# If the image changed, the second stage image should be changed too
|
# If the image changed, the second stage image should be changed too
|
||||||
FROM node:16-buster-slim
|
FROM node:16-buster-slim
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Curl
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
# 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 && \
|
||||||
|
@ -10,3 +13,14 @@ RUN apt update && \
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise==0.9.7 && \
|
pip3 --no-cache-dir install apprise==0.9.7 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install cloudflared
|
||||||
|
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||||
|
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
|
||||||
|
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||||
|
dpkg --add-architecture arm && \
|
||||||
|
apt update && \
|
||||||
|
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
rm -f cloudflared.deb
|
||||||
|
|
||||||
|
|
76
extra/beta/update-version.js
Normal file
76
extra/beta/update-version.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
const pkg = require("../../package.json");
|
||||||
|
const fs = require("fs");
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const util = require("../../src/util");
|
||||||
|
|
||||||
|
util.polyfill();
|
||||||
|
|
||||||
|
const oldVersion = pkg.version;
|
||||||
|
const version = process.env.VERSION;
|
||||||
|
|
||||||
|
console.log("Beta Version: " + version);
|
||||||
|
|
||||||
|
if (!oldVersion || oldVersion.includes("-beta.")) {
|
||||||
|
console.error("Error: old version should not be a beta version?");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!version || !version.includes("-beta.")) {
|
||||||
|
console.error("invalid version, beta version only");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = tagExists(version);
|
||||||
|
|
||||||
|
if (! exists) {
|
||||||
|
// Process package.json
|
||||||
|
pkg.version = version;
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
commit(version);
|
||||||
|
tag(version);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log("version tag exists, please delete the tag or use another tag");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit(version) {
|
||||||
|
let msg = "Update to " + version;
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
|
let stdout = res.stdout.toString().trim();
|
||||||
|
console.log(stdout);
|
||||||
|
|
||||||
|
if (stdout.includes("no changes added to commit")) {
|
||||||
|
throw new Error("commit error");
|
||||||
|
}
|
||||||
|
|
||||||
|
res = child_process.spawnSync("git", ["push", "origin", "master"]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag(version) {
|
||||||
|
let res = child_process.spawnSync("git", ["tag", version]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
|
||||||
|
res = child_process.spawnSync("git", ["push", "origin", version]);
|
||||||
|
console.log(res.stdout.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagExists(version) {
|
||||||
|
if (! version) {
|
||||||
|
throw new Error("invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||||
|
|
||||||
|
return res.stdout.toString().trim() === version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmdirSync(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
44
extra/download-cloudflared.js
Normal file
44
extra/download-cloudflared.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
//
|
||||||
|
|
||||||
|
const http = require("https"); // or 'https' for https:// URLs
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let arch = null;
|
||||||
|
|
||||||
|
if (platform === "linux/amd64") {
|
||||||
|
arch = "amd64";
|
||||||
|
} else if (platform === "linux/arm64") {
|
||||||
|
arch = "arm64";
|
||||||
|
} else if (platform === "linux/arm/v7") {
|
||||||
|
arch = "arm";
|
||||||
|
} else {
|
||||||
|
console.error("Invalid platform?? " + platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.createWriteStream("cloudflared.deb");
|
||||||
|
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||||
|
|
||||||
|
function get(url) {
|
||||||
|
http.get(url, function (res) {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
console.log("Redirect to " + res.headers.location);
|
||||||
|
get(res.headers.location);
|
||||||
|
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
res.pipe(file);
|
||||||
|
|
||||||
|
res.on("end", function () {
|
||||||
|
console.log("Downloaded");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(res.statusCode);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
19
extra/env2arg.js
Normal file
19
extra/env2arg.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
let env = process.env;
|
||||||
|
|
||||||
|
let cmd = process.argv[2];
|
||||||
|
let args = process.argv.slice(3);
|
||||||
|
let replacedArgs = [];
|
||||||
|
|
||||||
|
for (let arg of args) {
|
||||||
|
for (let key in env) {
|
||||||
|
arg = arg.replaceAll(`$${key}`, env[key]);
|
||||||
|
}
|
||||||
|
replacedArgs.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = childProcess.spawn(cmd, replacedArgs);
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
child.stderr.pipe(process.stderr);
|
|
@ -189,7 +189,7 @@ if (type == "local") {
|
||||||
bash("check=$(pm2 --version)");
|
bash("check=$(pm2 --version)");
|
||||||
if (check == "") {
|
if (check == "") {
|
||||||
println("Installing PM2");
|
println("Installing PM2");
|
||||||
bash("npm install pm2 -g");
|
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
||||||
bash("pm2 startup");
|
bash("pm2 startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6
extra/press-any-key.js
Normal file
6
extra/press-any-key.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on("data", process.exit.bind(process, 0));
|
||||||
|
|
|
@ -5,10 +5,8 @@ const util = require("../src/util");
|
||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const oldVersion = pkg.version;
|
const newVersion = process.env.VERSION;
|
||||||
const newVersion = process.argv[2];
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion);
|
|
||||||
console.log("New Version: " + newVersion);
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
if (! newVersion) {
|
if (! newVersion) {
|
||||||
|
@ -22,23 +20,20 @@ if (! exists) {
|
||||||
|
|
||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = newVersion;
|
pkg.version = newVersion;
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
commit(newVersion);
|
commit(newVersion);
|
||||||
tag(newVersion);
|
tag(newVersion);
|
||||||
|
|
||||||
updateWiki(oldVersion, newVersion);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("version exists");
|
console.log("version exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
function commit(version) {
|
function commit(version) {
|
||||||
let msg = "update to " + version;
|
let msg = "Update to " + version;
|
||||||
|
|
||||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
let stdout = res.stdout.toString().trim();
|
let stdout = res.stdout.toString().trim();
|
||||||
|
@ -64,37 +59,3 @@ function tagExists(version) {
|
||||||
return res.stdout.toString().trim() === version;
|
return res.stdout.toString().trim() === version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWiki(oldVersion, newVersion) {
|
|
||||||
const wikiDir = "./tmp/wiki";
|
|
||||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
|
||||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
|
||||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
|
||||||
fs.writeFileSync(howToUpdateFilename, content);
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["add", "-A"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Pushing to Github");
|
|
||||||
child_process.spawnSync("git", ["push"], {
|
|
||||||
cwd: wikiDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
safeDelete(wikiDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDelete(dir) {
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmdirSync(dir, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
48
extra/update-wiki-version.js
Normal file
48
extra/update-wiki-version.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const newVersion = process.env.VERSION;
|
||||||
|
|
||||||
|
if (!newVersion) {
|
||||||
|
console.log("Missing version");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWiki(newVersion);
|
||||||
|
|
||||||
|
function updateWiki(newVersion) {
|
||||||
|
const wikiDir = "./tmp/wiki";
|
||||||
|
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||||
|
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||||
|
|
||||||
|
// Replace the version: https://regex101.com/r/hmj2Bc/1
|
||||||
|
content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
|
||||||
|
fs.writeFileSync(howToUpdateFilename, content);
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["add", "-A"], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Pushing to Github");
|
||||||
|
child_process.spawnSync("git", ["push"], {
|
||||||
|
cwd: wikiDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
safeDelete(wikiDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDelete(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmdirSync(dir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -159,7 +159,7 @@ fi
|
||||||
check=$(pm2 --version)
|
check=$(pm2 --version)
|
||||||
if [ "$check" == "" ]; then
|
if [ "$check" == "" ]; then
|
||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
|
|
4185
package-lock.json
generated
4185
package-lock.json
generated
File diff suppressed because it is too large
Load diff
44
package.json
44
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.12.1",
|
"version": "1.14.0-beta.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -30,15 +30,14 @@
|
||||||
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
||||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.12.1-alpine --target release . --push",
|
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||||
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.12.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.12.1-debian --target release . --push",
|
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.12.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.13.1 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"update-version": "node extra/update-version.js",
|
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
"remove-2fa": "node extra/remove-2fa.js",
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
|
@ -51,7 +50,10 @@
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
|
||||||
"ncu-patch": "ncu -u -t patch"
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
|
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
|
"git-remove-tag": "git tag -d"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
|
@ -61,33 +63,34 @@
|
||||||
"@louislam/sqlite3": "~6.0.1",
|
"@louislam/sqlite3": "~6.0.1",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.26.0",
|
"axios": "~0.26.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"bree": "~7.1.0",
|
"bree": "~7.1.5",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"chart.js": "~3.6.0",
|
"chart.js": "~3.6.2",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"check-password-strength": "^2.0.5",
|
"check-password-strength": "^2.0.5",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.7",
|
"dayjs": "~1.10.8",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.0",
|
"express-basic-auth": "~1.2.1",
|
||||||
"favico.js": "^0.3.10",
|
"favico.js": "^0.3.10",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.5",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"limiter": "^2.1.0",
|
"limiter": "^2.1.0",
|
||||||
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"postcss-rtlcss": "~3.4.1",
|
||||||
"postcss-scss": "~4.0.2",
|
"postcss-scss": "~4.0.3",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.0",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"qrcode": "~1.5.0",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.3",
|
"redbean-node": "0.1.3",
|
||||||
"socket.io": "~4.4.1",
|
"socket.io": "~4.4.1",
|
||||||
|
@ -105,7 +108,7 @@
|
||||||
"vue-image-crop-upload": "~3.0.3",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.12",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0"
|
||||||
},
|
},
|
||||||
|
@ -113,10 +116,10 @@
|
||||||
"@actions/github": "~5.0.0",
|
"@actions/github": "~5.0.0",
|
||||||
"@babel/eslint-parser": "~7.15.8",
|
"@babel/eslint-parser": "~7.15.8",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@types/bootstrap": "~5.1.6",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@vitejs/plugin-legacy": "~1.6.3",
|
"@vitejs/plugin-legacy": "~1.6.4",
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
"@vitejs/plugin-vue": "~1.9.4",
|
||||||
"@vue/compiler-sfc": "~3.2.22",
|
"@vue/compiler-sfc": "~3.2.31",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"core-js": "~3.18.3",
|
"core-js": "~3.18.3",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
|
@ -124,7 +127,8 @@
|
||||||
"eslint": "~7.32.0",
|
"eslint": "~7.32.0",
|
||||||
"eslint-plugin-vue": "~7.18.0",
|
"eslint-plugin-vue": "~7.18.0",
|
||||||
"jest": "~27.2.5",
|
"jest": "~27.2.5",
|
||||||
"jest-puppeteer": "~6.0.0",
|
"jest-puppeteer": "~6.0.3",
|
||||||
|
"npm-check-updates": "^12.5.4",
|
||||||
"puppeteer": "~13.1.3",
|
"puppeteer": "~13.1.3",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "~14.2.0",
|
"stylelint": "~14.2.0",
|
||||||
|
|
|
@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter");
|
||||||
* @returns {Promise<Bean|null>}
|
* @returns {Promise<Bean|null>}
|
||||||
*/
|
*/
|
||||||
exports.login = async function (username, password) {
|
exports.login = async function (username, password) {
|
||||||
|
if (typeof username !== "string" || typeof password !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
username,
|
username,
|
||||||
]);
|
]);
|
||||||
|
@ -31,10 +35,6 @@ exports.login = async function (username, password) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function myAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
setting("disableAuth").then((result) => {
|
|
||||||
if (result) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
if (pass) {
|
if (pass) {
|
||||||
|
@ -49,13 +49,20 @@ function myAuthorizer(username, password, callback) {
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.basicAuth = basicAuth({
|
exports.basicAuth = async function (req, res, next) {
|
||||||
|
const middleware = basicAuth({
|
||||||
authorizer: myAuthorizer,
|
authorizer: myAuthorizer,
|
||||||
authorizeAsync: true,
|
authorizeAsync: true,
|
||||||
challenge: true,
|
challenge: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
|
||||||
|
if (!disabledAuth) {
|
||||||
|
middleware(req, res, next);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -218,6 +218,10 @@ class Database {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async migrateNewStatusPage() {
|
static async migrateNewStatusPage() {
|
||||||
|
|
||||||
|
// Fix 1.13.0 empty slug bug
|
||||||
|
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||||
|
|
||||||
let title = await setting("title");
|
let title = await setting("title");
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
|
|
|
@ -477,6 +477,12 @@ class Monitor extends BeanModel {
|
||||||
stop() {
|
stop() {
|
||||||
clearTimeout(this.heartbeatInterval);
|
clearTimeout(this.heartbeatInterval);
|
||||||
this.isStop = true;
|
this.isStop = true;
|
||||||
|
|
||||||
|
this.prometheus().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
prometheus() {
|
||||||
|
return new Prometheus(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,10 +9,8 @@ class Pushover extends NotificationProvider {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||||
|
|
||||||
try {
|
|
||||||
if (heartbeatJSON == null) {
|
|
||||||
let data = {
|
let data = {
|
||||||
"message": msg,
|
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
|
||||||
"user": notification.pushoveruserkey,
|
"user": notification.pushoveruserkey,
|
||||||
"token": notification.pushoverapptoken,
|
"token": notification.pushoverapptoken,
|
||||||
"sound": notification.pushoversounds,
|
"sound": notification.pushoversounds,
|
||||||
|
@ -22,23 +20,20 @@ class Pushover extends NotificationProvider {
|
||||||
"expire": "3600",
|
"expire": "3600",
|
||||||
"html": 1,
|
"html": 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (notification.pushoverdevice) {
|
||||||
|
data.device = notification.pushoverdevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
await axios.post(pushoverlink, data);
|
||||||
|
return okMsg;
|
||||||
|
} else {
|
||||||
|
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||||
await axios.post(pushoverlink, data);
|
await axios.post(pushoverlink, data);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = {
|
|
||||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
|
||||||
"user": notification.pushoveruserkey,
|
|
||||||
"token": notification.pushoverapptoken,
|
|
||||||
"sound": notification.pushoversounds,
|
|
||||||
"priority": notification.pushoverpriority,
|
|
||||||
"title": notification.pushovertitle,
|
|
||||||
"retry": "30",
|
|
||||||
"expire": "3600",
|
|
||||||
"html": 1,
|
|
||||||
};
|
|
||||||
await axios.post(pushoverlink, data);
|
|
||||||
return okMsg;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error);
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,16 @@ class Prometheus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
try {
|
||||||
|
monitor_cert_days_remaining.remove(this.monitorLabelValues);
|
||||||
|
monitor_cert_is_valid.remove(this.monitorLabelValues);
|
||||||
|
monitor_response_time.remove(this.monitorLabelValues);
|
||||||
|
monitor_status.remove(this.monitorLabelValues);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({
|
||||||
errorMessage: "Too frequently, try again later."
|
errorMessage: "Too frequently, try again later."
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const twoFaRateLimiter = new KumaRateLimiter({
|
||||||
|
tokensPerInterval: 30,
|
||||||
|
interval: "minute",
|
||||||
|
fireImmediately: true,
|
||||||
|
errorMessage: "Too frequently, try again later."
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loginRateLimiter
|
loginRateLimiter,
|
||||||
|
twoFaRateLimiter,
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,7 +52,7 @@ console.log("Importing this project modules");
|
||||||
debug("Importing Monitor");
|
debug("Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
debug("Importing Settings");
|
debug("Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||||
|
|
||||||
debug("Importing Notification");
|
debug("Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
|
@ -63,7 +63,7 @@ const Database = require("./database");
|
||||||
|
|
||||||
debug("Importing Background Jobs");
|
debug("Importing Background Jobs");
|
||||||
const { initBackgroundJobs } = require("./jobs");
|
const { initBackgroundJobs } = require("./jobs");
|
||||||
const { loginRateLimiter } = require("./rate-limiter");
|
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||||
|
|
||||||
const { basicAuth } = require("./auth");
|
const { basicAuth } = require("./auth");
|
||||||
const { login } = require("./auth");
|
const { login } = require("./auth");
|
||||||
|
@ -91,6 +91,7 @@ const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.p
|
||||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||||
|
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||||
|
|
||||||
// 2FA / notp verification defaults
|
// 2FA / notp verification defaults
|
||||||
const twofa_verification_opts = {
|
const twofa_verification_opts = {
|
||||||
|
@ -133,6 +134,7 @@ const { statusPageSocketHandler } = require("./socket-handlers/status-page-socke
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
const StatusPage = require("./model/status_page");
|
const StatusPage = require("./model/status_page");
|
||||||
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
@ -305,6 +307,15 @@ exports.entryPage = "dashboard";
|
||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
console.log("Login");
|
console.log("Login");
|
||||||
|
|
||||||
|
// Checking
|
||||||
|
if (typeof callback !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
if (! await loginRateLimiter.pass(callback)) {
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
|
@ -363,14 +374,27 @@ exports.entryPage = "dashboard";
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("logout", async (callback) => {
|
socket.on("logout", async (callback) => {
|
||||||
|
// Rate Limit
|
||||||
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.leave(socket.userID);
|
socket.leave(socket.userID);
|
||||||
socket.userID = null;
|
socket.userID = null;
|
||||||
|
|
||||||
|
if (typeof callback === "function") {
|
||||||
callback();
|
callback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("prepare2FA", async (callback) => {
|
socket.on("prepare2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
|
@ -405,14 +429,19 @@ exports.entryPage = "dashboard";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to prepare 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("save2FA", async (callback) => {
|
socket.on("save2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
|
@ -425,14 +454,19 @@ exports.entryPage = "dashboard";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to change 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disable2FA", async (callback) => {
|
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
await TwoFA.disable2FA(socket.userID);
|
await TwoFA.disable2FA(socket.userID);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
|
@ -442,12 +476,16 @@ exports.entryPage = "dashboard";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to change 2FA.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("verifyToken", async (token, callback) => {
|
socket.on("verifyToken", async (token, currentPassword, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
@ -466,12 +504,19 @@ exports.entryPage = "dashboard";
|
||||||
valid: false,
|
valid: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("twoFAStatus", async (callback) => {
|
socket.on("twoFAStatus", async (callback) => {
|
||||||
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
try {
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
@ -488,9 +533,10 @@ exports.entryPage = "dashboard";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Error while trying to get 2FA status.",
|
msg: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -579,6 +625,9 @@ exports.entryPage = "dashboard";
|
||||||
throw new Error("Permission denied.");
|
throw new Error("Permission denied.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset Prometheus labels
|
||||||
|
monitorList[monitor.id]?.prometheus()?.remove();
|
||||||
|
|
||||||
bean.name = monitor.name;
|
bean.name = monitor.name;
|
||||||
bean.type = monitor.type;
|
bean.type = monitor.type;
|
||||||
bean.url = monitor.url;
|
bean.url = monitor.url;
|
||||||
|
@ -936,21 +985,13 @@ exports.entryPage = "dashboard";
|
||||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
socket.userID,
|
await user.resetPassword(password.newPassword);
|
||||||
]);
|
|
||||||
|
|
||||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
|
||||||
|
|
||||||
user.resetPassword(password.newPassword);
|
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Password has been updated successfully.",
|
msg: "Password has been updated successfully.",
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
throw new Error("Incorrect current password");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
|
@ -977,10 +1018,14 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("setSettings", async (data, callback) => {
|
socket.on("setSettings", async (data, currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
|
if (data.disableAuth) {
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
}
|
||||||
|
|
||||||
await setSettings("general", data);
|
await setSettings("general", data);
|
||||||
exports.entryPage = data.entryPage;
|
exports.entryPage = data.entryPage;
|
||||||
|
|
||||||
|
@ -1319,6 +1364,7 @@ exports.entryPage = "dashboard";
|
||||||
|
|
||||||
// Status Page Socket Handler for admin only
|
// Status Page Socket Handler for admin only
|
||||||
statusPageSocketHandler(socket);
|
statusPageSocketHandler(socket);
|
||||||
|
cloudflaredSocketHandler(socket);
|
||||||
databaseSocketHandler(socket);
|
databaseSocketHandler(socket);
|
||||||
|
|
||||||
debug("added all socket handlers");
|
debug("added all socket handlers");
|
||||||
|
@ -1361,6 +1407,9 @@ exports.entryPage = "dashboard";
|
||||||
|
|
||||||
initBackgroundJobs(args);
|
initBackgroundJobs(args);
|
||||||
|
|
||||||
|
// Start cloudflared at the end if configured
|
||||||
|
await cloudflaredAutoStart(cloudflaredToken);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
|
@ -1404,6 +1453,8 @@ async function afterLogin(socket, user) {
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
|
await StatusPage.sendStatusPageList(io, socket);
|
||||||
|
|
||||||
for (let monitorID in monitorList) {
|
for (let monitorID in monitorList) {
|
||||||
await sendHeartbeatList(socket, monitorID);
|
await sendHeartbeatList(socket, monitorID);
|
||||||
}
|
}
|
||||||
|
@ -1415,8 +1466,6 @@ async function afterLogin(socket, user) {
|
||||||
for (let monitorID in monitorList) {
|
for (let monitorID in monitorList) {
|
||||||
await Monitor.sendStats(io, monitorID, user.id);
|
await Monitor.sendStats(io, monitorID, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await StatusPage.sendStatusPageList(io, socket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMonitorJSONList(userID) {
|
async function getMonitorJSONList(userID) {
|
||||||
|
|
84
server/socket-handlers/cloudflared-socket-handler.js
Normal file
84
server/socket-handlers/cloudflared-socket-handler.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
|
||||||
|
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
||||||
|
const { io } = require("../server");
|
||||||
|
|
||||||
|
const prefix = "cloudflared_";
|
||||||
|
const cloudflared = new CloudflaredTunnel();
|
||||||
|
|
||||||
|
cloudflared.change = (running, message) => {
|
||||||
|
io.to("cloudflared").emit(prefix + "running", running);
|
||||||
|
io.to("cloudflared").emit(prefix + "message", message);
|
||||||
|
};
|
||||||
|
|
||||||
|
cloudflared.error = (errorMessage) => {
|
||||||
|
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||||
|
|
||||||
|
socket.on(prefix + "join", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
socket.join("cloudflared");
|
||||||
|
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
|
||||||
|
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
|
||||||
|
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "leave", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
socket.leave("cloudflared");
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "start", async (token) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (token && typeof token === "string") {
|
||||||
|
cloudflared.token = token;
|
||||||
|
} else {
|
||||||
|
cloudflared.token = null;
|
||||||
|
}
|
||||||
|
cloudflared.start();
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
cloudflared.stop();
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(prefix + "removeToken", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await setSetting("cloudflaredTunnelToken", "");
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.autoStart = async (token) => {
|
||||||
|
if (!token) {
|
||||||
|
token = await setting("cloudflaredTunnelToken");
|
||||||
|
} else {
|
||||||
|
// Override the current token via args or env var
|
||||||
|
await setSetting("cloudflaredTunnelToken", token);
|
||||||
|
console.log("Use cloudflared token from args or env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
console.log("Start cloudflared");
|
||||||
|
cloudflared.token = token;
|
||||||
|
cloudflared.start();
|
||||||
|
}
|
||||||
|
};
|
|
@ -90,6 +90,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
checkSlug(config.slug);
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
@ -178,7 +180,12 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
// Delete groups that not in the list
|
// Delete groups that not in the list
|
||||||
debug("Delete groups that not in the list");
|
debug("Delete groups that not in the list");
|
||||||
const slots = groupIDList.map(() => "?").join(",");
|
const slots = groupIDList.map(() => "?").join(",");
|
||||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
|
||||||
|
const data = [
|
||||||
|
...groupIDList,
|
||||||
|
statusPage.id
|
||||||
|
];
|
||||||
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
|
||||||
|
|
||||||
// Also change entry page to new slug if it is the default one, and slug is changed.
|
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||||
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||||
|
@ -222,11 +229,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
// lower case only
|
// lower case only
|
||||||
slug = slug.toLowerCase();
|
slug = slug.toLowerCase();
|
||||||
|
|
||||||
// Check slug a-z, 0-9, - only
|
checkSlug(slug);
|
||||||
// Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
|
||||||
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
|
||||||
throw new Error("Invalid Slug");
|
|
||||||
}
|
|
||||||
|
|
||||||
let statusPage = R.dispense("status_page");
|
let statusPage = R.dispense("status_page");
|
||||||
statusPage.slug = slug;
|
statusPage.slug = slug;
|
||||||
|
@ -297,3 +300,23 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check slug a-z, 0-9, - only
|
||||||
|
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||||
|
*/
|
||||||
|
function checkSlug(slug) {
|
||||||
|
if (typeof slug !== "string") {
|
||||||
|
throw new Error("Slug must be string");
|
||||||
|
}
|
||||||
|
|
||||||
|
slug = slug.trim();
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
throw new Error("Slug cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
||||||
|
throw new Error("Invalid Slug");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
const tcpp = require("tcp-ping");
|
const tcpp = require("tcp-ping");
|
||||||
const Ping = require("./ping-lite");
|
const Ping = require("./ping-lite");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { debug } = require("../src/util");
|
const { debug, genSecret } = require("../src/util");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
|
@ -32,7 +31,7 @@ exports.initJWTSecret = async () => {
|
||||||
jwtSecretBean.key = "jwtSecret";
|
jwtSecretBean.key = "jwtSecret";
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
jwtSecretBean.value = passwordHash.generate(genSecret());
|
||||||
await R.store(jwtSecretBean);
|
await R.store(jwtSecretBean);
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
};
|
};
|
||||||
|
@ -321,6 +320,28 @@ exports.checkLogin = (socket) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For logged-in users, double-check the password
|
||||||
|
* @param socket
|
||||||
|
* @param currentPassword
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||||
|
if (typeof currentPassword !== "string") {
|
||||||
|
throw new Error("Wrong data type?");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!user || !passwordHash.verify(currentPassword, user.password)) {
|
||||||
|
throw new Error("Incorrect current password");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
exports.startUnitTest = async () => {
|
exports.startUnitTest = async () => {
|
||||||
console.log("Starting unit test...");
|
console.log("Starting unit test...");
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||||
|
|
|
@ -348,11 +348,8 @@ textarea.form-control {
|
||||||
|
|
||||||
.monitor-list {
|
.monitor-list {
|
||||||
&.scrollbar {
|
&.scrollbar {
|
||||||
min-height: calc(100vh - 240px);
|
|
||||||
max-height: calc(100vh - 30px);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: sticky;
|
height: calc(100% - 65px);
|
||||||
top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="shadow-box mb-3">
|
<div class="shadow-box mb-3" :style="boxStyle">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="placeholder"></div>
|
<div class="placeholder"></div>
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
|
@ -9,7 +9,9 @@
|
||||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||||
<font-awesome-icon icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</a>
|
</a>
|
||||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
<form>
|
||||||
|
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||||
|
@ -63,9 +65,16 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
windowTop: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
boxStyle() {
|
||||||
|
return {
|
||||||
|
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
|
||||||
|
@ -108,7 +117,20 @@ export default {
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("scroll", this.onScroll);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener("scroll", this.onScroll);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onScroll() {
|
||||||
|
if (window.top.scrollY <= 133) {
|
||||||
|
this.windowTop = window.top.scrollY;
|
||||||
|
} else {
|
||||||
|
this.windowTop = 133;
|
||||||
|
}
|
||||||
|
},
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
|
@ -122,6 +144,12 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.shadow-box {
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.small-padding {
|
.small-padding {
|
||||||
padding-left: 5px !important;
|
padding-left: 5px !important;
|
||||||
padding-right: 5px !important;
|
padding-right: 5px !important;
|
||||||
|
@ -142,6 +170,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.footer {
|
||||||
|
// background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 770px) {
|
@media (max-width: 770px) {
|
||||||
.list-header {
|
.list-header {
|
||||||
margin: -20px;
|
margin: -20px;
|
||||||
|
|
|
@ -19,6 +19,19 @@
|
||||||
</div>
|
</div>
|
||||||
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
||||||
|
|
||||||
|
<div v-if="!(uri && twoFAStatus == false)" class="mb-3">
|
||||||
|
<label for="current-password" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
||||||
{{ $t("Enable 2FA") }}
|
{{ $t("Enable 2FA") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -59,11 +72,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Modal } from "bootstrap"
|
import { Modal } from "bootstrap";
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
import VueQrcode from "vue-qrcode"
|
import VueQrcode from "vue-qrcode";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -73,35 +86,36 @@ export default {
|
||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
currentPassword: "",
|
||||||
processing: false,
|
processing: false,
|
||||||
uri: null,
|
uri: null,
|
||||||
tokenValid: false,
|
tokenValid: false,
|
||||||
twoFAStatus: null,
|
twoFAStatus: null,
|
||||||
token: null,
|
token: null,
|
||||||
showURI: false,
|
showURI: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
this.modal.show()
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmEnableTwoFA() {
|
confirmEnableTwoFA() {
|
||||||
this.$refs.confirmEnableTwoFA.show()
|
this.$refs.confirmEnableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmDisableTwoFA() {
|
confirmDisableTwoFA() {
|
||||||
this.$refs.confirmDisableTwoFA.show()
|
this.$refs.confirmDisableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
prepare2FA() {
|
prepare2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("prepare2FA", (res) => {
|
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -109,49 +123,51 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
save2FA() {
|
save2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("save2FA", (res) => {
|
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
disable2FA() {
|
disable2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.getSocket().emit("disable2FA", (res) => {
|
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
this.getStatus();
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken() {
|
verifyToken() {
|
||||||
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
|
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.tokenValid = res.valid;
|
this.tokenValid = res.valid;
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
|
@ -161,10 +177,10 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
139
src/components/settings/ReverseProxy.vue
Normal file
139
src/components/settings/ReverseProxy.vue
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4 class="mt-4">Cloudflare Tunnel</h4>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<div>
|
||||||
|
cloudflared:
|
||||||
|
<span v-if="installed === true" class="text-primary">{{ $t("Installed") }}</span>
|
||||||
|
<span v-else-if="installed === false" class="text-danger">{{ $t("Not installed") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ $t("Status") }}:
|
||||||
|
<span v-if="running" class="text-primary">{{ $t("Running") }}</span>
|
||||||
|
<span v-else-if="!running" class="text-danger">{{ $t("Not running") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="false">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="mt-3">
|
||||||
|
Message:
|
||||||
|
<textarea v-model="errorMessage" class="form-control" readonly></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="installed === false">(Download cloudflared from <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/">Cloudflare Website</a>)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If installed show token input -->
|
||||||
|
<div v-if="installed" class="mb-2">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="cloudflareTunnelToken">
|
||||||
|
Cloudflare Tunnel {{ $t("Token") }}
|
||||||
|
</label>
|
||||||
|
<HiddenInput
|
||||||
|
id="cloudflareTunnelToken"
|
||||||
|
v-model="cloudflareTunnelToken"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
:readonly="running"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
<div v-if="cloudflareTunnelToken" class="mb-3">
|
||||||
|
<span v-if="!running" class="remove-token" @click="removeToken">{{ $t("Remove Token") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Don't know how to get the token? Please read the guide:<br />
|
||||||
|
<a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel" target="_blank">
|
||||||
|
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button v-if="!running" class="btn btn-primary" type="submit" @click="start">
|
||||||
|
{{ $t("Start") }} cloudflared
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="running" class="btn btn-danger" type="submit" @click="$refs.confirmStop.show();">
|
||||||
|
{{ $t("Stop") }} cloudflared
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
|
||||||
|
The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="current-password2" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password2"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4">Other Software</h4>
|
||||||
|
<div>
|
||||||
|
For example: nginx, Apache and Traefik. <br />
|
||||||
|
Please read <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../../components/HiddenInput.vue";
|
||||||
|
import Confirm from "../Confirm.vue";
|
||||||
|
|
||||||
|
const prefix = "cloudflared_";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
Confirm
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
// See /src/mixins/socket.js
|
||||||
|
return this.$root.cloudflared;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$root.getSocket().emit(prefix + "join");
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.$root.getSocket().emit(prefix + "leave");
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
start() {
|
||||||
|
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeToken() {
|
||||||
|
this.$root.getSocket().emit(prefix + "removeToken");
|
||||||
|
this.cloudflareTunnelToken = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.remove-token {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -234,6 +234,19 @@
|
||||||
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
|
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
|
||||||
<p>Please use this option carefully!</p>
|
<p>Please use this option carefully!</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current-password2" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password2"
|
||||||
|
v-model="password.currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Confirm>
|
</Confirm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -310,7 +323,12 @@ export default {
|
||||||
|
|
||||||
disableAuth() {
|
disableAuth() {
|
||||||
this.settings.disableAuth = true;
|
this.settings.disableAuth = true;
|
||||||
this.saveSettings();
|
|
||||||
|
// Need current password to disable auth
|
||||||
|
// Set it to empty if done
|
||||||
|
this.saveSettings(() => {
|
||||||
|
this.password.currentPassword = "";
|
||||||
|
}, this.password.currentPassword);
|
||||||
},
|
},
|
||||||
|
|
||||||
enableAuth() {
|
enableAuth() {
|
||||||
|
|
|
@ -370,4 +370,5 @@ export default {
|
||||||
alertaApiKey: "API Ключ",
|
alertaApiKey: "API Ключ",
|
||||||
alertaAlertState: "Състояние на тревога",
|
alertaAlertState: "Състояние на тревога",
|
||||||
alertaRecoverState: "Състояние на възстановяване",
|
alertaRecoverState: "Състояние на възстановяване",
|
||||||
|
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
|
||||||
};
|
};
|
||||||
|
|
|
@ -183,7 +183,7 @@ export default {
|
||||||
"Edit Status Page": "Uredi Statusnu stranicu",
|
"Edit Status Page": "Uredi Statusnu stranicu",
|
||||||
"Go to Dashboard": "Na Kontrolnu ploču",
|
"Go to Dashboard": "Na Kontrolnu ploču",
|
||||||
"Status Page": "Statusna stranica",
|
"Status Page": "Statusna stranica",
|
||||||
"Status Pages": "Statusna stranica",
|
"Status Pages": "Statusne stranice",
|
||||||
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
||||||
here: "ovdje",
|
here: "ovdje",
|
||||||
Required: "Potrebno",
|
Required: "Potrebno",
|
||||||
|
@ -347,4 +347,30 @@ export default {
|
||||||
Cancel: "Otkaži",
|
Cancel: "Otkaži",
|
||||||
"Powered by": "Pokreće",
|
"Powered by": "Pokreće",
|
||||||
Saved: "Spremljeno",
|
Saved: "Spremljeno",
|
||||||
|
PushByTechulus: "Push by Techulus",
|
||||||
|
GoogleChat: "Google Chat (preko platforme Google Workspace)",
|
||||||
|
shrinkDatabaseDescription: "Pokreni VACUUM operaciju za SQLite. Ako je baza podataka kreirana nakon inačice 1.10.0, AUTO_VACUUM opcija već je uključena te ova akcija nije nužna.",
|
||||||
|
serwersms: "SerwerSMS.pl",
|
||||||
|
serwersmsAPIUser: "API korisničko ime (uključujući webapi_ prefiks)",
|
||||||
|
serwersmsAPIPassword: "API lozinka",
|
||||||
|
serwersmsPhoneNumber: "Broj telefona",
|
||||||
|
serwersmsSenderName: "Ime SMS pošiljatelja (registrirano preko korisničkog portala)",
|
||||||
|
stackfield: "Stackfield",
|
||||||
|
smtpDkimSettings: "DKIM postavke",
|
||||||
|
smtpDkimDesc: "Za više informacija, postoji Nodemailer DKIM {0}.",
|
||||||
|
documentation: "dokumentacija",
|
||||||
|
smtpDkimDomain: "Domena",
|
||||||
|
smtpDkimKeySelector: "Odabir ključa",
|
||||||
|
smtpDkimPrivateKey: "Privatni ključ",
|
||||||
|
smtpDkimHashAlgo: "Hash algoritam (neobavezno)",
|
||||||
|
smtpDkimheaderFieldNames: "Ključevi zaglavlja za potpis (neobavezno)",
|
||||||
|
smtpDkimskipFields: "Ključevi zaglavlja koji se neće potpisati (neobavezno)",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "Krajnja točka API-ja (Endpoint)",
|
||||||
|
alertaEnvironment: "Okruženje (Environment)",
|
||||||
|
alertaApiKey: "API ključ",
|
||||||
|
alertaAlertState: "Stanje upozorenja",
|
||||||
|
alertaRecoverState: "Stanje oporavka",
|
||||||
|
deleteStatusPageMsg: "Sigurno želite obrisati ovu statusnu stranicu?",
|
||||||
};
|
};
|
||||||
|
|
|
@ -180,8 +180,8 @@ export default {
|
||||||
"Add a monitor": "Добавить монитор",
|
"Add a monitor": "Добавить монитор",
|
||||||
"Edit Status Page": "Редактировать",
|
"Edit Status Page": "Редактировать",
|
||||||
"Go to Dashboard": "Панель управления",
|
"Go to Dashboard": "Панель управления",
|
||||||
"Status Page": "Мониторинг",
|
"Status Page": "Страница статуса",
|
||||||
"Status Pages": "Página de Status",
|
"Status Pages": "Страницы статуса",
|
||||||
Discard: "Отмена",
|
Discard: "Отмена",
|
||||||
"Create Incident": "Создать инцидент",
|
"Create Incident": "Создать инцидент",
|
||||||
"Switch to Dark Theme": "Тёмная тема",
|
"Switch to Dark Theme": "Тёмная тема",
|
||||||
|
@ -311,28 +311,82 @@ export default {
|
||||||
"One record": "Одна запись",
|
"One record": "Одна запись",
|
||||||
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
|
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
|
||||||
"Certificate Chain": "Цепочка сертификатов",
|
"Certificate Chain": "Цепочка сертификатов",
|
||||||
"Valid": "Действительный",
|
Valid: "Действительный",
|
||||||
"Hide Tags": "Скрыть тэги",
|
"Hide Tags": "Скрыть тэги",
|
||||||
"Title": "Название инцидента:",
|
Title: "Название инцидента:",
|
||||||
"Content": "Содержание инцидента:",
|
Content: "Содержание инцидента:",
|
||||||
"Post": "Опубликовать",
|
Post: "Опубликовать",
|
||||||
"Cancel": "Отмена",
|
Cancel: "Отмена",
|
||||||
"Created": "Создано",
|
Created: "Создано",
|
||||||
"Unpin": "Открепить",
|
Unpin: "Открепить",
|
||||||
"Show Tags": "Показать тэги",
|
"Show Tags": "Показать тэги",
|
||||||
"recent": "Сейчас",
|
recent: "Сейчас",
|
||||||
"3h": "3 часа",
|
"3h": "3 часа",
|
||||||
"6h": "6 часов",
|
"6h": "6 часов",
|
||||||
"24h": "24 часа",
|
"24h": "24 часа",
|
||||||
"1w": "1 неделя",
|
"1w": "1 неделя",
|
||||||
"No monitors available.": "Нет доступных мониторов",
|
"No monitors available.": "Нет доступных мониторов",
|
||||||
"Add one": "Добавить новый",
|
"Add one": "Добавить новый",
|
||||||
"Backup": "Резервная копия",
|
Backup: "Резервная копия",
|
||||||
"Security": "Безопасность",
|
Security: "Безопасность",
|
||||||
"Shrink Database": "Сжать Базу Данных",
|
"Shrink Database": "Сжать Базу Данных",
|
||||||
"Current User": "Текущий пользователь",
|
"Current User": "Текущий пользователь",
|
||||||
"About": "О программе",
|
About: "О программе",
|
||||||
"Description": "Описание",
|
Description: "Описание",
|
||||||
"Powered by": "Работает на основе скрипта от",
|
"Powered by": "Работает на основе скрипта от",
|
||||||
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
|
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
|
||||||
|
deleteStatusPageMsg: "Вы действительно хотите удалить эту страницу статуса сервисов?",
|
||||||
|
Style: "Стиль",
|
||||||
|
info: "ИНФО",
|
||||||
|
warning: "ВНИМАНИЕ",
|
||||||
|
danger: "ОШИБКА",
|
||||||
|
primary: "ОСНОВНОЙ",
|
||||||
|
light: "СВЕТЛЫЙ",
|
||||||
|
dark: "ТЕМНЫЙ",
|
||||||
|
"New Status Page": "Новая страница статуса",
|
||||||
|
"Show update if available": "Показывать доступные обновления",
|
||||||
|
"Also check beta release": "Проверять обновления для бета версий",
|
||||||
|
"Add New Status Page": "Добавить страницу статуса",
|
||||||
|
Next: "Далее",
|
||||||
|
"Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -",
|
||||||
|
"Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9",
|
||||||
|
"No consecutive dashes --": "Запрещено использовать тире --",
|
||||||
|
"HTTP Options": "HTTP Опции",
|
||||||
|
"Basic Auth": "HTTP Авторизация",
|
||||||
|
PushByTechulus: "Push by Techulus",
|
||||||
|
clicksendsms: "ClickSend SMS",
|
||||||
|
GoogleChat: "Google Chat (только Google Workspace)",
|
||||||
|
apiCredentials: "API реквизиты",
|
||||||
|
Done: "Готово",
|
||||||
|
Info: "Инфо",
|
||||||
|
"Steam API Key": "Steam API-Ключ",
|
||||||
|
"Pick a RR-Type...": "Выберите RR-Тип...",
|
||||||
|
"Pick Accepted Status Codes...": "Выберите принятые коды состояния...",
|
||||||
|
Default: "По умолчанию",
|
||||||
|
"Please input title and content": "Пожалуйста, введите название и содержание",
|
||||||
|
"Last Updated": "Последнее Обновление",
|
||||||
|
"Untitled Group": "Группа без названия",
|
||||||
|
Services: "Сервисы",
|
||||||
|
serwersms: "SerwerSMS.pl",
|
||||||
|
serwersmsAPIUser: "API Пользователь (включая префикс webapi_)",
|
||||||
|
serwersmsAPIPassword: "API Пароль",
|
||||||
|
serwersmsPhoneNumber: "Номер телефона",
|
||||||
|
serwersmsSenderName: "SMS Имя Отправителя (регистрированный через пользовательский портал)",
|
||||||
|
stackfield: "Stackfield",
|
||||||
|
smtpDkimSettings: "DKIM Настройки",
|
||||||
|
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
||||||
|
documentation: "документация",
|
||||||
|
smtpDkimDomain: "Имя Домена",
|
||||||
|
smtpDkimKeySelector: "Ключ",
|
||||||
|
smtpDkimPrivateKey: "Приватный ключ",
|
||||||
|
smtpDkimHashAlgo: "Алгоритм хэша (опционально)",
|
||||||
|
smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)",
|
||||||
|
smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)",
|
||||||
|
gorush: "Gorush",
|
||||||
|
alerta: "Alerta",
|
||||||
|
alertaApiEndpoint: "Конечная точка API",
|
||||||
|
alertaEnvironment: "Среда",
|
||||||
|
alertaApiKey: "Ключ API",
|
||||||
|
alertaAlertState: "Состояние алерта",
|
||||||
|
alertaRecoverState: "Состояние восстановления",
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{{ $root.connectionErrorMsg }}
|
{{ $root.connectionErrorMsg }}
|
||||||
|
<div v-if="$root.showReverseProxyGuide">
|
||||||
|
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -45,7 +48,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<router-view v-if="$root.loggedIn" />
|
<router-view v-if="$root.loggedIn || forceShowContent" />
|
||||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
@ -184,6 +187,8 @@ main {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: crimson;
|
background-color: crimson;
|
||||||
color: white;
|
color: white;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|
|
@ -41,6 +41,15 @@ export default {
|
||||||
statusPageListLoaded: false,
|
statusPageListLoaded: false,
|
||||||
statusPageList: [],
|
statusPageList: [],
|
||||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||||
|
showReverseProxyGuide: true,
|
||||||
|
cloudflared: {
|
||||||
|
cloudflareTunnelToken: "",
|
||||||
|
installed: null,
|
||||||
|
running: false,
|
||||||
|
message: "",
|
||||||
|
errorMessage: "",
|
||||||
|
currentPassword: "",
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -185,6 +194,7 @@ export default {
|
||||||
socket.on("connect_error", (err) => {
|
socket.on("connect_error", (err) => {
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||||
|
this.showReverseProxyGuide = true;
|
||||||
this.socket.connected = false;
|
this.socket.connected = false;
|
||||||
this.socket.firstConnect = false;
|
this.socket.firstConnect = false;
|
||||||
});
|
});
|
||||||
|
@ -199,6 +209,7 @@ export default {
|
||||||
console.log("Connected to the socket server");
|
console.log("Connected to the socket server");
|
||||||
this.socket.connectCount++;
|
this.socket.connectCount++;
|
||||||
this.socket.connected = true;
|
this.socket.connected = true;
|
||||||
|
this.showReverseProxyGuide = false;
|
||||||
|
|
||||||
// Reset Heartbeat list if it is re-connect
|
// Reset Heartbeat list if it is re-connect
|
||||||
if (this.socket.connectCount >= 2) {
|
if (this.socket.connectCount >= 2) {
|
||||||
|
@ -228,6 +239,12 @@ export default {
|
||||||
this.socket.firstConnect = false;
|
this.socket.firstConnect = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// cloudflared
|
||||||
|
socket.on("cloudflared_installed", (res) => this.cloudflared.installed = res);
|
||||||
|
socket.on("cloudflared_running", (res) => this.cloudflared.running = res);
|
||||||
|
socket.on("cloudflared_message", (res) => this.cloudflared.message = res);
|
||||||
|
socket.on("cloudflared_errorMessage", (res) => this.cloudflared.errorMessage = res);
|
||||||
|
socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res);
|
||||||
},
|
},
|
||||||
|
|
||||||
storage() {
|
storage() {
|
||||||
|
|
99
src/pages/NotFound.vue
Normal file
99
src/pages/NotFound.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Desktop header -->
|
||||||
|
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||||
|
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||||
|
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
|
||||||
|
<span class="fs-4 title">Uptime Kuma</span>
|
||||||
|
</router-link>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Mobile header -->
|
||||||
|
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
|
||||||
|
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||||
|
<object class="bi" width="40" height="40" data="/icon.svg" />
|
||||||
|
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||||
|
</router-link>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div>
|
||||||
|
<strong>🐻 {{ $t("Page Not Found") }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guide">
|
||||||
|
Most likely causes:
|
||||||
|
<ul>
|
||||||
|
<li>The resource is no longer available.</li>
|
||||||
|
<li>There might be a typing error in the address.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
What you can try:<br />
|
||||||
|
<ul>
|
||||||
|
<li>Retype the address.</li>
|
||||||
|
<li><a href="#" class="go-back" @click="goBack()">Go back to the previous page.</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.go-back {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
padding-top: 30px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide {
|
||||||
|
max-width: 800px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
header {
|
||||||
|
background-color: $dark-header-bg;
|
||||||
|
border-bottom-color: $dark-header-bg !important;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #f0f6fc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -75,6 +75,9 @@ export default {
|
||||||
notifications: {
|
notifications: {
|
||||||
title: this.$t("Notifications"),
|
title: this.$t("Notifications"),
|
||||||
},
|
},
|
||||||
|
"reverse-proxy": {
|
||||||
|
title: this.$t("Reverse Proxy"),
|
||||||
|
},
|
||||||
"monitor-history": {
|
"monitor-history": {
|
||||||
title: this.$t("Monitor History"),
|
title: this.$t("Monitor History"),
|
||||||
},
|
},
|
||||||
|
@ -131,10 +134,18 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSettings() {
|
/**
|
||||||
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
* Save Settings
|
||||||
|
* @param currentPassword (Optional) Only need for disableAuth to true
|
||||||
|
*/
|
||||||
|
saveSettings(callback, currentPassword) {
|
||||||
|
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -518,6 +518,7 @@ export default {
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
let startTime = new Date();
|
let startTime = new Date();
|
||||||
|
this.config.slug = this.config.slug.trim().toLowerCase();
|
||||||
|
|
||||||
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
|
@ -14,12 +14,14 @@ import Entry from "./pages/Entry.vue";
|
||||||
import Appearance from "./components/settings/Appearance.vue";
|
import Appearance from "./components/settings/Appearance.vue";
|
||||||
import General from "./components/settings/General.vue";
|
import General from "./components/settings/General.vue";
|
||||||
import Notifications from "./components/settings/Notifications.vue";
|
import Notifications from "./components/settings/Notifications.vue";
|
||||||
|
import ReverseProxy from "./components/settings/ReverseProxy.vue";
|
||||||
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
import Security from "./components/settings/Security.vue";
|
import Security from "./components/settings/Security.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";
|
||||||
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
||||||
import AddStatusPage from "./pages/AddStatusPage.vue";
|
import AddStatusPage from "./pages/AddStatusPage.vue";
|
||||||
|
import NotFound from "./pages/NotFound.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -82,6 +84,10 @@ const routes = [
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
component: Notifications,
|
component: Notifications,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "reverse-proxy",
|
||||||
|
component: ReverseProxy,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "monitor-history",
|
path: "monitor-history",
|
||||||
component: MonitorHistory,
|
component: MonitorHistory,
|
||||||
|
@ -128,6 +134,10 @@ const routes = [
|
||||||
path: "/status/:slug",
|
path: "/status/:slug",
|
||||||
component: StatusPage,
|
component: StatusPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:pathMatch(.*)*",
|
||||||
|
component: NotFound,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|
Loading…
Add table
Reference in a new issue