parent
8e441dd8f7
commit
966dfa6f88
211
server/server.js
211
server/server.js
|
@ -65,8 +65,6 @@ log.debug("server", "Importing http-graceful-shutdown");
|
||||||
const gracefulShutdown = require("http-graceful-shutdown");
|
const gracefulShutdown = require("http-graceful-shutdown");
|
||||||
log.debug("server", "Importing prometheus-api-metrics");
|
log.debug("server", "Importing prometheus-api-metrics");
|
||||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||||
log.debug("server", "Importing compare-versions");
|
|
||||||
const compareVersions = require("compare-versions");
|
|
||||||
const { passwordStrength } = require("check-password-strength");
|
const { passwordStrength } = require("check-password-strength");
|
||||||
|
|
||||||
log.debug("server", "Importing 2FA Modules");
|
log.debug("server", "Importing 2FA Modules");
|
||||||
|
@ -91,9 +89,6 @@ log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
Notification.init();
|
Notification.init();
|
||||||
|
|
||||||
log.debug("server", "Importing Proxy");
|
|
||||||
const { Proxy } = require("./proxy");
|
|
||||||
|
|
||||||
log.debug("server", "Importing Database");
|
log.debug("server", "Importing Database");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
|
|
||||||
|
@ -1434,212 +1429,6 @@ let needSetup = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("uploadBackup", async (uploadedJSON, importHandle, callback) => {
|
|
||||||
try {
|
|
||||||
checkLogin(socket);
|
|
||||||
|
|
||||||
let backupData = JSON.parse(uploadedJSON);
|
|
||||||
|
|
||||||
log.info("manage", `Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
|
||||||
|
|
||||||
let notificationListData = backupData.notificationList;
|
|
||||||
let proxyListData = backupData.proxyList;
|
|
||||||
let monitorListData = backupData.monitorList;
|
|
||||||
|
|
||||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
|
||||||
|
|
||||||
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
|
||||||
if (importHandle === "overwrite") {
|
|
||||||
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
|
||||||
for (let id in server.monitorList) {
|
|
||||||
let monitor = server.monitorList[id];
|
|
||||||
await monitor.stop();
|
|
||||||
}
|
|
||||||
await R.exec("DELETE FROM heartbeat");
|
|
||||||
await R.exec("DELETE FROM monitor_notification");
|
|
||||||
await R.exec("DELETE FROM monitor_tls_info");
|
|
||||||
await R.exec("DELETE FROM notification");
|
|
||||||
await R.exec("DELETE FROM monitor_tag");
|
|
||||||
await R.exec("DELETE FROM tag");
|
|
||||||
await R.exec("DELETE FROM monitor");
|
|
||||||
await R.exec("DELETE FROM proxy");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only starts importing if the backup file contains at least one notification
|
|
||||||
if (notificationListData.length >= 1) {
|
|
||||||
// Get every existing notification name and puts them in one simple string
|
|
||||||
let notificationNameList = await R.getAll("SELECT name FROM notification");
|
|
||||||
let notificationNameListString = JSON.stringify(notificationNameList);
|
|
||||||
|
|
||||||
for (let i = 0; i < notificationListData.length; i++) {
|
|
||||||
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
|
||||||
if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
|
||||||
|
|
||||||
let notification = JSON.parse(notificationListData[i].config);
|
|
||||||
await Notification.save(notification, null, socket.userID);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only starts importing if the backup file contains at least one proxy
|
|
||||||
if (proxyListData && proxyListData.length >= 1) {
|
|
||||||
const proxies = await R.findAll("proxy");
|
|
||||||
|
|
||||||
// Loop over proxy list and save proxies
|
|
||||||
for (const proxy of proxyListData) {
|
|
||||||
const exists = proxies.find(item => item.id === proxy.id);
|
|
||||||
|
|
||||||
// Do not process when proxy already exists in import handle is skip and keep
|
|
||||||
if ([ "skip", "keep" ].includes(importHandle) && !exists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save proxy as new entry if exists update exists one
|
|
||||||
await Proxy.save(proxy, exists ? proxy.id : undefined, proxy.userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only starts importing if the backup file contains at least one monitor
|
|
||||||
if (monitorListData.length >= 1) {
|
|
||||||
// Get every existing monitor name and puts them in one simple string
|
|
||||||
let monitorNameList = await R.getAll("SELECT name FROM monitor");
|
|
||||||
let monitorNameListString = JSON.stringify(monitorNameList);
|
|
||||||
|
|
||||||
for (let i = 0; i < monitorListData.length; i++) {
|
|
||||||
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
|
||||||
if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
|
||||||
|
|
||||||
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
|
|
||||||
// --- Start ---
|
|
||||||
|
|
||||||
// Define default values
|
|
||||||
let retryInterval = 0;
|
|
||||||
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
|
|
||||||
|
|
||||||
/*
|
|
||||||
Only replace the default value with the backup file data for the specific version, where it appears the first time
|
|
||||||
More information about that where "let version" will be defined
|
|
||||||
*/
|
|
||||||
if (version17x) {
|
|
||||||
retryInterval = monitorListData[i].retryInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- End ---
|
|
||||||
|
|
||||||
let monitor = {
|
|
||||||
// Define the new variable from earlier here
|
|
||||||
name: monitorListData[i].name,
|
|
||||||
description: monitorListData[i].description,
|
|
||||||
type: monitorListData[i].type,
|
|
||||||
url: monitorListData[i].url,
|
|
||||||
method: monitorListData[i].method || "GET",
|
|
||||||
body: monitorListData[i].body,
|
|
||||||
headers: monitorListData[i].headers,
|
|
||||||
authMethod: monitorListData[i].authMethod,
|
|
||||||
basic_auth_user: monitorListData[i].basic_auth_user,
|
|
||||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
|
||||||
authWorkstation: monitorListData[i].authWorkstation,
|
|
||||||
authDomain: monitorListData[i].authDomain,
|
|
||||||
timeout,
|
|
||||||
interval: monitorListData[i].interval,
|
|
||||||
retryInterval: retryInterval,
|
|
||||||
resendInterval: monitorListData[i].resendInterval || 0,
|
|
||||||
hostname: monitorListData[i].hostname,
|
|
||||||
maxretries: monitorListData[i].maxretries,
|
|
||||||
port: monitorListData[i].port,
|
|
||||||
keyword: monitorListData[i].keyword,
|
|
||||||
invertKeyword: monitorListData[i].invertKeyword,
|
|
||||||
ignoreTls: monitorListData[i].ignoreTls,
|
|
||||||
upsideDown: monitorListData[i].upsideDown,
|
|
||||||
maxredirects: monitorListData[i].maxredirects,
|
|
||||||
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
|
|
||||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
|
||||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
|
||||||
notificationIDList: monitorListData[i].notificationIDList,
|
|
||||||
proxy_id: monitorListData[i].proxy_id || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (monitorListData[i].pushToken) {
|
|
||||||
monitor.pushToken = monitorListData[i].pushToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bean = R.dispense("monitor");
|
|
||||||
|
|
||||||
let notificationIDList = monitor.notificationIDList;
|
|
||||||
delete monitor.notificationIDList;
|
|
||||||
|
|
||||||
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
|
||||||
delete monitor.accepted_statuscodes;
|
|
||||||
|
|
||||||
bean.import(monitor);
|
|
||||||
bean.user_id = socket.userID;
|
|
||||||
await R.store(bean);
|
|
||||||
|
|
||||||
// Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented
|
|
||||||
if (version17x) {
|
|
||||||
// Only import if the specific monitor has tags assigned
|
|
||||||
for (const oldTag of monitorListData[i].tags) {
|
|
||||||
|
|
||||||
// Check if tag already exists and get data ->
|
|
||||||
let tag = await R.findOne("tag", " name = ?", [
|
|
||||||
oldTag.name,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let tagId;
|
|
||||||
if (!tag) {
|
|
||||||
// -> If it doesn't exist, create new tag from backup file
|
|
||||||
let beanTag = R.dispense("tag");
|
|
||||||
beanTag.name = oldTag.name;
|
|
||||||
beanTag.color = oldTag.color;
|
|
||||||
await R.store(beanTag);
|
|
||||||
|
|
||||||
tagId = beanTag.id;
|
|
||||||
} else {
|
|
||||||
// -> If it already exist, set tagId to value from database
|
|
||||||
tagId = tag.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign the new created tag to the monitor
|
|
||||||
await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [
|
|
||||||
tagId,
|
|
||||||
bean.id,
|
|
||||||
oldTag.value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
|
||||||
|
|
||||||
// If monitor was active start it immediately, otherwise pause it
|
|
||||||
if (monitorListData[i].active === 1) {
|
|
||||||
await startMonitor(socket.userID, bean.id);
|
|
||||||
} else {
|
|
||||||
await pauseMonitor(socket.userID, bean.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendNotificationList(socket);
|
|
||||||
await server.sendMonitorList(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
msg: "successBackupRestored",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("clearEvents", async (monitorID, callback) => {
|
socket.on("clearEvents", async (monitorID, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
|
@ -1,225 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="my-4">
|
|
||||||
<div class="alert alert-warning" role="alert" style="border-radius: 15px;">
|
|
||||||
{{ $t("backupOutdatedWarning") }}<br />
|
|
||||||
<br />
|
|
||||||
{{ $t("backupRecommend") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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="import-backend"
|
|
||||||
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";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Confirm,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
processing: false,
|
|
||||||
importHandle: "skip",
|
|
||||||
importAlert: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* Show the confimation dialog confirming the configuration
|
|
||||||
* be imported
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
confirmImport() {
|
|
||||||
this.$refs.confirmImport.show();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download a backup of the configuration
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import the specified backup file
|
|
||||||
* @returns {string|void} Error message
|
|
||||||
*/
|
|
||||||
importBackup() {
|
|
||||||
this.processing = true;
|
|
||||||
let uploadItem = document.getElementById("import-backend").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;
|
|
||||||
|
|
||||||
this.$root.toastRes(res);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/vars.scss";
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
#import-backend {
|
|
||||||
&::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>
|
|
|
@ -113,9 +113,6 @@ export default {
|
||||||
proxies: {
|
proxies: {
|
||||||
title: this.$t("Proxies"),
|
title: this.$t("Proxies"),
|
||||||
},
|
},
|
||||||
backup: {
|
|
||||||
title: this.$t("Backup"),
|
|
||||||
},
|
|
||||||
about: {
|
about: {
|
||||||
title: this.$t("About"),
|
title: this.$t("About"),
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,7 +30,6 @@ import Tags from "./components/settings/Tags.vue";
|
||||||
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
const Security = () => import("./components/settings/Security.vue");
|
const Security = () => import("./components/settings/Security.vue");
|
||||||
import Proxies from "./components/settings/Proxies.vue";
|
import Proxies from "./components/settings/Proxies.vue";
|
||||||
import Backup from "./components/settings/Backup.vue";
|
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
@ -126,10 +125,6 @@ const routes = [
|
||||||
path: "proxies",
|
path: "proxies",
|
||||||
component: Proxies,
|
component: Proxies,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "backup",
|
|
||||||
component: Backup,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "about",
|
path: "about",
|
||||||
component: About,
|
component: About,
|
||||||
|
|
Loading…
Reference in New Issue