diff --git a/db/patch-2fa.sql b/db/patch-2fa.sql new file mode 100644 index 000000000..35069d854 --- /dev/null +++ b/db/patch-2fa.sql @@ -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; diff --git a/package.json b/package.json index e1c405cf3..e71fde810 100644 --- a/package.json +++ b/package.json @@ -56,20 +56,24 @@ "http-graceful-shutdown": "^3.1.4", "jsonwebtoken": "^8.5.1", "nodemailer": "^6.6.3", + "notp": "^2.0.3", "password-hash": "^1.2.2", "prom-client": "^13.2.0", "prometheus-api-metrics": "^3.2.0", + "qrcode": "^1.4.4", "redbean-node": "0.1.2", "socket.io": "^4.2.0", "socket.io-client": "^4.2.0", "sqlite3": "github:mapbox/node-sqlite3#593c9d", "tcp-ping": "^0.1.1", + "thirty-two": "^1.0.2", "v-pagination-3": "^0.1.6", "vue": "^3.2.8", "vue-chart-3": "^0.5.7", "vue-confirm-dialog": "^1.0.2", "vue-i18n": "^9.1.7", "vue-multiselect": "^3.0.0-alpha.2", + "vue-qrcode": "^1.0.0", "vue-router": "^4.0.11", "vue-toastification": "^2.0.0-rc.1" }, diff --git a/server/database.js b/server/database.js index e0bb0c9b8..4b3ad443e 100644 --- a/server/database.js +++ b/server/database.js @@ -30,6 +30,7 @@ class Database { static patchList = { "patch-setting-value-type.sql": true, "patch-improve-performance.sql": true, + "patch-2fa.sql": true, } /** diff --git a/server/server.js b/server/server.js index e4ad586aa..08225ebb2 100644 --- a/server/server.js +++ b/server/server.js @@ -22,11 +22,15 @@ const gracefulShutdown = require("http-graceful-shutdown"); debug("Importing 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"); debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); -const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server"); +const { getSettings, setSettings, setting, initJWTSecret, genSecret } = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); @@ -219,12 +223,38 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); if (user) { afterLogin(socket, user) - callback({ - ok: true, - token: jwt.sign({ - username: data.username, - }, jwtSecret), - }) + if (user.twofaStatus == 0) { + callback({ + ok: true, + token: jwt.sign({ + 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 { callback({ ok: false, @@ -240,6 +270,130 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); 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) => { callback(needSetup); }); diff --git a/server/util-server.js b/server/util-server.js index a2fef0656..079bd82f3 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -271,3 +271,13 @@ exports.getTotalClientInRoom = (io, roomName) => { 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; +} diff --git a/src/components/Login.vue b/src/components/Login.vue index bd51759c7..ca36fdb9f 100644 --- a/src/components/Login.vue +++ b/src/components/Login.vue @@ -4,16 +4,23 @@ <form @submit.prevent="submit"> <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"> <label for="floatingInput">{{ $t("Username") }}</label> </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"> <label for="floatingPassword">{{ $t("Password") }}</label> </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"> <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input"> @@ -42,16 +49,24 @@ export default { processing: false, username: "", password: "", - + token: "", res: null, + tokenRequired: false, } }, methods: { submit() { 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.res = res; + console.log(res) + + if (res.tokenRequired) { + this.tokenRequired = true; + } else { + this.res = res; + } }) }, }, diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index ffb7ba71d..d689b0c91 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -410,7 +410,7 @@ <div class="form-check form-switch"> <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> diff --git a/src/components/TwoFADialog.vue b/src/components/TwoFADialog.vue new file mode 100644 index 000000000..b7b9668d8 --- /dev/null +++ b/src/components/TwoFADialog.vue @@ -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> diff --git a/src/languages/da-DK.js b/src/languages/da-DK.js index 2859655e8..6e8069aa1 100644 --- a/src/languages/da-DK.js +++ b/src/languages/da-DK.js @@ -17,8 +17,8 @@ export default { Down: "Inaktiv", Pending: "Afventer", Unknown: "Ukendt", - Pause: "Pause", - pauseDashboardHome: "Pauset", + Pause: "Stands", + pauseDashboardHome: "Standset", Name: "Navn", Status: "Status", DateTime: "Dato / Tid", @@ -36,8 +36,8 @@ export default { hour: "Timer", "-hour": "-Timer", checkEverySecond: "Tjek hvert {0} sekund", - "Avg.": "Gennemsnit", - Response: " Respons", + "Avg.": "Gns.", + Response: "Respons", Ping: "Ping", "Monitor Type": "Overvåger Type", Keyword: "Nøgleord", @@ -103,29 +103,29 @@ export default { "Resolver Server": "Navne-server", rrtypeDescription: "Vælg den type RR, du vil overvåge.", "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", "Repeat Password": "Gentag adgangskoden", "Resource Record Type": "Resource Record Type", - respTime: "Resp. Time (ms)", + respTime: "Resp. Tid (ms)", notAvailableShort: "N/A", - Create: "Create", - clearEventsMsg: "Are you sure want to delete all events for this monitor?", - clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", - confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", - "Clear Data": "Clear Data", + Create: "Opret", + clearEventsMsg: "Er du sikker på vil slette alle events for denne Overvåger?", + clearHeartbeatsMsg: "Er du sikker på vil slette alle heartbeats for denne Overvåger?", + confirmClearStatisticsMsg: "Vil du helt sikkert slette ALLE statistikker?", + "Clear Data": "Ryd Data", Events: "Events", Heartbeats: "Heartbeats", - "Auto Get": "Auto Get", - enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", - "Default enabled": "Default enabled", - "Also apply to existing monitors": "Also apply to existing monitors", - "Import/Export Backup": "Import/Export Backup", - Export: "Export", + "Auto Get": "Auto-hent", + enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.", + "Default enabled": "Standard aktiveret", + "Also apply to existing monitors": "Anvend også på eksisterende overvågere", + "Import/Export Backup": " Importér/Eksportér sikkerhedskopi", + Export: "Eksport", Import: "Import", - backupDescription: "You can backup all monitors and all notifications into a JSON file.", - backupDescription2: "PS: History and event data is not included.", - backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", - alertNoFile: "Please select a file to import.", - alertWrongFileType: "Please select a JSON file." + backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.", + backupDescription2: "PS: Historik og hændelsesdata er ikke inkluderet.", + backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.", + alertNoFile: "Vælg en fil der skal importeres.", + alertWrongFileType: "Vælg venligst en JSON-fil." } diff --git a/src/languages/de-DE.js b/src/languages/de-DE.js index 083d939a4..e401f2bf8 100644 --- a/src/languages/de-DE.js +++ b/src/languages/de-DE.js @@ -36,8 +36,8 @@ export default { hour: "Stunde", "-hour": "-Stunden", checkEverySecond: "Überprüfe alle {0} Sekunden", - "Avg.": "Durchschn. ", - Response: " Antwortzeit", + "Avg.": "Durchschn.", + Response: "Antwortzeit", Ping: "Ping", "Monitor Type": "Monitor Typ", Keyword: "Schlüsselwort", @@ -119,7 +119,7 @@ export default { respTime: "Antw. Zeit (ms)", notAvailableShort: "N/A", "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.", Create: "Erstellen", "Auto Get": "Auto Get", @@ -135,4 +135,19 @@ export default { "Import Options": "Import Optionen", confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.", "Keep both": "Beide behalten", + 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" } diff --git a/src/languages/en.js b/src/languages/en.js index 0644610de..c81db3105 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1,7 +1,7 @@ export default { languageName: "English", checkEverySecond: "Check every {0} seconds.", - "Avg.": "Avg. ", + "Avg.": "Avg.", retriesDescription: "Maximum retries before the service is marked as down and a notification is sent", ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites", upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.", @@ -22,6 +22,10 @@ export default { confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.", confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", + 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", Dashboard: "Dashboard", "New Update": "New Update", @@ -119,7 +123,7 @@ export default { respTime: "Resp. Time (ms)", notAvailableShort: "N/A", "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", "Clear Data": "Clear Data", Events: "Events", @@ -135,4 +139,15 @@ export default { "Overwrite": "Overwrite", "Import Options": "Import Options", "Keep both": "Keep both", + "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" } diff --git a/src/languages/es-ES.js b/src/languages/es-ES.js index ba216dcff..b52c1654f 100644 --- a/src/languages/es-ES.js +++ b/src/languages/es-ES.js @@ -1,7 +1,7 @@ export default { languageName: "Español", 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.", ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS", upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.", diff --git a/src/languages/et-EE.js b/src/languages/et-EE.js index 3992e9025..fca24a33d 100644 --- a/src/languages/et-EE.js +++ b/src/languages/et-EE.js @@ -1,7 +1,7 @@ export default { languageName: "eesti", checkEverySecond: "Kontrolli {0} sekundilise vahega.", - "Avg.": "≈ ", + "Avg.": "≈", retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.", ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.", upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.", @@ -10,7 +10,7 @@ export default { passwordNotMatchMsg: "Salasõnad ei kattu.", notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.", keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)", - pauseDashboardHome: "Seiskamine", + pauseDashboardHome: "Seismas", deleteMonitorMsg: "Kas soovid eemaldada seire?", deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?", resoverserverDescription: "Cloudflare on vaikimisi pöördserver.", @@ -109,23 +109,23 @@ export default { "Repeat Password": "korda salasõna", respTime: "Reageerimisaeg (ms)", 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.", - clearEventsMsg: "Are you sure want to delete all events for this monitor?", - clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", - confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", - "Import/Export Backup": "Import/Export Backup", - Export: "Export", + enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.", + clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?", + clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?", + confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?", + "Import/Export Backup": "Impordi/Ekspordi varukoopia", + Export: "Eksport", Import: "Import", - "Default enabled": "Default enabled", - "Also apply to existing monitors": "Also apply to existing monitors", - Create: "Create", - "Clear Data": "Clear Data", - Events: "Events", - Heartbeats: "Heartbeats", - "Auto Get": "Auto Get", - backupDescription: "You can backup all monitors and all notifications into a JSON file.", - backupDescription2: "PS: History and event data is not included.", - backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", - alertNoFile: "Please select a file to import.", - alertWrongFileType: "Please select a JSON file." + "Default enabled": "Kasuta vaikimisi", + "Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel", + Create: "Loo konto", + "Clear Data": "Eemalda andmed", + Events: "Sündmused", + Heartbeats: "Tuksed", + "Auto Get": "Hangi automaatselt", + backupDescription: "Varunda kõik seired ja teavitused JSON faili.", + backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.", + backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.", + alertNoFile: "Palun lisa fail, mida importida.", + alertWrongFileType: "Palun lisa JSON-formaadis fail." } diff --git a/src/languages/ja.js b/src/languages/ja.js index 4ef10a5a0..6d0693d5a 100644 --- a/src/languages/ja.js +++ b/src/languages/ja.js @@ -1,7 +1,7 @@ export default { languageName: "日本語", checkEverySecond: "{0}秒ごとにチェックします。", - "Avg.": "平均 ", + "Avg.": "平均", retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数", ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する", upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。", diff --git a/src/languages/ko-KR.js b/src/languages/ko-KR.js index da1491a5e..68479ade1 100644 --- a/src/languages/ko-KR.js +++ b/src/languages/ko-KR.js @@ -1,7 +1,7 @@ export default { languageName: "한국어", checkEverySecond: "{0} 초마다 체크해요.", - "Avg.": "평균 ", + "Avg.": "평균", retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수", ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기", upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.", diff --git a/src/languages/nl-NL.js b/src/languages/nl-NL.js index 2c5f10104..48e3b3a29 100644 --- a/src/languages/nl-NL.js +++ b/src/languages/nl-NL.js @@ -1,7 +1,7 @@ export default { languageName: "Nederlands", 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", ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites", upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.", diff --git a/src/languages/pl.js b/src/languages/pl.js index 4863cccbd..3029b3497 100644 --- a/src/languages/pl.js +++ b/src/languages/pl.js @@ -1,7 +1,7 @@ export default { languageName: "Polski", 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", 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.", diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js index 2dbe254e4..7f5eae760 100644 --- a/src/languages/ru-RU.js +++ b/src/languages/ru-RU.js @@ -1,7 +1,7 @@ export default { languageName: "Русский", checkEverySecond: "Проверять каждые {0} секунд.", - "Avg.": "Средн. ", + "Avg.": "Средн.", retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления", ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов", upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.", diff --git a/src/languages/sr-latn.js b/src/languages/sr-latn.js index a149cb2c8..3dd73d2cc 100644 --- a/src/languages/sr-latn.js +++ b/src/languages/sr-latn.js @@ -1,7 +1,7 @@ export default { languageName: "Srpski", 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.", ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.", upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.", diff --git a/src/languages/sr.js b/src/languages/sr.js index f3e24a662..6931d272e 100644 --- a/src/languages/sr.js +++ b/src/languages/sr.js @@ -1,7 +1,7 @@ export default { languageName: "Српски", checkEverySecond: "Провери сваких {0} секунди.", - "Avg.": "Просечни ", + "Avg.": "Просечни", retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.", ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.", upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.", diff --git a/src/languages/sv-SE.js b/src/languages/sv-SE.js index 0b4d02ca6..f8749e289 100644 --- a/src/languages/sv-SE.js +++ b/src/languages/sv-SE.js @@ -1,7 +1,7 @@ export default { languageName: "Svenska", 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", 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.", diff --git a/src/mixins/socket.js b/src/mixins/socket.js index e0088a59f..22d6c510a 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -203,11 +203,15 @@ export default { } }, - login(username, password, callback) { + login(username, password, token, callback) { socket.emit("login", { username, password, + token, }, (res) => { + if (res.tokenRequired) { + callback(res) + } if (res.ok) { this.storage().token = res.token; @@ -242,6 +246,26 @@ export default { 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) { socket.emit("add", monitor, callback) }, diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue index 9f9cda1de..186184ec9 100644 --- a/src/pages/DashboardHome.vue +++ b/src/pages/DashboardHome.vue @@ -26,7 +26,7 @@ </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"> <thead> <tr> @@ -178,5 +178,10 @@ table { tr { transition: all ease-in-out 0.2ms; } + + @media (max-width: 550px) { + table-layout: fixed; + overflow-wrap: break-word; + } } </style> diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 776f1c1dd..9092b1792 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -55,7 +55,7 @@ </span> </div> <div class="col"> - <h4>{{ $t("Avg.") }}{{ pingTitle }}</h4> + <h4>{{ $t("Avg.") }} {{ pingTitle }}</h4> <p>(24{{ $t("-hour") }})</p> <span class="num"><CountUp :value="avgPing" /></span> </div> diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index 9502f11ce..84c3f8b75 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -120,6 +120,14 @@ </form> </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> <p> @@ -163,10 +171,10 @@ <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <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-primary me-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 class="btn btn-outline-danger me-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</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 mb-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</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 mb-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button> </div> </template> </div> @@ -205,6 +213,7 @@ </footer> <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' "> @@ -291,6 +300,7 @@ 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"; dayjs.extend(utc) dayjs.extend(timezone) @@ -301,6 +311,7 @@ const toast = useToast() export default { components: { NotificationDialog, + TwoFADialog, Confirm, }, data() { diff --git a/src/pages/Setup.vue b/src/pages/Setup.vue index 9cb7c5ecb..7966f9e9b 100644 --- a/src/pages/Setup.vue +++ b/src/pages/Setup.vue @@ -87,7 +87,7 @@ export default { if (res.ok) { this.processing = true; - this.$root.login(this.username, this.password, (res) => { + this.$root.login(this.username, this.password, "", (res) => { this.processing = false; this.$router.push("/") })