mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 23:04:04 +00:00
Merge branch 'louislam:master' into master
This commit is contained in:
commit
f9b8dbf4db
26 changed files with 3001 additions and 2518 deletions
|
@ -1,6 +1,6 @@
|
|||
# Project Info
|
||||
|
||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that.
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
|
@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
|||
|
||||
## Can I create a pull request for Uptime Kuma?
|
||||
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and performance, I will merge into the master branch once it is tested.
|
||||
|
||||
If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first.
|
||||
|
||||
|
@ -66,13 +66,13 @@ I do not have such knowledge to test it.
|
|||
|
||||
#### ⚠ Low Priority - Harsh Mode
|
||||
|
||||
Some pull requests are required to modifiy the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change.
|
||||
Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change.
|
||||
|
||||
- Touch large parts of code of any very important features
|
||||
- Touch monitoring logic
|
||||
- Drop a table or drop a column for any reason
|
||||
- Touch the entry point of Docker or Node.js
|
||||
- Modifiy auth
|
||||
- Modify auth
|
||||
|
||||
|
||||
#### *️⃣ Low Priority
|
||||
|
@ -114,7 +114,7 @@ I personally do not like something need to learn so much and need to config so m
|
|||
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||
|
||||
## Install dependencies
|
||||
|
@ -141,7 +141,7 @@ express.js is just used for serving the frontend built files (index.html, .js an
|
|||
|
||||
- model/ (Object model, auto mapping to the database table name)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- notification-providers/ (indivdual notification logic)
|
||||
- notification-providers/ (individual notification logic)
|
||||
- routers/ (Express Routers)
|
||||
- scoket-handler (Socket.io Handlers)
|
||||
- server.js (Server main logic)
|
||||
|
|
33
config/jest-debug-env.js
Normal file
33
config/jest-debug-env.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
||||
const util = require("util");
|
||||
|
||||
class DebugEnv extends PuppeteerEnvironment {
|
||||
async handleTestEvent(event, state) {
|
||||
const ignoredEvents = [
|
||||
"setup",
|
||||
"add_hook",
|
||||
"start_describe_definition",
|
||||
"add_test",
|
||||
"finish_describe_definition",
|
||||
"run_start",
|
||||
"run_describe_start",
|
||||
"test_start",
|
||||
"hook_start",
|
||||
"hook_success",
|
||||
"test_fn_start",
|
||||
"test_fn_success",
|
||||
"test_done",
|
||||
"run_describe_finish",
|
||||
"run_finish",
|
||||
"teardown",
|
||||
"test_fn_failure",
|
||||
];
|
||||
if (!ignoredEvents.includes(event.name)) {
|
||||
console.log(
|
||||
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DebugEnv;
|
|
@ -1,6 +1,20 @@
|
|||
module.exports = {
|
||||
"launch": {
|
||||
"dumpio": true,
|
||||
"slowMo": 500,
|
||||
"headless": process.env.HEADLESS_TEST || false,
|
||||
"userDataDir": "./data/test-chrome-profile",
|
||||
args: [
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
"--no-default-browser-check",
|
||||
"--no-experiments",
|
||||
"--no-first-run",
|
||||
"--no-pings",
|
||||
"--no-sandbox",
|
||||
"--no-zygote",
|
||||
"--single-process",
|
||||
],
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = {
|
|||
"__DEV__": true
|
||||
},
|
||||
"testRegex": "./test/e2e.spec.js",
|
||||
"testEnvironment": "./config/jest-debug-env.js",
|
||||
"rootDir": "..",
|
||||
"testTimeout": 30000,
|
||||
};
|
||||
|
|
10
db/patch-monitor-basic-auth.sql
Normal file
10
db/patch-monitor-basic-auth.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD basic_auth_user TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD basic_auth_pass TEXT default null;
|
||||
|
||||
COMMIT;
|
3375
package-lock.json
generated
3375
package-lock.json
generated
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
@ -22,7 +22,7 @@
|
|||
"build": "vite build --config ./config/vite.config.js",
|
||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||
"test-with-build": "npm run build && npm test",
|
||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js",
|
||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --runInBand --config=./config/jest.config.js",
|
||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
||||
"tsc": "tsc",
|
||||
|
@ -56,7 +56,7 @@
|
|||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||
"@louislam/sqlite3": "~6.0.0",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
|
@ -74,7 +74,7 @@
|
|||
"express": "~4.17.1",
|
||||
"express-basic-auth": "~1.2.0",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.4",
|
||||
"http-graceful-shutdown": "~3.1.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
|
@ -83,7 +83,7 @@
|
|||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.1",
|
||||
"postcss-scss": "~4.0.2",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.0",
|
||||
"qrcode": "~1.4.4",
|
||||
|
@ -103,30 +103,30 @@
|
|||
"vue-image-crop-upload": "~3.0.3",
|
||||
"vue-multiselect": "~3.0.0-alpha.2",
|
||||
"vue-qrcode": "~1.0.0",
|
||||
"vue-router": "~4.0.11",
|
||||
"vue-toastification": "~2.0.0-rc.1",
|
||||
"vue-router": "~4.0.12",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"vuedraggable": "~4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "~7.15.7",
|
||||
"@babel/eslint-parser": "~7.15.8",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.6",
|
||||
"@vitejs/plugin-legacy": "~1.6.2",
|
||||
"@vitejs/plugin-legacy": "~1.6.3",
|
||||
"@vitejs/plugin-vue": "~1.9.4",
|
||||
"@vue/compiler-sfc": "~3.2.20",
|
||||
"@vue/compiler-sfc": "~3.2.22",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"core-js": "~3.18.3",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"jest": "~27.2.4",
|
||||
"jest": "~27.2.5",
|
||||
"jest-puppeteer": "~6.0.0",
|
||||
"puppeteer": "~10.4.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~13.13.1",
|
||||
"stylelint-config-standard": "~22.0.0",
|
||||
"typescript": "~4.4.3",
|
||||
"vite": "~2.6.13"
|
||||
"typescript": "~4.4.4",
|
||||
"vite": "~2.6.14"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ class Database {
|
|||
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||
"patch-2fa-invalidate-used-token.sql": true,
|
||||
"patch-notification_sent_history.sql": true,
|
||||
"patch-monitor-basic-auth.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,7 +80,7 @@ class Database {
|
|||
console.log(`Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
static async connect() {
|
||||
static async connect(testMode = false) {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
|
@ -112,8 +113,13 @@ class Database {
|
|||
await R.autoloadModels("./server/model");
|
||||
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
if (testMode) {
|
||||
// Change to MEMORY
|
||||
await R.exec("PRAGMA journal_mode = MEMORY");
|
||||
} else {
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||
|
||||
|
|
|
@ -58,6 +58,8 @@ class Monitor extends BeanModel {
|
|||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers,
|
||||
basic_auth_user: this.basic_auth_user,
|
||||
basic_auth_pass: this.basic_auth_pass,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
|
@ -80,6 +82,15 @@ class Monitor extends BeanModel {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeBase64(user, pass) {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
|
@ -141,7 +152,16 @@ class Monitor extends BeanModel {
|
|||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
// HTTP basic auth
|
||||
let basicAuthHeader = {};
|
||||
if (this.basic_auth_user) {
|
||||
basicAuthHeader = {
|
||||
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
||||
};
|
||||
}
|
||||
|
||||
debug(`[${this.name}] Prepare Options for axios`);
|
||||
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
|
@ -151,6 +171,7 @@ class Monitor extends BeanModel {
|
|||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
...(basicAuthHeader),
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
|
|
|
@ -177,7 +177,7 @@ exports.entryPage = "dashboard";
|
|||
|
||||
(async () => {
|
||||
Database.init(args);
|
||||
await initDatabase();
|
||||
await initDatabase(testMode);
|
||||
|
||||
exports.entryPage = await setting("entryPage");
|
||||
|
||||
|
@ -539,8 +539,8 @@ exports.entryPage = "dashboard";
|
|||
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
await sendMonitorList(socket);
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
|
@ -573,6 +573,8 @@ exports.entryPage = "dashboard";
|
|||
bean.method = monitor.method;
|
||||
bean.body = monitor.body;
|
||||
bean.headers = monitor.headers;
|
||||
bean.basic_auth_user = monitor.basic_auth_user;
|
||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.hostname = monitor.hostname;
|
||||
|
@ -1137,6 +1139,8 @@ exports.entryPage = "dashboard";
|
|||
method: monitorListData[i].method || "GET",
|
||||
body: monitorListData[i].body,
|
||||
headers: monitorListData[i].headers,
|
||||
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
hostname: monitorListData[i].hostname,
|
||||
|
@ -1415,14 +1419,14 @@ async function getMonitorJSONList(userID) {
|
|||
return result;
|
||||
}
|
||||
|
||||
async function initDatabase() {
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database");
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
console.log("Connecting to the Database");
|
||||
await Database.connect();
|
||||
await Database.connect(testMode);
|
||||
console.log("Connected");
|
||||
|
||||
// Patch the database
|
||||
|
|
|
@ -12,6 +12,7 @@ $dark-font-color2: #020b05;
|
|||
$dark-bg: #0d1117;
|
||||
$dark-bg2: #070a10;
|
||||
$dark-border-color: #1d2634;
|
||||
$dark-header-bg: #161b22;
|
||||
|
||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
|
|
@ -137,7 +137,7 @@ export default {
|
|||
justify-content: space-between;
|
||||
|
||||
.dark & {
|
||||
background-color: #161b22;
|
||||
background-color: $dark-header-bg;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
25
src/components/settings/About.vue
Normal file
25
src/components/settings/About.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
||||
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
margin: 4em 1em;
|
||||
}
|
||||
.update-link {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
149
src/components/settings/Appearance.vue
Normal file
149
src/components/settings/Appearance.vue
Normal file
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="my-4">
|
||||
<label for="language" class="form-label">
|
||||
{{ $t("Language") }}
|
||||
</label>
|
||||
<select id="language" v-model="$i18n.locale" class="form-select">
|
||||
<option
|
||||
v-for="(lang, i) in $i18n.availableLocales"
|
||||
:key="`Lang${i}`"
|
||||
:value="lang"
|
||||
>
|
||||
{{ $i18n.messages[lang].languageName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||
<div>
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
aria-label="Basic checkbox toggle button group"
|
||||
>
|
||||
<input
|
||||
id="btncheck1"
|
||||
v-model="$root.userTheme"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="theme"
|
||||
autocomplete="off"
|
||||
value="light"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="btncheck1">
|
||||
{{ $t("Light") }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="btncheck2"
|
||||
v-model="$root.userTheme"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="theme"
|
||||
autocomplete="off"
|
||||
value="dark"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="btncheck2">
|
||||
{{ $t("Dark") }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="btncheck3"
|
||||
v-model="$root.userTheme"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="theme"
|
||||
autocomplete="off"
|
||||
value="auto"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="btncheck3">
|
||||
{{ $t("Auto") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
|
||||
<div>
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
aria-label="Basic checkbox toggle button group"
|
||||
>
|
||||
<input
|
||||
id="btncheck4"
|
||||
v-model="$root.userHeartbeatBar"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="heartbeatBarTheme"
|
||||
autocomplete="off"
|
||||
value="normal"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="btncheck4">
|
||||
{{ $t("Normal") }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="btncheck5"
|
||||
v-model="$root.userHeartbeatBar"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="heartbeatBarTheme"
|
||||
autocomplete="off"
|
||||
value="bottom"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="btncheck5">
|
||||
{{ $t("Bottom") }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="btncheck6"
|
||||
v-model="$root.userHeartbeatBar"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="heartbeatBarTheme"
|
||||
autocomplete="off"
|
||||
value="none"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="btncheck6">
|
||||
{{ $t("None") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setPageLocale } from "../../util-frontend";
|
||||
export default {
|
||||
watch: {
|
||||
"$i18n.locale"() {
|
||||
localStorage.locale = this.$i18n.locale;
|
||||
setPageLocale();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.btn-check:active + .btn-outline-primary,
|
||||
.btn-check:checked + .btn-outline-primary,
|
||||
.btn-check:hover + .btn-outline-primary {
|
||||
color: #fff;
|
||||
|
||||
.dark & {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.list-group-item {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
213
src/components/settings/Backup.vue
Normal file
213
src/components/settings/Backup.vue
Normal file
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="my-4">
|
||||
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
||||
|
||||
<p>
|
||||
{{ $t("backupDescription") }} <br />
|
||||
({{ $t("backupDescription2") }}) <br />
|
||||
</p>
|
||||
|
||||
<div class="mb-2">
|
||||
<button class="btn btn-primary" @click="downloadBackup">
|
||||
{{ $t("Export") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<strong>{{ $t("backupDescription3") }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
|
||||
|
||||
<label class="form-label">{{ $t("Options") }}:</label>
|
||||
<br />
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
id="radioKeep"
|
||||
v-model="importHandle"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="radioImportHandle"
|
||||
value="keep"
|
||||
/>
|
||||
<label class="form-check-label" for="radioKeep">
|
||||
{{ $t("Keep both") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
id="radioSkip"
|
||||
v-model="importHandle"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="radioImportHandle"
|
||||
value="skip"
|
||||
/>
|
||||
<label class="form-check-label" for="radioSkip">
|
||||
{{ $t("Skip existing") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
id="radioOverwrite"
|
||||
v-model="importHandle"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="radioImportHandle"
|
||||
value="overwrite"
|
||||
/>
|
||||
<label class="form-check-label" for="radioOverwrite">
|
||||
{{ $t("Overwrite") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text mb-2">
|
||||
{{ $t("importHandleDescription") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<input
|
||||
id="importBackup"
|
||||
type="file"
|
||||
class="form-control"
|
||||
accept="application/json"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-2 justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
:disabled="processing"
|
||||
@click="confirmImport"
|
||||
>
|
||||
<div
|
||||
v-if="processing"
|
||||
class="spinner-border spinner-border-sm me-1"
|
||||
></div>
|
||||
{{ $t("Import") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="importAlert"
|
||||
class="alert alert-danger mt-3"
|
||||
style="padding: 6px 16px"
|
||||
>
|
||||
{{ importAlert }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm
|
||||
ref="confirmImport"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="importBackup"
|
||||
>
|
||||
{{ $t("confirmImportMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../../components/Confirm.vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useToast } from "vue-toastification";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
importHandle: "skip",
|
||||
importAlert: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
confirmImport() {
|
||||
this.$refs.confirmImport.show();
|
||||
},
|
||||
|
||||
downloadBackup() {
|
||||
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||
let monitorList = Object.values(this.$root.monitorList);
|
||||
let exportData = {
|
||||
version: this.$root.info.version,
|
||||
notificationList: this.$root.notificationList,
|
||||
monitorList: monitorList,
|
||||
};
|
||||
exportData = JSON.stringify(exportData, null, 4);
|
||||
let downloadItem = document.createElement("a");
|
||||
downloadItem.setAttribute(
|
||||
"href",
|
||||
"data:application/json;charset=utf-8," +
|
||||
encodeURIComponent(exportData)
|
||||
);
|
||||
downloadItem.setAttribute("download", fileName);
|
||||
downloadItem.click();
|
||||
},
|
||||
|
||||
importBackup() {
|
||||
this.processing = true;
|
||||
let uploadItem = document.getElementById("importBackup").files;
|
||||
|
||||
if (uploadItem.length <= 0) {
|
||||
this.processing = false;
|
||||
return (this.importAlert = this.$t("alertNoFile"));
|
||||
}
|
||||
|
||||
if (uploadItem.item(0).type !== "application/json") {
|
||||
this.processing = false;
|
||||
return (this.importAlert = this.$t("alertWrongFileType"));
|
||||
}
|
||||
|
||||
let fileReader = new FileReader();
|
||||
fileReader.readAsText(uploadItem.item(0));
|
||||
|
||||
fileReader.onload = (item) => {
|
||||
this.$root.uploadBackup(
|
||||
item.target.result,
|
||||
this.importHandle,
|
||||
(res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
#importBackup {
|
||||
&::file-selector-button {
|
||||
color: $primary;
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||
color: $dark-font-color2;
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
191
src/components/settings/General.vue
Normal file
191
src/components/settings/General.vue
Normal file
|
@ -0,0 +1,191 @@
|
|||
<template>
|
||||
<div>
|
||||
<form class="my-4" @submit.prevent="saveGeneral">
|
||||
<!-- Timezone -->
|
||||
<div class="mb-4">
|
||||
<label for="timezone" class="form-label">
|
||||
{{ $t("Timezone") }}
|
||||
</label>
|
||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||
<option value="auto">
|
||||
{{ $t("Auto") }}: {{ guessTimezone }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(timezone, index) in timezoneList"
|
||||
:key="index"
|
||||
:value="timezone.value"
|
||||
>
|
||||
{{ timezone.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Search Engine -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
{{ $t("Search Engine Visibility") }}
|
||||
</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="searchEngineIndexYes"
|
||||
v-model="settings.searchEngineIndex"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="flexRadioDefault"
|
||||
:value="true"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="searchEngineIndexYes">
|
||||
{{ $t("Allow indexing") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="searchEngineIndexNo"
|
||||
v-model="settings.searchEngineIndex"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="flexRadioDefault"
|
||||
:value="false"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="searchEngineIndexNo">
|
||||
{{ $t("Discourage search engines from indexing site") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entry Page -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">{{ $t("Entry Page") }}</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="entryPageYes"
|
||||
v-model="settings.entryPage"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="statusPage"
|
||||
value="dashboard"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="entryPageYes">
|
||||
{{ $t("Dashboard") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="entryPageNo"
|
||||
v-model="settings.entryPage"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="statusPage"
|
||||
value="statusPage"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="entryPageNo">
|
||||
{{ $t("Status Page") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Base URL -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="primaryBaseURL">
|
||||
{{ $t("Primary Base URL") }}
|
||||
</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
id="primaryBaseURL"
|
||||
v-model="settings.primaryBaseURL"
|
||||
class="form-control"
|
||||
name="primaryBaseURL"
|
||||
placeholder="https://"
|
||||
pattern="https?://.+"
|
||||
/>
|
||||
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
|
||||
{{ $t("Auto Get") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Steam API Key -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="steamAPIKey">
|
||||
{{ $t("Steam API Key") }}
|
||||
</label>
|
||||
<HiddenInput
|
||||
id="steamAPIKey"
|
||||
v-model="settings.steamAPIKey"
|
||||
/>
|
||||
<div class="form-text">
|
||||
{{ $t("steamApiKeyDescription") }}
|
||||
<a href="https://steamcommunity.com/dev" target="_blank">
|
||||
https://steamcommunity.com/dev
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../../components/HiddenInput.vue";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import { timezoneList } from "../../util-frontend";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
timezoneList: timezoneList(),
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
},
|
||||
guessTimezone() {
|
||||
return dayjs.tz.guess();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
saveGeneral() {
|
||||
localStorage.timezone = this.$root.userTimezone;
|
||||
this.saveSettings();
|
||||
},
|
||||
autoGetPrimaryBaseURL() {
|
||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
133
src/components/settings/MonitorHistory.vue
Normal file
133
src/components/settings/MonitorHistory.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="my-4">
|
||||
<label for="keepDataPeriodDays" class="form-label">
|
||||
{{
|
||||
$t("clearDataOlderThan", [
|
||||
settings.keepDataPeriodDays,
|
||||
])
|
||||
}}
|
||||
</label>
|
||||
<input
|
||||
id="keepDataPeriodDays"
|
||||
v-model="settings.keepDataPeriodDays"
|
||||
type="number"
|
||||
class="form-control"
|
||||
required
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<div class="my-3">
|
||||
<button class="btn btn-outline-info me-2" @click="shrinkDatabase">
|
||||
{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})
|
||||
</button>
|
||||
<div class="form-text mt-2 mb-4 ms-2">{{ $t("shrinkDatabaseDescription") }}</div>
|
||||
</div>
|
||||
<button
|
||||
id="clearAllStats-btn"
|
||||
class="btn btn-outline-danger me-2 mb-2"
|
||||
@click="confirmClearStatistics"
|
||||
>
|
||||
{{ $t("Clear all statistics") }}
|
||||
</button>
|
||||
</div>
|
||||
<Confirm
|
||||
ref="confirmClearStatistics"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="clearStatistics"
|
||||
>
|
||||
{{ $t("confirmClearStatisticsMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../../components/Confirm.vue";
|
||||
import { debug } from "../../util.ts";
|
||||
import { useToast } from "vue-toastification";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
databaseSize: 0,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
},
|
||||
databaseSizeDisplay() {
|
||||
return (
|
||||
Math.round((this.databaseSize / 1024 / 1024) * 10) / 10 + " MB"
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadDatabaseSize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadDatabaseSize() {
|
||||
debug("load database size");
|
||||
this.$root.getSocket().emit("getDatabaseSize", (res) => {
|
||||
if (res.ok) {
|
||||
this.databaseSize = res.size;
|
||||
debug("database size: " + res.size);
|
||||
} else {
|
||||
debug(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
shrinkDatabase() {
|
||||
this.$root.getSocket().emit("shrinkDatabase", (res) => {
|
||||
if (res.ok) {
|
||||
this.loadDatabaseSize();
|
||||
toast.success("Done");
|
||||
} else {
|
||||
debug(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
confirmClearStatistics() {
|
||||
this.$refs.confirmClearStatistics.show();
|
||||
},
|
||||
|
||||
clearStatistics() {
|
||||
this.$root.clearStatistics((res) => {
|
||||
if (res.ok) {
|
||||
this.$router.go();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
46
src/components/settings/Notifications.vue
Normal file
46
src/components/settings/Notifications.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="notification-list my-4">
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ $t("notificationDescription") }}
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||
{{ notification.name }}<br>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
{{ $t("Setup Notification") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotificationDialog from "../../components/NotificationDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationDialog
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.list-group-item {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
323
src/components/settings/Security.vue
Normal file
323
src/components/settings/Security.vue
Normal file
|
@ -0,0 +1,323 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="settingsLoaded" class="my-4">
|
||||
<!-- Change Password -->
|
||||
<template v-if="!settings.disableAuth">
|
||||
<p>
|
||||
{{ $t("Current User") }}: <strong>{{ username }}</strong>
|
||||
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
|
||||
</p>
|
||||
|
||||
<h5 class="my-4">{{ $t("Change Password") }}</h5>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">
|
||||
{{ $t("Current Password") }}
|
||||
</label>
|
||||
<input
|
||||
id="current-password"
|
||||
v-model="password.currentPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">
|
||||
{{ $t("New Password") }}
|
||||
</label>
|
||||
<input
|
||||
id="new-password"
|
||||
v-model="password.newPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">
|
||||
{{ $t("Repeat New Password") }}
|
||||
</label>
|
||||
<input
|
||||
id="repeat-new-password"
|
||||
v-model="password.repeatNewPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': invalidPassword }"
|
||||
required
|
||||
/>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("passwordNotMatchMsg") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ $t("Update Password") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
||||
<h5 class="my-4">
|
||||
{{ $t("Two Factor Authentication") }}
|
||||
</h5>
|
||||
<div class="mb-4">
|
||||
<button
|
||||
class="btn btn-primary me-2"
|
||||
type="button"
|
||||
@click="$refs.TwoFADialog.show()"
|
||||
>
|
||||
{{ $t("2FA Settings") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<!-- Advanced -->
|
||||
<h5 class="my-4">{{ $t("Advanced") }}</h5>
|
||||
|
||||
<div class="mb-4">
|
||||
<button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
||||
<button v-if="! settings.disableAuth" id="disableAuth-btn" class="btn btn-primary me-2 mb-2" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TwoFADialog ref="TwoFADialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||
<template v-if="$i18n.locale === 'es-ES' ">
|
||||
<p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p>
|
||||
<p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p>
|
||||
<p>Por favor usar con cuidado.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'pt-BR' ">
|
||||
<p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p>
|
||||
<p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p>
|
||||
<p>Por favor, utilize isso com cautela.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'zh-HK' ">
|
||||
<p>你是否確認<strong>取消登入認証</strong>?</p>
|
||||
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p>
|
||||
<p>請小心使用。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'zh-CN' ">
|
||||
<p>是否确定 <strong>取消登录验证</strong>?</p>
|
||||
<p>这是为 <strong>有第三方认证</strong> 的用户提供的功能,如 Cloudflare Access</p>
|
||||
<p>请谨慎使用!</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'zh-TW' ">
|
||||
<p>你是否要<strong>取消登入驗證</strong>?</p>
|
||||
<p>此功能是設計給已有<strong>第三方認證</strong>的使用者,例如 Cloudflare Access。</p>
|
||||
<p>請謹慎使用。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'de-DE' ">
|
||||
<p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p>
|
||||
<p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p>
|
||||
<p>Bitte mit Vorsicht nutzen.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'sr' ">
|
||||
<p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p>
|
||||
<p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p>
|
||||
<p>Молим Вас користите ово са пажњом.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'sr-latn' ">
|
||||
<p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p>
|
||||
<p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p>
|
||||
<p>Molim Vas koristite ovo sa pažnjom.</p>
|
||||
</template>
|
||||
|
||||
<template v-if="$i18n.locale === 'hr-HR' ">
|
||||
<p>Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?</p>
|
||||
<p>To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.</p>
|
||||
<p>Pažljivo koristite ovu opciju.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'tr-TR' ">
|
||||
<p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p>
|
||||
<p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p>
|
||||
<p>Lütfen dikkatli kullanın.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'ko-KR' ">
|
||||
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
|
||||
<p>이 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong>을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.</p>
|
||||
<p>신중하게 사용하세요.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'pl' ">
|
||||
<p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p>
|
||||
<p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p>
|
||||
<p>Proszę używać ostrożnie.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'et-EE' ">
|
||||
<p>Kas soovid <strong>lülitada autentimise välja</strong>?</p>
|
||||
<p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p>
|
||||
<p>Palun kasuta vastutustundlikult.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'it-IT' ">
|
||||
<p>Si è certi di voler <strong>disabilitare l'autenticazione</strong>?</p>
|
||||
<p>È per <strong>chi ha l'autenticazione gestita da terze parti</strong> messa davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p>
|
||||
<p>Utilizzare con attenzione.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'id-ID' ">
|
||||
<p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p>
|
||||
<p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p>
|
||||
<p>Gunakan dengan hati-hati.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'ru-RU' ">
|
||||
<p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p>
|
||||
<p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p>
|
||||
<p>Пожалуйста, используйте с осторожностью.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'fa' ">
|
||||
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
|
||||
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
|
||||
<p>لطفا از این امکان با دقت استفاده کنید.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'bg-BG' ">
|
||||
<p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p>
|
||||
<p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p>
|
||||
<p>Моля, използвайте с повишено внимание.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'hu' ">
|
||||
<p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p>
|
||||
<p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p>
|
||||
<p>Használja megfontoltan!</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'nb-NO' ">
|
||||
<p>Er du sikker på at du vil <strong>deaktiver autentisering</strong>?</p>
|
||||
<p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p>
|
||||
<p>Vennligst vær forsiktig.</p>
|
||||
</template>
|
||||
|
||||
<!-- English (en) -->
|
||||
<template v-else>
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</template>
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../../components/Confirm.vue";
|
||||
import TwoFADialog from "../../components/TwoFADialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
TwoFADialog
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
username: "",
|
||||
invalidPassword: false,
|
||||
password: {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"password.repeatNewPassword"() {
|
||||
this.invalidPassword = false;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadUsername();
|
||||
},
|
||||
|
||||
methods: {
|
||||
savePassword() {
|
||||
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
||||
this.invalidPassword = true;
|
||||
} else {
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit("changePassword", this.password, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
if (res.ok) {
|
||||
this.password.currentPassword = "";
|
||||
this.password.newPassword = "";
|
||||
this.password.repeatNewPassword = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadUsername() {
|
||||
const jwtPayload = this.$root.getJWTPayload();
|
||||
|
||||
if (jwtPayload) {
|
||||
this.username = jwtPayload.username;
|
||||
}
|
||||
},
|
||||
|
||||
disableAuth() {
|
||||
this.settings.disableAuth = true;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
enableAuth() {
|
||||
this.settings.disableAuth = false;
|
||||
this.saveSettings();
|
||||
this.$root.storage().removeItem("token");
|
||||
location.reload();
|
||||
},
|
||||
|
||||
confirmDisableAuth() {
|
||||
this.$refs.confirmDisableAuth.show();
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
h5:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 50%;
|
||||
padding-top: 8px;
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,7 @@
|
|||
2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
|
||||
3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
|
||||
4. Your language file should be filled in. You can translate now.
|
||||
5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
||||
5. Translate `src/components/settings/Security.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
||||
6. Import your language file in `src/i18n.js` and add it to `languageList` constant.
|
||||
7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||
|
||||
|
|
|
@ -307,4 +307,5 @@ export default {
|
|||
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
|
||||
"Current User": "Current User",
|
||||
recent: "Recent",
|
||||
shrinkDatabaseDescription: "Trigger database VACCUM for SQLite. If your database is created after 1.10.0, AUTO_VACCUM is already enabled and this action is not needed.",
|
||||
};
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li v-if="$root.loggedIn" class="nav-item">
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<router-link to="/settings" class="nav-link" :class="{ active: $route.path.includes('settings') }">
|
||||
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
@ -188,8 +188,8 @@ main {
|
|||
|
||||
.dark {
|
||||
header {
|
||||
background-color: #161b22;
|
||||
border-bottom-color: #161b22 !important;
|
||||
background-color: $dark-header-bg;
|
||||
border-bottom-color: $dark-header-bg !important;
|
||||
|
||||
span {
|
||||
color: #f0f6fc;
|
||||
|
|
|
@ -194,7 +194,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -265,6 +265,19 @@
|
|||
<label for="headers" class="form-label">{{ $t("Headers") }}</label>
|
||||
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- HTTP Basic Auth -->
|
||||
<h4 class="mt-5 mb-2">{{ $t("HTTP Basic Auth") }}</h4>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="basicauth" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="basicauth" class="form-label">{{ $t("Password") }}</label>
|
||||
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" class="form-control" :placeholder="$t('Password')">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,527 +1,91 @@
|
|||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 v-show="show" class="mb-3">
|
||||
{{ $t("Settings") }}
|
||||
</h1>
|
||||
<div>
|
||||
<h1 v-show="show" class="mb-3">
|
||||
{{ $t("Settings") }}
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">{{ $t("Appearance") }}</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label">{{ $t("Language") }}</label>
|
||||
<select id="language" v-model="$i18n.locale" class="form-select">
|
||||
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
|
||||
{{ $i18n.messages[lang].languageName }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="settings-menu">
|
||||
<router-link
|
||||
v-for="(item, key) in subMenus"
|
||||
:key="key"
|
||||
:to="`/settings/${key}`"
|
||||
>
|
||||
<div class="menu-item">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
|
||||
<label class="btn btn-outline-primary" for="btncheck1">{{ $t("Light") }}</label>
|
||||
|
||||
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
|
||||
<label class="btn btn-outline-primary" for="btncheck2">{{ $t("Dark") }}</label>
|
||||
|
||||
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
|
||||
<label class="btn btn-outline-primary" for="btncheck3">{{ $t("Auto") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
|
||||
<label class="btn btn-outline-primary" for="btncheck4">{{ $t("Normal") }}</label>
|
||||
|
||||
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
|
||||
<label class="btn btn-outline-primary" for="btncheck5">{{ $t("Bottom") }}</label>
|
||||
|
||||
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
|
||||
<label class="btn btn-outline-primary" for="btncheck6">{{ $t("None") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Settings -->
|
||||
<h2 class="mt-5 mb-2">{{ $t("General") }}</h2>
|
||||
|
||||
<form class="mb-3" @submit.prevent="saveGeneral">
|
||||
<!-- Timezone -->
|
||||
<div class="mb-4">
|
||||
<label for="timezone" class="form-label">{{ $t("Timezone") }}</label>
|
||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||
<option value="auto">
|
||||
{{ $t("Auto") }}: {{ guessTimezone }}
|
||||
</option>
|
||||
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
|
||||
{{ timezone.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Search Engine -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">{{ $t("Search Engine Visibility") }}</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required>
|
||||
<label class="form-check-label" for="searchEngineIndexYes">
|
||||
{{ $t("Allow indexing") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
|
||||
<label class="form-check-label" for="searchEngineIndexNo">
|
||||
{{ $t("Discourage search engines from indexing site") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entry Page -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">{{ $t("Entry Page") }}</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input id="entryPageYes" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="dashboard" required>
|
||||
<label class="form-check-label" for="entryPageYes">
|
||||
{{ $t("Dashboard") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input id="entryPageNo" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="statusPage" required>
|
||||
<label class="form-check-label" for="entryPageNo">
|
||||
{{ $t("Status Page") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Base URL -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="primaryBaseURL">{{ $t("Primary Base URL") }}</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+">
|
||||
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">{{ $t("Auto Get") }}</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steam API Key -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="steamAPIKey">{{ $t("Steam API Key") }}</label>
|
||||
<HiddenInput id="steamAPIKey" v-model="settings.steamAPIKey" />
|
||||
<div class="form-text">
|
||||
{{ $t("steamApiKeyDescription") }}<a href="https://steamcommunity.com/dev" target="_blank">https://steamcommunity.com/dev</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitor History -->
|
||||
<div class="mb-4">
|
||||
<h4 class="mt-4">{{ $t("Monitor History") }}</h4>
|
||||
<div class="mt-2">
|
||||
<label for="keepDataPeriodDays" class="form-label">{{ $t("clearDataOlderThan", [ settings.keepDataPeriodDays ]) }}</label>
|
||||
<input id="keepDataPeriodDays" v-model="settings.keepDataPeriodDays" type="number" class="form-control" required min="1" step="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template v-if="loaded">
|
||||
<!-- Change Password -->
|
||||
<template v-if="! settings.disableAuth">
|
||||
<h2 class="mt-5 mb-2">{{ $t("Change Password") }}</h2>
|
||||
<p>{{ $t("Current User") }}: <strong>{{ username }}</strong></p>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">{{ $t("Current Password") }}</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">{{ $t("New Password") }}</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">{{ $t("Repeat New Password") }}</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("passwordNotMatchMsg") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ $t("Update Password") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
||||
<h2 class="mb-2">
|
||||
{{ $t("Two Factor Authentication") }}
|
||||
</h2>
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-2">{{ $t("Export Backup") }}</h2>
|
||||
|
||||
<p>
|
||||
{{ $t("backupDescription") }} <br />
|
||||
({{ $t("backupDescription2") }}) <br />
|
||||
</p>
|
||||
|
||||
<div class="mb-2">
|
||||
<button class="btn btn-primary" @click="downloadBackup">{{ $t("Export") }}</button>
|
||||
</div>
|
||||
|
||||
<p><strong>{{ $t("backupDescription3") }}</strong></p>
|
||||
|
||||
<h2 class="mt-5 mb-2">{{ $t("Import Backup") }}</h2>
|
||||
|
||||
<label class="form-label">{{ $t("Options") }}:</label>
|
||||
<br>
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="radioKeep" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="keep">
|
||||
<label class="form-check-label" for="radioKeep">{{ $t("Keep both") }}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="radioSkip" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="skip">
|
||||
<label class="form-check-label" for="radioSkip">{{ $t("Skip existing") }}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="radioOverwrite" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="overwrite">
|
||||
<label class="form-check-label" for="radioOverwrite">{{ $t("Overwrite") }}</label>
|
||||
</div>
|
||||
<div class="form-text mb-2">
|
||||
{{ $t("importHandleDescription") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<input id="importBackup" type="file" class="form-control" accept="application/json">
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-2 justify-content-end">
|
||||
<button type="button" class="btn btn-outline-primary" :disabled="processing" @click="confirmImport">
|
||||
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||
{{ $t("Import") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;">
|
||||
{{ importAlert }}
|
||||
</div>
|
||||
|
||||
<!-- Advanced -->
|
||||
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-2 mb-2" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
|
||||
<button class="btn btn-outline-danger me-2 mb-2" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button>
|
||||
<button class="btn btn-info me-2 mb-2" @click="shrinkDatabase">{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})</button>
|
||||
</div>
|
||||
</template>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<div class="settings-content-header">
|
||||
{{ subMenus[currentPage].title }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="notification-list ">
|
||||
<h2>{{ $t("Notifications") }}</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ $t("notificationDescription") }}
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||
{{ notification.name }}<br>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
{{ $t("Setup Notification") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<h2 class="mt-5">{{ $t("Info") }}</h2>
|
||||
|
||||
{{ $t("Version") }}: {{ $root.info.version }} <br />
|
||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
|
||||
<div class="mx-3">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="slide-fade" appear>
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
<TwoFADialog ref="TwoFADialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||
<template v-if="$i18n.locale === 'es-ES' ">
|
||||
<p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p>
|
||||
<p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p>
|
||||
<p>Por favor usar con cuidado.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'pt-BR' ">
|
||||
<p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p>
|
||||
<p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p>
|
||||
<p>Por favor, utilize isso com cautela.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'zh-HK' ">
|
||||
<p>你是否確認<strong>取消登入認証</strong>?</p>
|
||||
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p>
|
||||
<p>請小心使用。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'zh-CN' ">
|
||||
<p>是否确定 <strong>取消登录验证</strong>?</p>
|
||||
<p>这是为 <strong>有第三方认证</strong> 的用户提供的功能,如 Cloudflare Access</p>
|
||||
<p>请谨慎使用!</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'zh-TW' ">
|
||||
<p>你是否要<strong>取消登入驗證</strong>?</p>
|
||||
<p>此功能是設計給已有<strong>第三方認證</strong>的使用者,例如 Cloudflare Access。</p>
|
||||
<p>請謹慎使用。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'de-DE' ">
|
||||
<p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p>
|
||||
<p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p>
|
||||
<p>Bitte mit Vorsicht nutzen.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'sr' ">
|
||||
<p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p>
|
||||
<p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p>
|
||||
<p>Молим Вас користите ово са пажњом.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'sr-latn' ">
|
||||
<p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p>
|
||||
<p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p>
|
||||
<p>Molim Vas koristite ovo sa pažnjom.</p>
|
||||
</template>
|
||||
|
||||
<template v-if="$i18n.locale === 'hr-HR' ">
|
||||
<p>Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?</p>
|
||||
<p>To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.</p>
|
||||
<p>Pažljivo koristite ovu opciju.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'tr-TR' ">
|
||||
<p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p>
|
||||
<p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p>
|
||||
<p>Lütfen dikkatli kullanın.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'ko-KR' ">
|
||||
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
|
||||
<p>이 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong>을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.</p>
|
||||
<p>신중하게 사용하세요.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'pl' ">
|
||||
<p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p>
|
||||
<p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p>
|
||||
<p>Proszę używać ostrożnie.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'et-EE' ">
|
||||
<p>Kas soovid <strong>lülitada autentimise välja</strong>?</p>
|
||||
<p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p>
|
||||
<p>Palun kasuta vastutustundlikult.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'it-IT' ">
|
||||
<p>Si è certi di voler <strong>disabilitare l'autenticazione</strong>?</p>
|
||||
<p>È per <strong>chi ha l'autenticazione gestita da terze parti</strong> messa davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p>
|
||||
<p>Utilizzare con attenzione.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'id-ID' ">
|
||||
<p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p>
|
||||
<p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p>
|
||||
<p>Gunakan dengan hati-hati.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'ru-RU' ">
|
||||
<p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p>
|
||||
<p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p>
|
||||
<p>Пожалуйста, используйте с осторожностью.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'fa' ">
|
||||
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
|
||||
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
|
||||
<p>لطفا از این امکان با دقت استفاده کنید.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'bg-BG' ">
|
||||
<p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p>
|
||||
<p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p>
|
||||
<p>Моля, използвайте с повишено внимание.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'hu' ">
|
||||
<p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p>
|
||||
<p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p>
|
||||
<p>Használja megfontoltan!</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'nb-NO' ">
|
||||
<p>Er du sikker på at du vil <strong>deaktiver autentisering</strong>?</p>
|
||||
<p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p>
|
||||
<p>Vennligst vær forsiktig.</p>
|
||||
</template>
|
||||
|
||||
<!-- English (en) -->
|
||||
<template v-else>
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</template>
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmClearStatistics" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearStatistics">
|
||||
{{ $t("confirmClearStatisticsMsg") }}
|
||||
</Confirm>
|
||||
<Confirm ref="confirmImport" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="importBackup">
|
||||
{{ $t("confirmImportMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../components/HiddenInput.vue";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
import TwoFADialog from "../components/TwoFADialog.vue";
|
||||
import jwt_decode from "jwt-decode";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
import { timezoneList, setPageLocale } from "../util-frontend";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { debug } from "../util.ts";
|
||||
|
||||
const toast = useToast();
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationDialog,
|
||||
TwoFADialog,
|
||||
Confirm,
|
||||
HiddenInput,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
timezoneList: timezoneList(),
|
||||
guessTimezone: dayjs.tz.guess(),
|
||||
show: true,
|
||||
invalidPassword: false,
|
||||
password: {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
},
|
||||
settings: {
|
||||
|
||||
settings: {},
|
||||
settingsLoaded: false,
|
||||
|
||||
subMenus: {
|
||||
general: {
|
||||
title: this.$t("General"),
|
||||
},
|
||||
appearance: {
|
||||
title: this.$t("Appearance"),
|
||||
},
|
||||
notifications: {
|
||||
title: this.$t("Notifications"),
|
||||
},
|
||||
"monitor-history": {
|
||||
title: this.$t("Monitor History"),
|
||||
},
|
||||
security: {
|
||||
title: this.$t("Security"),
|
||||
},
|
||||
backup: {
|
||||
title: this.$t("Backup"),
|
||||
},
|
||||
about: {
|
||||
title: this.$t("About"),
|
||||
},
|
||||
},
|
||||
loaded: false,
|
||||
importAlert: null,
|
||||
importHandle: "skip",
|
||||
processing: false,
|
||||
databaseSize: 0,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
databaseSizeDisplay() {
|
||||
return Math.round(this.databaseSize / 1024 / 1024 * 10) / 10 + " MB";
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"password.repeatNewPassword"() {
|
||||
this.invalidPassword = false;
|
||||
},
|
||||
|
||||
"$i18n.locale"() {
|
||||
localStorage.locale = this.$i18n.locale;
|
||||
setPageLocale();
|
||||
currentPage() {
|
||||
let pathEnd = useRoute().path.split("/").at(-1);
|
||||
if (pathEnd == "settings" || pathEnd == null) {
|
||||
return "general";
|
||||
}
|
||||
return pathEnd;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadUsername();
|
||||
this.loadSettings();
|
||||
this.loadDatabaseSize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
saveGeneral() {
|
||||
localStorage.timezone = this.$root.userTimezone;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
savePassword() {
|
||||
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
||||
this.invalidPassword = true;
|
||||
} else {
|
||||
this.$root.getSocket().emit("changePassword", this.password, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
if (res.ok) {
|
||||
this.password.currentPassword = "";
|
||||
this.password.newPassword = "";
|
||||
this.password.repeatNewPassword = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadUsername() {
|
||||
const jwtPayload = this.$root.getJWTPayload();
|
||||
|
||||
if (jwtPayload) {
|
||||
this.username = jwtPayload.username;
|
||||
}
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
this.$root.getSocket().emit("getSettings", (res) => {
|
||||
this.settings = res.data;
|
||||
|
@ -538,7 +102,7 @@ export default {
|
|||
this.settings.keepDataPeriodDays = 180;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.settingsLoaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -548,116 +112,6 @@ export default {
|
|||
this.loadSettings();
|
||||
});
|
||||
},
|
||||
|
||||
confirmDisableAuth() {
|
||||
this.$refs.confirmDisableAuth.show();
|
||||
},
|
||||
|
||||
confirmClearStatistics() {
|
||||
this.$refs.confirmClearStatistics.show();
|
||||
},
|
||||
|
||||
confirmImport() {
|
||||
this.$refs.confirmImport.show();
|
||||
},
|
||||
|
||||
disableAuth() {
|
||||
this.settings.disableAuth = true;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
enableAuth() {
|
||||
this.settings.disableAuth = false;
|
||||
this.saveSettings();
|
||||
this.$root.storage().removeItem("token");
|
||||
location.reload();
|
||||
},
|
||||
|
||||
downloadBackup() {
|
||||
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||
let monitorList = Object.values(this.$root.monitorList);
|
||||
let exportData = {
|
||||
version: this.$root.info.version,
|
||||
notificationList: this.$root.notificationList,
|
||||
monitorList: monitorList,
|
||||
};
|
||||
exportData = JSON.stringify(exportData, null, 4);
|
||||
let downloadItem = document.createElement("a");
|
||||
downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURIComponent(exportData));
|
||||
downloadItem.setAttribute("download", fileName);
|
||||
downloadItem.click();
|
||||
},
|
||||
|
||||
importBackup() {
|
||||
this.processing = true;
|
||||
let uploadItem = document.getElementById("importBackup").files;
|
||||
|
||||
if (uploadItem.length <= 0) {
|
||||
this.processing = false;
|
||||
return this.importAlert = this.$t("alertNoFile");
|
||||
}
|
||||
|
||||
if (uploadItem.item(0).type !== "application/json") {
|
||||
this.processing = false;
|
||||
return this.importAlert = this.$t("alertWrongFileType");
|
||||
}
|
||||
|
||||
let fileReader = new FileReader();
|
||||
fileReader.readAsText(uploadItem.item(0));
|
||||
|
||||
fileReader.onload = item => {
|
||||
this.$root.uploadBackup(item.target.result, this.importHandle, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
clearStatistics() {
|
||||
this.$root.clearStatistics((res) => {
|
||||
if (res.ok) {
|
||||
this.$router.go();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
autoGetPrimaryBaseURL() {
|
||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||
},
|
||||
|
||||
shrinkDatabase() {
|
||||
this.$root.getSocket().emit("shrinkDatabase", (res) => {
|
||||
if (res.ok) {
|
||||
this.loadDatabaseSize();
|
||||
toast.success("Done");
|
||||
} else {
|
||||
debug(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadDatabaseSize() {
|
||||
debug("load database size");
|
||||
this.$root.getSocket().emit("getDatabaseSize", (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
this.databaseSize = res.size;
|
||||
debug("database size: " + res.size);
|
||||
} else {
|
||||
debug(res);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -667,37 +121,7 @@ export default {
|
|||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.btn-check:active + .btn-outline-primary,
|
||||
.btn-check:checked + .btn-outline-primary,
|
||||
.btn-check:hover + .btn-outline-primary {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.list-group-item {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.btn-check:active + .btn-outline-primary,
|
||||
.btn-check:checked + .btn-outline-primary,
|
||||
.btn-check:hover + .btn-outline-primary {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#importBackup {
|
||||
&::file-selector-button {
|
||||
color: $primary;
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||
color: $dark-font-color2;
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
min-height: calc(100vh - 155px);
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -707,4 +131,59 @@ footer {
|
|||
padding-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-menu {
|
||||
flex: 0 0 auto;
|
||||
width: 300px;
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
border-radius: 10px;
|
||||
margin: 0.5em;
|
||||
padding: 0.7em 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: $highlight-white;
|
||||
|
||||
.dark & {
|
||||
background: $dark-header-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.active .menu-item {
|
||||
background: $highlight-white;
|
||||
border-left: 4px solid $primary;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
.dark & {
|
||||
background: $dark-header-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - 300px);
|
||||
|
||||
.settings-content-header {
|
||||
width: calc(100% + 20px);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 0 10px 0 0;
|
||||
margin-top: -20px;
|
||||
margin-right: -20px;
|
||||
padding: 12.5px 1em;
|
||||
font-size: 26px;
|
||||
|
||||
.dark & {
|
||||
background: $dark-header-bg;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,6 +11,14 @@ import Setup from "./pages/Setup.vue";
|
|||
const StatusPage = () => import("./pages/StatusPage.vue");
|
||||
import Entry from "./pages/Entry.vue";
|
||||
|
||||
import Appearance from "./components/settings/Appearance.vue";
|
||||
import General from "./components/settings/General.vue";
|
||||
import Notifications from "./components/settings/Notifications.vue";
|
||||
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||
import Security from "./components/settings/Security.vue";
|
||||
import Backup from "./components/settings/Backup.vue";
|
||||
import About from "./components/settings/About.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
|
@ -59,6 +67,37 @@ const routes = [
|
|||
{
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
children: [
|
||||
{
|
||||
path: "general",
|
||||
alias: "",
|
||||
component: General,
|
||||
},
|
||||
{
|
||||
path: "appearance",
|
||||
component: Appearance,
|
||||
},
|
||||
{
|
||||
path: "notifications",
|
||||
component: Notifications,
|
||||
},
|
||||
{
|
||||
path: "monitor-history",
|
||||
component: MonitorHistory,
|
||||
},
|
||||
{
|
||||
path: "security",
|
||||
component: Security,
|
||||
},
|
||||
{
|
||||
path: "backup",
|
||||
component: Backup,
|
||||
},
|
||||
{
|
||||
path: "about",
|
||||
component: About,
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
106
test/e2e.spec.js
106
test/e2e.spec.js
|
@ -59,18 +59,37 @@ describe("Init", () => {
|
|||
|
||||
// Go to /
|
||||
await page.goto(baseURL);
|
||||
await sleep(3000);
|
||||
await page.waitForSelector("h1.mb-3");
|
||||
pathname = await page.evaluate(() => location.pathname);
|
||||
expect(pathname).toEqual("/dashboard");
|
||||
});
|
||||
|
||||
it("should create monitor", async () => {
|
||||
// Create monitor
|
||||
await page.goto(baseURL + "/add");
|
||||
await page.waitForSelector("#name");
|
||||
|
||||
await page.type("#name", "Myself");
|
||||
await page.waitForSelector("#url");
|
||||
await page.click("#url", { clickCount: 3 });
|
||||
await page.keyboard.type(baseURL);
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
const badge = document.querySelector("span.badge");
|
||||
return badge && badge.innerText == "100%";
|
||||
}, { timeout: 5000 });
|
||||
|
||||
});
|
||||
|
||||
// Settings Page
|
||||
describe("Settings", () => {
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
await page.goto(baseURL + "/settings");
|
||||
});
|
||||
|
||||
it("Change Language", async () => {
|
||||
await page.goto(baseURL + "/settings/appearance");
|
||||
await page.waitForSelector("#language");
|
||||
|
||||
await page.select("#language", "zh-HK");
|
||||
|
@ -83,20 +102,33 @@ describe("Init", () => {
|
|||
});
|
||||
|
||||
it("Change Theme", async () => {
|
||||
await sleep(1000);
|
||||
await page.goto(baseURL + "/settings/appearance");
|
||||
|
||||
// Dark
|
||||
await click(page, ".btn[for=btncheck2]");
|
||||
await page.waitForSelector("div.dark");
|
||||
|
||||
await sleep(1000);
|
||||
await page.waitForSelector(".btn[for=btncheck1]");
|
||||
|
||||
// Light
|
||||
await click(page, ".btn[for=btncheck1]");
|
||||
await page.waitForSelector("div.light");
|
||||
});
|
||||
|
||||
// TODO: Heartbeat Bar Style
|
||||
it("Change Heartbeat Bar Style", async () => {
|
||||
await page.goto(baseURL + "/settings/appearance");
|
||||
|
||||
// Bottom
|
||||
await click(page, ".btn[for=btncheck5]");
|
||||
await page.waitForSelector("div.hp-bar-big");
|
||||
|
||||
// None
|
||||
await click(page, ".btn[for=btncheck6]");
|
||||
await page.waitForSelector("div.hp-bar-big", {
|
||||
hidden: true,
|
||||
timeout: 1000
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Timezone
|
||||
|
||||
|
@ -108,14 +140,14 @@ describe("Init", () => {
|
|||
// Yes
|
||||
await click(page, "#searchEngineIndexYes");
|
||||
await click(page, "form > div > .btn[type=submit]");
|
||||
await sleep(2000);
|
||||
await sleep(1000);
|
||||
res = await axios.get(baseURL + "/robots.txt");
|
||||
expect(res.data).not.toContain("Disallow: /");
|
||||
|
||||
// No
|
||||
await click(page, "#searchEngineIndexNo");
|
||||
await click(page, "form > div > .btn[type=submit]");
|
||||
await sleep(2000);
|
||||
await sleep(1000);
|
||||
res = await axios.get(baseURL + "/robots.txt");
|
||||
expect(res.data).toContain("Disallow: /");
|
||||
});
|
||||
|
@ -125,25 +157,25 @@ describe("Init", () => {
|
|||
|
||||
// Default
|
||||
await newPage.goto(baseURL);
|
||||
await sleep(3000);
|
||||
await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
|
||||
let pathname = await newPage.evaluate(() => location.pathname);
|
||||
expect(pathname).toEqual("/dashboard");
|
||||
|
||||
// Status Page
|
||||
await click(page, "#entryPageNo");
|
||||
await click(page, "form > div > .btn[type=submit]");
|
||||
await sleep(4000);
|
||||
await sleep(1000);
|
||||
await newPage.goto(baseURL);
|
||||
await sleep(4000);
|
||||
await newPage.waitForSelector("img.logo", { timeout: 3000 });
|
||||
pathname = await newPage.evaluate(() => location.pathname);
|
||||
expect(pathname).toEqual("/status");
|
||||
|
||||
// Back to Dashboard
|
||||
await click(page, "#entryPageYes");
|
||||
await click(page, "form > div > .btn[type=submit]");
|
||||
await sleep(4000);
|
||||
await sleep(1000);
|
||||
await newPage.goto(baseURL);
|
||||
await sleep(4000);
|
||||
await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
|
||||
pathname = await newPage.evaluate(() => location.pathname);
|
||||
expect(pathname).toEqual("/dashboard");
|
||||
|
||||
|
@ -151,7 +183,7 @@ describe("Init", () => {
|
|||
});
|
||||
|
||||
it("Change Password (wrong current password)", async () => {
|
||||
await page.goto(baseURL + "/settings");
|
||||
await page.goto(baseURL + "/settings/security");
|
||||
await page.waitForSelector("#current-password");
|
||||
|
||||
await page.type("#current-password", "wrong_passw$$d");
|
||||
|
@ -159,10 +191,10 @@ describe("Init", () => {
|
|||
await page.type("#repeat-new-password", "new_password123");
|
||||
|
||||
// Save
|
||||
await click(page, "form > div > .btn[type=submit]", 1);
|
||||
await sleep(4000);
|
||||
await click(page, "form > div > .btn[type=submit]", 0);
|
||||
await sleep(1000);
|
||||
|
||||
await click(page, ".btn-danger.btn.me-2");
|
||||
await click(page, "#logout-btn");
|
||||
await login("admin", "new_password123");
|
||||
let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
|
||||
expect(elementCount).toEqual(1);
|
||||
|
@ -171,24 +203,26 @@ describe("Init", () => {
|
|||
});
|
||||
|
||||
it("Change Password (wrong repeat)", async () => {
|
||||
await page.goto(baseURL + "/settings");
|
||||
await page.goto(baseURL + "/settings/security");
|
||||
await page.waitForSelector("#current-password");
|
||||
|
||||
await page.type("#current-password", "admin123");
|
||||
await page.type("#new-password", "new_password123");
|
||||
await page.type("#repeat-new-password", "new_password1234567898797898");
|
||||
|
||||
await click(page, "form > div > .btn[type=submit]", 1);
|
||||
await sleep(4000);
|
||||
await click(page, "form > div > .btn[type=submit]", 0);
|
||||
await sleep(1000);
|
||||
|
||||
await click(page, ".btn-danger.btn.me-2");
|
||||
await click(page, "#logout-btn");
|
||||
await login("admin", "new_password123");
|
||||
|
||||
let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
|
||||
expect(elementCount).toEqual(1);
|
||||
|
||||
await login("admin", "admin123");
|
||||
await sleep(3000);
|
||||
await page.waitForSelector("#current-password");
|
||||
let pathname = await page.evaluate(() => location.pathname);
|
||||
expect(pathname).toEqual("/settings/security");
|
||||
});
|
||||
|
||||
// TODO: 2FA
|
||||
|
@ -197,9 +231,35 @@ describe("Init", () => {
|
|||
|
||||
// TODO: Import Backup
|
||||
|
||||
// TODO: Disable Auth
|
||||
it("Should disable & enable auth", async () => {
|
||||
await page.goto(baseURL + "/settings/security");
|
||||
await click(page, "#disableAuth-btn");
|
||||
await click(page, ".btn.btn-danger[data-bs-dismiss='modal']", 2); // Not a good way to do it
|
||||
await page.waitForSelector("#enableAuth-btn", { timeout: 3000 });
|
||||
await page.waitForSelector("#logout-btn", {
|
||||
hidden: true,
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
// TODO: Clear Stats
|
||||
const newPage = await browser.newPage();
|
||||
await newPage.goto(baseURL);
|
||||
await newPage.waitForSelector("span.badge", { timeout: 3000 });
|
||||
newPage.close();
|
||||
|
||||
await click(page, "#enableAuth-btn");
|
||||
await login("admin", "admin123");
|
||||
await page.waitForSelector("#disableAuth-btn", { timeout: 3000 });
|
||||
});
|
||||
|
||||
// it("Should clear all statistics", async () => {
|
||||
// await page.goto(baseURL + "/settings/monitor-history");
|
||||
// await click(page, "#clearAllStats-btn");
|
||||
// await click(page, ".btn.btn-danger");
|
||||
// await page.waitForFunction(() => {
|
||||
// const badge = document.querySelector("span.badge");
|
||||
// return badge && badge.innerText == "0%";
|
||||
// }, { timeout: 3000 });
|
||||
// });
|
||||
});
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in a new issue