commit
807519d07d
@ -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 = {
|
module.exports = {
|
||||||
"launch": {
|
"launch": {
|
||||||
|
"dumpio": true,
|
||||||
|
"slowMo": 500,
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
"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",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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;
|
@ -0,0 +1,60 @@
|
|||||||
|
console.log("== Uptime Kuma Remove 2FA Tool ==");
|
||||||
|
console.log("Loading the database");
|
||||||
|
|
||||||
|
const Database = require("../server/database");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const readline = require("readline");
|
||||||
|
const TwoFA = require("../server/2fa");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Database.init(args);
|
||||||
|
await Database.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
const user = await R.findOne("user");
|
||||||
|
if (! user) {
|
||||||
|
throw new Error("user not found, have you installed?");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
let ans = await question("Are you sure want to remove 2FA? [y/N]");
|
||||||
|
|
||||||
|
if (ans.toLowerCase() === "y") {
|
||||||
|
await TwoFA.disable2FA(user.id);
|
||||||
|
console.log("2FA has been removed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("Finished.");
|
||||||
|
};
|
||||||
|
|
||||||
|
function question(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@
|
|||||||
|
const { checkLogin } = require("./util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class TwoFA {
|
||||||
|
|
||||||
|
static async disable2FA(userID) {
|
||||||
|
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TwoFA;
|
@ -0,0 +1,44 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SerwerSMS extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "serwersms";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
"username": notification.serwersmsUsername,
|
||||||
|
"password": notification.serwersmsPassword,
|
||||||
|
"phone": notification.serwersmsPhoneNumber,
|
||||||
|
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||||
|
"sender": notification.serwersmsSenderName,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config);
|
||||||
|
|
||||||
|
if (!resp.data.success) {
|
||||||
|
if (resp.data.error) {
|
||||||
|
let error = `SerwerSMS.pl API returned error code ${resp.data.error.code} (${resp.data.error.type}) with error message: ${resp.data.error.message}`;
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
} else {
|
||||||
|
let error = "SerwerSMS.pl API returned an unexpected response";
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SerwerSMS;
|
@ -0,0 +1,41 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
|
||||||
|
class Stackfield extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "stackfield";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
// Stackfield message formatting: https://www.stackfield.com/help/formatting-messages-2001
|
||||||
|
|
||||||
|
let textMsg = "+Uptime Kuma Alert+";
|
||||||
|
|
||||||
|
if (monitorJSON && monitorJSON.name) {
|
||||||
|
textMsg += `\n*${monitorJSON.name}*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += `\n${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL) {
|
||||||
|
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"Title": textMsg,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.stackfieldwebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Stackfield;
|
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-username" class="form-label">{{ $t('serwersmsAPIUser') }}</label>
|
||||||
|
<input id="serwersms-username" v-model="$parent.notification.serwersmsUsername" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
|
||||||
|
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>
|
||||||
|
<input id="serwersms-phone-number" v-model="$parent.notification.serwersmsPhoneNumber" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serwersms-sender-name" class="form-label">{{ $t("serwersmsSenderName") }}</label>
|
||||||
|
<input id="serwersms-sender-name" v-model="$parent.notification.serwersmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stackfield-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="stackfield-webhook-url" v-model="$parent.notification.stackfieldwebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://www.stackfield.com/developer-api#AnchorAPI2" target="_blank">https://www.stackfield.com/developer-api#AnchorAPI2</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -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>
|
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="language" class="form-label">
|
||||||
|
{{ $t("Language") }}
|
||||||
|
</label>
|
||||||
|
<select id="language" v-model="$root.language" 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>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</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>
|
@ -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>
|
@ -0,0 +1,192 @@
|
|||||||
|
<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"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
/>
|
||||||
|
<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>
|
@ -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>
|
@ -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>
|
@ -0,0 +1,33 @@
|
|||||||
|
import { currentLocale } from "../i18n";
|
||||||
|
import { setPageLocale } from "../util-frontend";
|
||||||
|
const langModules = import.meta.glob("../languages/*.js");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
language: currentLocale(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
if (this.language !== "en") {
|
||||||
|
await this.changeLang(this.language);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async language(lang) {
|
||||||
|
await this.changeLang(lang);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async changeLang(lang) {
|
||||||
|
let message = (await langModules["../languages/" + lang + ".js"]()).default;
|
||||||
|
this.$i18n.setLocaleMessage(lang, message);
|
||||||
|
this.$i18n.locale = lang;
|
||||||
|
localStorage.locale = lang;
|
||||||
|
setPageLocale();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in new issue