mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-25 07:39:22 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master'
This commit is contained in:
		
							
								
								
									
										10
									
								
								db/patch-2fa.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/patch-2fa.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| ALTER TABLE user | ||||
|     ADD twofa_secret VARCHAR(64); | ||||
|  | ||||
| ALTER TABLE user | ||||
|     ADD twofa_status BOOLEAN default 0 NOT NULL; | ||||
|  | ||||
| COMMIT; | ||||
| @@ -58,20 +58,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" | ||||
|     }, | ||||
|   | ||||
| @@ -30,6 +30,7 @@ class Database { | ||||
|     static patchList = { | ||||
|         "patch-setting-value-type.sql": true, | ||||
|         "patch-improve-performance.sql": true, | ||||
|         "patch-2fa.sql": true, | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
							
								
								
									
										156
									
								
								server/server.js
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								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) | ||||
|  | ||||
|                 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); | ||||
|         }); | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|                 console.log(res) | ||||
|  | ||||
|                 if (res.tokenRequired) { | ||||
|                     this.tokenRequired = true; | ||||
|                 } else { | ||||
|                     this.res = res; | ||||
|                 } | ||||
|             }) | ||||
|         }, | ||||
|     }, | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										178
									
								
								src/components/TwoFADialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/components/TwoFADialog.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -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,7 +36,7 @@ export default { | ||||
|     hour: "Timer", | ||||
|     "-hour": "-Timer", | ||||
|     checkEverySecond: "Tjek hvert {0} sekund", | ||||
|     "Avg.": "Gennemsnit", | ||||
|     "Avg.": "Gns.", | ||||
|     Response: "Respons", | ||||
|     Ping: "Ping", | ||||
|     "Monitor Type": "Overvåger Type", | ||||
| @@ -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." | ||||
| } | ||||
|   | ||||
| @@ -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", | ||||
| @@ -128,5 +128,19 @@ export default { | ||||
|     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.", | ||||
|     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" | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,10 @@ export default { | ||||
|     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?", | ||||
|     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", | ||||
| @@ -117,7 +121,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", | ||||
| @@ -128,5 +132,15 @@ export default { | ||||
|     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.", | ||||
|     "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" | ||||
| } | ||||
|   | ||||
| @@ -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." | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|         }, | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
| @@ -144,10 +152,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> | ||||
| @@ -186,6 +194,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' "> | ||||
| @@ -269,6 +278,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) | ||||
|  | ||||
| @@ -279,6 +289,7 @@ const toast = useToast() | ||||
| export default { | ||||
|     components: { | ||||
|         NotificationDialog, | ||||
|         TwoFADialog, | ||||
|         Confirm, | ||||
|     }, | ||||
|     data() { | ||||
| @@ -383,7 +394,7 @@ export default { | ||||
|                 notificationList: this.$root.notificationList, | ||||
|                 monitorList: monitorList, | ||||
|             } | ||||
|             exportData = JSON.stringify(exportData); | ||||
|             exportData = JSON.stringify(exportData, null, 4); | ||||
|             let downloadItem = document.createElement("a"); | ||||
|             downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData)); | ||||
|             downloadItem.setAttribute("download", fileName); | ||||
|   | ||||
| @@ -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("/") | ||||
|                     }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user