Merge remote-tracking branch 'origin/master'

This commit is contained in:
LouisLam 2021-09-13 00:38:17 +08:00
commit 02247c4174
26 changed files with 518 additions and 78 deletions

10
db/patch-2fa.sql Normal file
View 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 user
ADD twofa_secret VARCHAR(64);
ALTER TABLE user
ADD twofa_status BOOLEAN default 0 NOT NULL;
COMMIT;

View File

@ -58,20 +58,24 @@
"http-graceful-shutdown": "^3.1.4", "http-graceful-shutdown": "^3.1.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3", "nodemailer": "^6.6.3",
"notp": "^2.0.3",
"password-hash": "^1.2.2", "password-hash": "^1.2.2",
"prom-client": "^13.2.0", "prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0", "prometheus-api-metrics": "^3.2.0",
"qrcode": "^1.4.4",
"redbean-node": "0.1.2", "redbean-node": "0.1.2",
"socket.io": "^4.2.0", "socket.io": "^4.2.0",
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"sqlite3": "github:mapbox/node-sqlite3#593c9d", "sqlite3": "github:mapbox/node-sqlite3#593c9d",
"tcp-ping": "^0.1.1", "tcp-ping": "^0.1.1",
"thirty-two": "^1.0.2",
"v-pagination-3": "^0.1.6", "v-pagination-3": "^0.1.6",
"vue": "^3.2.8", "vue": "^3.2.8",
"vue-chart-3": "^0.5.7", "vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "^1.0.2",
"vue-i18n": "^9.1.7", "vue-i18n": "^9.1.7",
"vue-multiselect": "^3.0.0-alpha.2", "vue-multiselect": "^3.0.0-alpha.2",
"vue-qrcode": "^1.0.0",
"vue-router": "^4.0.11", "vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"
}, },

View File

@ -30,6 +30,7 @@ class Database {
static patchList = { static patchList = {
"patch-setting-value-type.sql": true, "patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true, "patch-improve-performance.sql": true,
"patch-2fa.sql": true,
} }
/** /**

View File

@ -22,11 +22,15 @@ const gracefulShutdown = require("http-graceful-shutdown");
debug("Importing prometheus-api-metrics"); debug("Importing prometheus-api-metrics");
const prometheusAPIMetrics = require("prometheus-api-metrics"); const prometheusAPIMetrics = require("prometheus-api-metrics");
debug("Importing 2FA Modules");
const notp = require("notp");
const base32 = require("thirty-two");
console.log("Importing this project modules"); console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret, genSecret } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -219,12 +223,38 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user) { if (user) {
afterLogin(socket, user) afterLogin(socket, user)
callback({ if (user.twofaStatus == 0) {
ok: true, callback({
token: jwt.sign({ ok: true,
username: data.username, token: jwt.sign({
}, jwtSecret), username: data.username,
}) }, jwtSecret),
})
}
if (user.twofaStatus == 1 && !data.token) {
callback({
tokenRequired: true,
})
}
if (data.token) {
let verify = notp.totp.verify(data.token, user.twofa_secret);
if (verify && verify.delta == 0) {
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
})
} else {
callback({
ok: false,
msg: "Invalid Token!",
})
}
}
} else { } else {
callback({ callback({
ok: false, ok: false,
@ -240,6 +270,130 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
callback(); callback();
}); });
socket.on("prepare2FA", async (callback) => {
try {
checkLogin(socket)
let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID,
])
if (user.twofa_status == 0) {
let newSecret = await genSecret()
let encodedSecret = base32.encode(newSecret);
let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [
newSecret,
socket.userID,
]);
callback({
ok: true,
uri: uri,
})
} else {
callback({
ok: false,
msg: "2FA is already enabled.",
})
}
} catch (error) {
callback({
ok: false,
msg: "Error while trying to prepare 2FA.",
})
}
});
socket.on("save2FA", async (callback) => {
try {
checkLogin(socket)
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
socket.userID,
]);
callback({
ok: true,
msg: "2FA Enabled.",
})
} catch (error) {
callback({
ok: false,
msg: "Error while trying to change 2FA.",
})
}
});
socket.on("disable2FA", async (callback) => {
try {
checkLogin(socket)
await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
socket.userID,
]);
callback({
ok: true,
msg: "2FA Disabled.",
})
} catch (error) {
callback({
ok: false,
msg: "Error while trying to change 2FA.",
})
}
});
socket.on("verifyToken", async (token, callback) => {
let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID,
])
let verify = notp.totp.verify(token, user.twofa_secret);
if (verify && verify.delta == 0) {
callback({
ok: true,
valid: true,
})
} else {
callback({
ok: false,
msg: "Invalid Token.",
valid: false,
})
}
});
socket.on("twoFAStatus", async (callback) => {
checkLogin(socket)
try {
let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID,
])
if (user.twofa_status == 1) {
callback({
ok: true,
status: true,
})
} else {
callback({
ok: true,
status: false,
})
}
} catch (error) {
callback({
ok: false,
msg: "Error while trying to get 2FA status.",
})
}
});
socket.on("needSetup", async (callback) => { socket.on("needSetup", async (callback) => {
callback(needSetup); callback(needSetup);
}); });

View File

@ -271,3 +271,13 @@ exports.getTotalClientInRoom = (io, roomName) => {
return 0; return 0;
} }
} }
exports.genSecret = () => {
let secret = "";
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charsLength = chars.length;
for ( let i = 0; i < 64; i++ ) {
secret += chars.charAt(Math.floor(Math.random() * charsLength));
}
return secret;
}

View File

@ -4,16 +4,23 @@
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<h1 class="h3 mb-3 fw-normal" /> <h1 class="h3 mb-3 fw-normal" />
<div class="form-floating"> <div v-if="!tokenRequired" class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username"> <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">{{ $t("Username") }}</label> <label for="floatingInput">{{ $t("Username") }}</label>
</div> </div>
<div class="form-floating mt-3"> <div v-if="!tokenRequired" class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password"> <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">{{ $t("Password") }}</label> <label for="floatingPassword">{{ $t("Password") }}</label>
</div> </div>
<div v-if="tokenRequired">
<div class="form-floating mt-3">
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
<label for="floatingToken">{{ $t("Token") }}</label>
</div>
</div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4"> <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
<div class="form-check"> <div class="form-check">
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input"> <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
@ -42,16 +49,24 @@ export default {
processing: false, processing: false,
username: "", username: "",
password: "", password: "",
token: "",
res: null, res: null,
tokenRequired: false,
} }
}, },
methods: { methods: {
submit() { submit() {
this.processing = true; this.processing = true;
this.$root.login(this.username, this.password, (res) => {
this.$root.login(this.username, this.password, this.token, (res) => {
this.processing = false; this.processing = false;
this.res = res; console.log(res)
if (res.tokenRequired) {
this.tokenRequired = true;
} else {
this.res = res;
}
}) })
}, },
}, },

View File

@ -410,7 +410,7 @@
<div class="form-check form-switch"> <div class="form-check form-switch">
<input v-model="notification.applyExisting" class="form-check-input" type="checkbox"> <input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Also apply to existing monitors") }}</label> <label class="form-check-label">{{ $t("Apply on all existing monitors") }}</label>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,178 @@
<template>
<form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("Setup 2FA") }}
<span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
</h5>
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<div v-if="uri && twoFAStatus == false" class="mx-auto text-center" style="width: 210px;">
<vue-qrcode :key="uri" :value="uri" type="image/png" :quality="1" :color="{ light: '#ffffffff' }" />
<button v-show="!showURI" type="button" class="btn btn-outline-primary btn-sm mt-2" @click="showURI = true">{{ $t("Show URI") }}</button>
</div>
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
{{ $t("Enable 2FA") }}
</button>
<button v-if="twoFAStatus == true" class="btn btn-danger" type="button" :disabled="processing" @click="confirmDisableTwoFA()">
{{ $t("Disable 2FA") }}
</button>
<div v-if="uri && twoFAStatus == false" class="mt-3">
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
<div class="input-group">
<input v-model="token" type="text" maxlength="6" class="form-control">
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
</div>
<p v-show="tokenValid" class="mt-2" style="color: green">{{ $t("tokenValidSettingsMsg") }}</p>
</div>
</div>
</div>
<div v-if="uri && twoFAStatus == false" class="modal-footer">
<button type="submit" class="btn btn-primary" :disabled="processing || tokenValid == false" @click="confirmEnableTwoFA()">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
{{ $t("confirmEnableTwoFAMsg") }}
</Confirm>
<Confirm ref="confirmDisableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disable2FA">
{{ $t("confirmDisableTwoFAMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap"
import Confirm from "./Confirm.vue";
import VueQrcode from "vue-qrcode"
import { useToast } from "vue-toastification"
const toast = useToast()
export default {
components: {
Confirm,
VueQrcode,
},
props: {},
data() {
return {
processing: false,
uri: null,
tokenValid: false,
twoFAStatus: null,
token: null,
showURI: false,
}
},
mounted() {
this.modal = new Modal(this.$refs.modal)
this.getStatus();
},
methods: {
show() {
this.modal.show()
},
confirmEnableTwoFA() {
this.$refs.confirmEnableTwoFA.show()
},
confirmDisableTwoFA() {
this.$refs.confirmDisableTwoFA.show()
},
prepare2FA() {
this.processing = true;
this.$root.getSocket().emit("prepare2FA", (res) => {
this.processing = false;
if (res.ok) {
this.uri = res.uri;
} else {
toast.error(res.msg);
}
})
},
save2FA() {
this.processing = true;
this.$root.getSocket().emit("save2FA", (res) => {
this.processing = false;
if (res.ok) {
this.$root.toastRes(res)
this.getStatus();
this.modal.hide();
} else {
toast.error(res.msg);
}
})
},
disable2FA() {
this.processing = true;
this.$root.getSocket().emit("disable2FA", (res) => {
this.processing = false;
if (res.ok) {
this.$root.toastRes(res)
this.getStatus();
this.modal.hide();
} else {
toast.error(res.msg);
}
})
},
verifyToken() {
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
if (res.ok) {
this.tokenValid = res.valid;
} else {
toast.error(res.msg);
}
})
},
getStatus() {
this.$root.getSocket().emit("twoFAStatus", (res) => {
if (res.ok) {
this.twoFAStatus = res.status;
} else {
toast.error(res.msg);
}
})
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

View File

@ -17,8 +17,8 @@ export default {
Down: "Inaktiv", Down: "Inaktiv",
Pending: "Afventer", Pending: "Afventer",
Unknown: "Ukendt", Unknown: "Ukendt",
Pause: "Pause", Pause: "Stands",
pauseDashboardHome: "Pauset", pauseDashboardHome: "Standset",
Name: "Navn", Name: "Navn",
Status: "Status", Status: "Status",
DateTime: "Dato / Tid", DateTime: "Dato / Tid",
@ -36,8 +36,8 @@ export default {
hour: "Timer", hour: "Timer",
"-hour": "-Timer", "-hour": "-Timer",
checkEverySecond: "Tjek hvert {0} sekund", checkEverySecond: "Tjek hvert {0} sekund",
"Avg.": "Gennemsnit", "Avg.": "Gns.",
Response: " Respons", Response: "Respons",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Overvåger Type", "Monitor Type": "Overvåger Type",
Keyword: "Nøgleord", Keyword: "Nøgleord",
@ -103,29 +103,29 @@ export default {
"Resolver Server": "Navne-server", "Resolver Server": "Navne-server",
rrtypeDescription: "Vælg den type RR, du vil overvåge.", rrtypeDescription: "Vælg den type RR, du vil overvåge.",
"Last Result": "Seneste resultat", "Last Result": "Seneste resultat",
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?", pauseMonitorMsg: "Er du sikker på, at du vil standse Overvågeren?",
"Create your admin account": "Opret din administratorkonto", "Create your admin account": "Opret din administratorkonto",
"Repeat Password": "Gentag adgangskoden", "Repeat Password": "Gentag adgangskoden",
"Resource Record Type": "Resource Record Type", "Resource Record Type": "Resource Record Type",
respTime: "Resp. Time (ms)", respTime: "Resp. Tid (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
Create: "Create", Create: "Opret",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "Er du sikker på vil slette alle events for denne Overvåger?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "Er du sikker på vil slette alle heartbeats for denne Overvåger?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "Vil du helt sikkert slette ALLE statistikker?",
"Clear Data": "Clear Data", "Clear Data": "Ryd Data",
Events: "Events", Events: "Events",
Heartbeats: "Heartbeats", Heartbeats: "Heartbeats",
"Auto Get": "Auto Get", "Auto Get": "Auto-hent",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
"Default enabled": "Default enabled", "Default enabled": "Standard aktiveret",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Anvend også på eksisterende overvågere",
"Import/Export Backup": "Import/Export Backup", "Import/Export Backup": " Importér/Eksportér sikkerhedskopi",
Export: "Export", Export: "Eksport",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
backupDescription2: "PS: History and event data is not included.", backupDescription2: "PS: Historik og hændelsesdata er ikke inkluderet.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
alertNoFile: "Please select a file to import.", alertNoFile: "Vælg en fil der skal importeres.",
alertWrongFileType: "Please select a JSON file." alertWrongFileType: "Vælg venligst en JSON-fil."
} }

View File

@ -36,8 +36,8 @@ export default {
hour: "Stunde", hour: "Stunde",
"-hour": "-Stunden", "-hour": "-Stunden",
checkEverySecond: "Überprüfe alle {0} Sekunden", checkEverySecond: "Überprüfe alle {0} Sekunden",
"Avg.": "Durchschn. ", "Avg.": "Durchschn.",
Response: " Antwortzeit", Response: "Antwortzeit",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Monitor Typ", "Monitor Type": "Monitor Typ",
Keyword: "Schlüsselwort", Keyword: "Schlüsselwort",
@ -119,7 +119,7 @@ export default {
respTime: "Antw. Zeit (ms)", respTime: "Antw. Zeit (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
"Default enabled": "Standardmäßig aktiviert", "Default enabled": "Standardmäßig aktiviert",
"Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren", "Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.", enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
Create: "Erstellen", Create: "Erstellen",
"Auto Get": "Auto Get", "Auto Get": "Auto Get",
@ -128,5 +128,19 @@ export default {
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.", backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
alertNoFile: "Bitte wähle eine Datei zum importieren aus.", alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
alertWrongFileType: "Bitte wähle eine JSON Datei aus.", alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
"Verify Token": "Token verifizieren",
"Setup 2FA": "2FA Einrichten",
"Enable 2FA": "2FA Aktivieren",
"Disable 2FA": "2FA deaktivieren",
"2FA Settings": "2FA Einstellungen",
confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
"Two Factor Authentication": "Zwei Faktor Authentifizierung",
Active: "Aktiv",
Inactive: "Inaktiv",
Token: "Token",
"Show URI": "URI Anzeigen",
"Clear all statistics": "Lösche alle Statistiken" "Clear all statistics": "Lösche alle Statistiken"
} }

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "English", languageName: "English",
checkEverySecond: "Check every {0} seconds.", checkEverySecond: "Check every {0} seconds.",
"Avg.": "Avg. ", "Avg.": "Avg.",
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent", retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites", ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.", upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
@ -20,6 +20,10 @@ export default {
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
Settings: "Settings", Settings: "Settings",
Dashboard: "Dashboard", Dashboard: "Dashboard",
"New Update": "New Update", "New Update": "New Update",
@ -117,7 +121,7 @@ export default {
respTime: "Resp. Time (ms)", respTime: "Resp. Time (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Apply on all existing monitors": "Apply on all existing monitors",
Create: "Create", Create: "Create",
"Clear Data": "Clear Data", "Clear Data": "Clear Data",
Events: "Events", Events: "Events",
@ -128,5 +132,15 @@ export default {
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.", alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.", alertWrongFileType: "Please select a JSON file.",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics" "Clear all statistics": "Clear all Statistics"
} }

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Español", languageName: "Español",
checkEverySecond: "Comprobar cada {0} segundos.", checkEverySecond: "Comprobar cada {0} segundos.",
"Avg.": "Media. ", "Avg.": "Media.",
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.", retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS", ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.", upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "eesti", languageName: "eesti",
checkEverySecond: "Kontrolli {0} sekundilise vahega.", checkEverySecond: "Kontrolli {0} sekundilise vahega.",
"Avg.": "≈ ", "Avg.": "≈",
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.", retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.", ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.", upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
@ -10,7 +10,7 @@ export default {
passwordNotMatchMsg: "Salasõnad ei kattu.", passwordNotMatchMsg: "Salasõnad ei kattu.",
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.", notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)", keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
pauseDashboardHome: "Seiskamine", pauseDashboardHome: "Seismas",
deleteMonitorMsg: "Kas soovid eemaldada seire?", deleteMonitorMsg: "Kas soovid eemaldada seire?",
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?", deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.", resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
@ -109,23 +109,23 @@ export default {
"Repeat Password": "korda salasõna", "Repeat Password": "korda salasõna",
respTime: "Reageerimisaeg (ms)", respTime: "Reageerimisaeg (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
"Import/Export Backup": "Import/Export Backup", "Import/Export Backup": "Impordi/Ekspordi varukoopia",
Export: "Export", Export: "Eksport",
Import: "Import", Import: "Import",
"Default enabled": "Default enabled", "Default enabled": "Kasuta vaikimisi",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel",
Create: "Create", Create: "Loo konto",
"Clear Data": "Clear Data", "Clear Data": "Eemalda andmed",
Events: "Events", Events: "Sündmused",
Heartbeats: "Heartbeats", Heartbeats: "Tuksed",
"Auto Get": "Auto Get", "Auto Get": "Hangi automaatselt",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
backupDescription2: "PS: History and event data is not included.", backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.",
alertNoFile: "Please select a file to import.", alertNoFile: "Palun lisa fail, mida importida.",
alertWrongFileType: "Please select a JSON file." alertWrongFileType: "Palun lisa JSON-formaadis fail."
} }

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "日本語", languageName: "日本語",
checkEverySecond: "{0}秒ごとにチェックします。", checkEverySecond: "{0}秒ごとにチェックします。",
"Avg.": "平均 ", "Avg.": "平均",
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数", retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する", ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。", upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "한국어", languageName: "한국어",
checkEverySecond: "{0} 초마다 체크해요.", checkEverySecond: "{0} 초마다 체크해요.",
"Avg.": "평균 ", "Avg.": "평균",
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수", retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기", ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.", upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Nederlands", languageName: "Nederlands",
checkEverySecond: "Controleer elke {0} seconden.", checkEverySecond: "Controleer elke {0} seconden.",
"Avg.": "Gem. ", "Avg.": "Gem.",
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden", retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites", ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.", upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Polski", languageName: "Polski",
checkEverySecond: "Sprawdzaj co {0} sekund.", checkEverySecond: "Sprawdzaj co {0} sekund.",
"Avg.": "Średnia ", "Avg.": "Średnia",
retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie", retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie",
ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS", ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS",
upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.", upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Русский", languageName: "Русский",
checkEverySecond: "Проверять каждые {0} секунд.", checkEverySecond: "Проверять каждые {0} секунд.",
"Avg.": "Средн. ", "Avg.": "Средн.",
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления", retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов", ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.", upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Srpski", languageName: "Srpski",
checkEverySecond: "Proveri svakih {0} sekundi.", checkEverySecond: "Proveri svakih {0} sekundi.",
"Avg.": "Prosečni ", "Avg.": "Prosečni",
retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.", retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.",
ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.", ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.",
upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.", upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Српски", languageName: "Српски",
checkEverySecond: "Провери сваких {0} секунди.", checkEverySecond: "Провери сваких {0} секунди.",
"Avg.": "Просечни ", "Avg.": "Просечни",
retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.", retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.", ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.", upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",

View File

@ -1,7 +1,7 @@
export default { export default {
languageName: "Svenska", languageName: "Svenska",
checkEverySecond: "Uppdatera var {0} sekund.", checkEverySecond: "Uppdatera var {0} sekund.",
"Avg.": "Genomsnittligt ", "Avg.": "Genomsnittligt",
retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas", retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas",
ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS", ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS",
upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.", upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.",

View File

@ -203,11 +203,15 @@ export default {
} }
}, },
login(username, password, callback) { login(username, password, token, callback) {
socket.emit("login", { socket.emit("login", {
username, username,
password, password,
token,
}, (res) => { }, (res) => {
if (res.tokenRequired) {
callback(res)
}
if (res.ok) { if (res.ok) {
this.storage().token = res.token; this.storage().token = res.token;
@ -242,6 +246,26 @@ export default {
this.clearData() this.clearData()
}, },
prepare2FA(callback) {
socket.emit("prepare2FA", callback)
},
save2FA(secret, callback) {
socket.emit("save2FA", callback)
},
disable2FA(callback) {
socket.emit("disable2FA", callback)
},
verifyToken(token, callback) {
socket.emit("verifyToken", token, callback)
},
twoFAStatus(callback) {
socket.emit("twoFAStatus", callback)
},
add(monitor, callback) { add(monitor, callback) {
socket.emit("add", monitor, callback) socket.emit("add", monitor, callback)
}, },

View File

@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<div class="shadow-box table-shadow-box" style="overflow-x: scroll;"> <div class="shadow-box table-shadow-box" style="overflow-x: hidden;">
<table class="table table-borderless table-hover"> <table class="table table-borderless table-hover">
<thead> <thead>
<tr> <tr>
@ -178,5 +178,10 @@ table {
tr { tr {
transition: all ease-in-out 0.2ms; transition: all ease-in-out 0.2ms;
} }
@media (max-width: 550px) {
table-layout: fixed;
overflow-wrap: break-word;
}
} }
</style> </style>

View File

@ -55,7 +55,7 @@
</span> </span>
</div> </div>
<div class="col"> <div class="col">
<h4>{{ $t("Avg.") }}{{ pingTitle }}</h4> <h4>{{ $t("Avg.") }} {{ pingTitle }}</h4>
<p>(24{{ $t("-hour") }})</p> <p>(24{{ $t("-hour") }})</p>
<span class="num"><CountUp :value="avgPing" /></span> <span class="num"><CountUp :value="avgPing" /></span>
</div> </div>

View File

@ -120,6 +120,14 @@
</form> </form>
</template> </template>
<h2 class="mt-5 mb-2">
{{ $t("Two Factor Authentication") }}
</h2>
<div class="mb-3">
<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("Import/Export Backup") }}</h2> <h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
<p> <p>
@ -144,10 +152,10 @@
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div class="mb-3"> <div class="mb-3">
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button> <button v-if="settings.disableAuth" class="btn btn-outline-primary me-1 mb-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button> <button v-if="! settings.disableAuth" class="btn btn-primary me-1 mb-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button> <button v-if="! settings.disableAuth" class="btn btn-danger me-1 mb-1" @click="$root.logout">{{ $t("Logout") }}</button>
<button class="btn btn-outline-danger me-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button> <button class="btn btn-outline-danger me-1 mb-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button>
</div> </div>
</template> </template>
</div> </div>
@ -186,6 +194,7 @@
</footer> </footer>
<NotificationDialog ref="notificationDialog" /> <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"> <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' "> <template v-if="$i18n.locale === 'es-ES' ">
@ -269,6 +278,7 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc" import utc from "dayjs/plugin/utc"
import timezone from "dayjs/plugin/timezone" import timezone from "dayjs/plugin/timezone"
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import TwoFADialog from "../components/TwoFADialog.vue";
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -279,6 +289,7 @@ const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog, NotificationDialog,
TwoFADialog,
Confirm, Confirm,
}, },
data() { data() {
@ -383,7 +394,7 @@ export default {
notificationList: this.$root.notificationList, notificationList: this.$root.notificationList,
monitorList: monitorList, monitorList: monitorList,
} }
exportData = JSON.stringify(exportData); exportData = JSON.stringify(exportData, null, 4);
let downloadItem = document.createElement("a"); let downloadItem = document.createElement("a");
downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData)); downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData));
downloadItem.setAttribute("download", fileName); downloadItem.setAttribute("download", fileName);

View File

@ -87,7 +87,7 @@ export default {
if (res.ok) { if (res.ok) {
this.processing = true; this.processing = true;
this.$root.login(this.username, this.password, (res) => { this.$root.login(this.username, this.password, "", (res) => {
this.processing = false; this.processing = false;
this.$router.push("/") this.$router.push("/")
}) })