mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-31 19:39:20 +08:00 
			
		
		
		
	Some improvements
This commit is contained in:
		| @@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter"); | ||||
|  * @returns {Promise<Bean|null>} | ||||
|  */ | ||||
| exports.login = async function (username, password) { | ||||
|     if (typeof username !== "string" || typeof password !== "string") { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     let user = await R.findOne("user", " username = ? AND active = 1 ", [ | ||||
|         username, | ||||
|     ]); | ||||
|   | ||||
| @@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({ | ||||
|     errorMessage: "Too frequently, try again later." | ||||
| }); | ||||
|  | ||||
| const twoFaRateLimiter = new KumaRateLimiter({ | ||||
|     tokensPerInterval: 30, | ||||
|     interval: "minute", | ||||
|     fireImmediately: true, | ||||
|     errorMessage: "Too frequently, try again later." | ||||
| }); | ||||
|  | ||||
| module.exports = { | ||||
|     loginRateLimiter | ||||
|     loginRateLimiter, | ||||
|     twoFaRateLimiter, | ||||
| }; | ||||
|   | ||||
							
								
								
									
										120
									
								
								server/server.js
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								server/server.js
									
									
									
									
									
								
							| @@ -52,7 +52,7 @@ console.log("Importing this project modules"); | ||||
| debug("Importing Monitor"); | ||||
| const Monitor = require("./model/monitor"); | ||||
| debug("Importing Settings"); | ||||
| const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server"); | ||||
| const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server"); | ||||
|  | ||||
| debug("Importing Notification"); | ||||
| const { Notification } = require("./notification"); | ||||
| @@ -63,7 +63,7 @@ const Database = require("./database"); | ||||
|  | ||||
| debug("Importing Background Jobs"); | ||||
| const { initBackgroundJobs } = require("./jobs"); | ||||
| const { loginRateLimiter } = require("./rate-limiter"); | ||||
| const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter"); | ||||
|  | ||||
| const { basicAuth } = require("./auth"); | ||||
| const { login } = require("./auth"); | ||||
| @@ -305,6 +305,15 @@ exports.entryPage = "dashboard"; | ||||
|         socket.on("login", async (data, callback) => { | ||||
|             console.log("Login"); | ||||
|  | ||||
|             // Checking | ||||
|             if (typeof callback !== "function") { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (!data) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Login Rate Limit | ||||
|             if (! await loginRateLimiter.pass(callback)) { | ||||
|                 return; | ||||
| @@ -363,14 +372,27 @@ exports.entryPage = "dashboard"; | ||||
|         }); | ||||
|  | ||||
|         socket.on("logout", async (callback) => { | ||||
|             // Rate Limit | ||||
|             if (! await loginRateLimiter.pass(callback)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             socket.leave(socket.userID); | ||||
|             socket.userID = null; | ||||
|             callback(); | ||||
|  | ||||
|             if (typeof callback === "function") { | ||||
|                 callback(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("prepare2FA", async (callback) => { | ||||
|         socket.on("prepare2FA", async (currentPassword, callback) => { | ||||
|             try { | ||||
|                 if (! await twoFaRateLimiter.pass(callback)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 checkLogin(socket); | ||||
|                 await doubleCheckPassword(socket, currentPassword); | ||||
|  | ||||
|                 let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|                     socket.userID, | ||||
| @@ -405,14 +427,19 @@ exports.entryPage = "dashboard"; | ||||
|             } catch (error) { | ||||
|                 callback({ | ||||
|                     ok: false, | ||||
|                     msg: "Error while trying to prepare 2FA.", | ||||
|                     msg: error.message, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("save2FA", async (callback) => { | ||||
|         socket.on("save2FA", async (currentPassword, callback) => { | ||||
|             try { | ||||
|                 if (! await twoFaRateLimiter.pass(callback)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 checkLogin(socket); | ||||
|                 await doubleCheckPassword(socket, currentPassword); | ||||
|  | ||||
|                 await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ | ||||
|                     socket.userID, | ||||
| @@ -425,14 +452,19 @@ exports.entryPage = "dashboard"; | ||||
|             } catch (error) { | ||||
|                 callback({ | ||||
|                     ok: false, | ||||
|                     msg: "Error while trying to change 2FA.", | ||||
|                     msg: error.message, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("disable2FA", async (callback) => { | ||||
|         socket.on("disable2FA", async (currentPassword, callback) => { | ||||
|             try { | ||||
|                 if (! await twoFaRateLimiter.pass(callback)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 checkLogin(socket); | ||||
|                 await doubleCheckPassword(socket, currentPassword); | ||||
|                 await TwoFA.disable2FA(socket.userID); | ||||
|  | ||||
|                 callback({ | ||||
| @@ -442,36 +474,47 @@ exports.entryPage = "dashboard"; | ||||
|             } catch (error) { | ||||
|                 callback({ | ||||
|                     ok: false, | ||||
|                     msg: "Error while trying to change 2FA.", | ||||
|                     msg: error.message, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("verifyToken", async (token, callback) => { | ||||
|             let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|                 socket.userID, | ||||
|             ]); | ||||
|         socket.on("verifyToken", async (token, currentPassword, callback) => { | ||||
|             try { | ||||
|                 checkLogin(socket); | ||||
|                 await doubleCheckPassword(socket, currentPassword); | ||||
|  | ||||
|             let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts); | ||||
|                 let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|                     socket.userID, | ||||
|                 ]); | ||||
|  | ||||
|             if (user.twofa_last_token !== token && verify) { | ||||
|                 callback({ | ||||
|                     ok: true, | ||||
|                     valid: true, | ||||
|                 }); | ||||
|             } else { | ||||
|                 let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts); | ||||
|  | ||||
|                 if (user.twofa_last_token !== token && verify) { | ||||
|                     callback({ | ||||
|                         ok: true, | ||||
|                         valid: true, | ||||
|                     }); | ||||
|                 } else { | ||||
|                     callback({ | ||||
|                         ok: false, | ||||
|                         msg: "Invalid Token.", | ||||
|                         valid: false, | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|             } catch (error) { | ||||
|                 callback({ | ||||
|                     ok: false, | ||||
|                     msg: "Invalid Token.", | ||||
|                     valid: false, | ||||
|                     msg: error.message, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("twoFAStatus", async (callback) => { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             try { | ||||
|                 checkLogin(socket); | ||||
|  | ||||
|                 let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|                     socket.userID, | ||||
|                 ]); | ||||
| @@ -488,9 +531,10 @@ exports.entryPage = "dashboard"; | ||||
|                     }); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.log(error); | ||||
|                 callback({ | ||||
|                     ok: false, | ||||
|                     msg: "Error while trying to get 2FA status.", | ||||
|                     msg: error.message, | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| @@ -936,21 +980,13 @@ exports.entryPage = "dashboard"; | ||||
|                     throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); | ||||
|                 } | ||||
|  | ||||
|                 let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|                     socket.userID, | ||||
|                 ]); | ||||
|                 let user = await doubleCheckPassword(socket, password.currentPassword); | ||||
|                 await user.resetPassword(password.newPassword); | ||||
|  | ||||
|                 if (user && passwordHash.verify(password.currentPassword, user.password)) { | ||||
|  | ||||
|                     user.resetPassword(password.newPassword); | ||||
|  | ||||
|                     callback({ | ||||
|                         ok: true, | ||||
|                         msg: "Password has been updated successfully.", | ||||
|                     }); | ||||
|                 } else { | ||||
|                     throw new Error("Incorrect current password"); | ||||
|                 } | ||||
|                 callback({ | ||||
|                     ok: true, | ||||
|                     msg: "Password has been updated successfully.", | ||||
|                 }); | ||||
|  | ||||
|             } catch (e) { | ||||
|                 callback({ | ||||
| @@ -977,10 +1013,14 @@ exports.entryPage = "dashboard"; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("setSettings", async (data, callback) => { | ||||
|         socket.on("setSettings", async (data, currentPassword, callback) => { | ||||
|             try { | ||||
|                 checkLogin(socket); | ||||
|  | ||||
|                 if (data.disableAuth) { | ||||
|                     await doubleCheckPassword(socket, currentPassword); | ||||
|                 } | ||||
|  | ||||
|                 await setSettings("general", data); | ||||
|                 exports.entryPage = data.entryPage; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| const tcpp = require("tcp-ping"); | ||||
| const Ping = require("./ping-lite"); | ||||
| const { R } = require("redbean-node"); | ||||
| const { debug } = require("../src/util"); | ||||
| const { debug, genSecret } = require("../src/util"); | ||||
| const passwordHash = require("./password-hash"); | ||||
| const dayjs = require("dayjs"); | ||||
| const { Resolver } = require("dns"); | ||||
| const child_process = require("child_process"); | ||||
| const iconv = require("iconv-lite"); | ||||
| @@ -32,7 +31,7 @@ exports.initJWTSecret = async () => { | ||||
|         jwtSecretBean.key = "jwtSecret"; | ||||
|     } | ||||
|  | ||||
|     jwtSecretBean.value = passwordHash.generate(dayjs() + ""); | ||||
|     jwtSecretBean.value = passwordHash.generate(genSecret()); | ||||
|     await R.store(jwtSecretBean); | ||||
|     return jwtSecretBean; | ||||
| }; | ||||
| @@ -321,6 +320,28 @@ exports.checkLogin = (socket) => { | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * For logged-in users, double-check the password | ||||
|  * @param socket | ||||
|  * @param currentPassword | ||||
|  * @returns {Promise<Bean>} | ||||
|  */ | ||||
| exports.doubleCheckPassword = async (socket, currentPassword) => { | ||||
|     if (typeof currentPassword !== "string") { | ||||
|         throw new Error("Wrong data type?"); | ||||
|     } | ||||
|  | ||||
|     let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|         socket.userID, | ||||
|     ]); | ||||
|  | ||||
|     if (!user || !passwordHash.verify(currentPassword, user.password)) { | ||||
|         throw new Error("Incorrect current password"); | ||||
|     } | ||||
|  | ||||
|     return user; | ||||
| }; | ||||
|  | ||||
| exports.startUnitTest = async () => { | ||||
|     console.log("Starting unit test..."); | ||||
|     const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; | ||||
|   | ||||
| @@ -9,7 +9,9 @@ | ||||
|                 <a v-if="searchText != ''" class="search-icon" @click="clearSearchText"> | ||||
|                     <font-awesome-icon icon="times" /> | ||||
|                 </a> | ||||
|                 <input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" /> | ||||
|                 <form> | ||||
|                     <input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" /> | ||||
|                 </form> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="monitor-list" :class="{ scrollbar: scrollbar }"> | ||||
|   | ||||
| @@ -19,6 +19,19 @@ | ||||
|                             </div> | ||||
|                             <p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p> | ||||
|  | ||||
|                             <div v-if="!(uri && twoFAStatus == false)" class="mb-3"> | ||||
|                                 <label for="current-password" class="form-label"> | ||||
|                                     {{ $t("Current Password") }} | ||||
|                                 </label> | ||||
|                                 <input | ||||
|                                     id="current-password" | ||||
|                                     v-model="currentPassword" | ||||
|                                     type="password" | ||||
|                                     class="form-control" | ||||
|                                     required | ||||
|                                 /> | ||||
|                             </div> | ||||
|  | ||||
|                             <button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()"> | ||||
|                                 {{ $t("Enable 2FA") }} | ||||
|                             </button> | ||||
| @@ -59,11 +72,11 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { Modal } from "bootstrap" | ||||
| import { Modal } from "bootstrap"; | ||||
| import Confirm from "./Confirm.vue"; | ||||
| import VueQrcode from "vue-qrcode" | ||||
| import { useToast } from "vue-toastification" | ||||
| const toast = useToast() | ||||
| import VueQrcode from "vue-qrcode"; | ||||
| import { useToast } from "vue-toastification"; | ||||
| const toast = useToast(); | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -73,35 +86,36 @@ export default { | ||||
|     props: {}, | ||||
|     data() { | ||||
|         return { | ||||
|             currentPassword: "", | ||||
|             processing: false, | ||||
|             uri: null, | ||||
|             tokenValid: false, | ||||
|             twoFAStatus: null, | ||||
|             token: null, | ||||
|             showURI: false, | ||||
|         } | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.modal = new Modal(this.$refs.modal) | ||||
|         this.modal = new Modal(this.$refs.modal); | ||||
|         this.getStatus(); | ||||
|     }, | ||||
|     methods: { | ||||
|         show() { | ||||
|             this.modal.show() | ||||
|             this.modal.show(); | ||||
|         }, | ||||
|  | ||||
|         confirmEnableTwoFA() { | ||||
|             this.$refs.confirmEnableTwoFA.show() | ||||
|             this.$refs.confirmEnableTwoFA.show(); | ||||
|         }, | ||||
|  | ||||
|         confirmDisableTwoFA() { | ||||
|             this.$refs.confirmDisableTwoFA.show() | ||||
|             this.$refs.confirmDisableTwoFA.show(); | ||||
|         }, | ||||
|  | ||||
|         prepare2FA() { | ||||
|             this.processing = true; | ||||
|  | ||||
|             this.$root.getSocket().emit("prepare2FA", (res) => { | ||||
|             this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => { | ||||
|                 this.processing = false; | ||||
|  | ||||
|                 if (res.ok) { | ||||
| @@ -109,49 +123,51 @@ export default { | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         save2FA() { | ||||
|             this.processing = true; | ||||
|  | ||||
|             this.$root.getSocket().emit("save2FA", (res) => { | ||||
|             this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => { | ||||
|                 this.processing = false; | ||||
|  | ||||
|                 if (res.ok) { | ||||
|                     this.$root.toastRes(res) | ||||
|                     this.$root.toastRes(res); | ||||
|                     this.getStatus(); | ||||
|                     this.currentPassword = ""; | ||||
|                     this.modal.hide(); | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         disable2FA() { | ||||
|             this.processing = true; | ||||
|  | ||||
|             this.$root.getSocket().emit("disable2FA", (res) => { | ||||
|             this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => { | ||||
|                 this.processing = false; | ||||
|  | ||||
|                 if (res.ok) { | ||||
|                     this.$root.toastRes(res) | ||||
|                     this.$root.toastRes(res); | ||||
|                     this.getStatus(); | ||||
|                     this.currentPassword = ""; | ||||
|                     this.modal.hide(); | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         verifyToken() { | ||||
|             this.$root.getSocket().emit("verifyToken", this.token, (res) => { | ||||
|             this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.tokenValid = res.valid; | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         getStatus() { | ||||
| @@ -161,10 +177,10 @@ export default { | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   | ||||
| @@ -215,14 +215,14 @@ | ||||
|                 <p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p> | ||||
|                 <p>Vennligst vær forsiktig.</p> | ||||
|             </template> | ||||
|              | ||||
|  | ||||
|             <template v-else-if="$i18n.locale === 'cs-CZ' "> | ||||
|                 <p>Opravdu chcete <strong>deaktivovat autentifikaci</strong>?</p> | ||||
|                 <p>Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.</p> | ||||
|                 <p>Používejte ji prosím s rozmyslem.</p> | ||||
|             </template> | ||||
|  | ||||
| 			<template v-else-if="$i18n.locale === 'vi-VN' "> | ||||
|             <template v-else-if="$i18n.locale === 'vi-VN' "> | ||||
|                 <p>Bạn có muốn <strong>TẮT XÁC THỰC</strong> không?</p> | ||||
|                 <p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng có thể truy cập và cướp quyền điều khiển.</p> | ||||
|                 <p>Vui lòng <strong>cẩn thận</strong>.</p> | ||||
| @@ -234,6 +234,19 @@ | ||||
|                 <p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p> | ||||
|                 <p>Please use this option carefully!</p> | ||||
|             </template> | ||||
|  | ||||
|             <div class="mb-3"> | ||||
|                 <label for="current-password2" class="form-label"> | ||||
|                     {{ $t("Current Password") }} | ||||
|                 </label> | ||||
|                 <input | ||||
|                     id="current-password2" | ||||
|                     v-model="password.currentPassword" | ||||
|                     type="password" | ||||
|                     class="form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </div> | ||||
|         </Confirm> | ||||
|     </div> | ||||
| </template> | ||||
| @@ -310,7 +323,12 @@ export default { | ||||
|  | ||||
|         disableAuth() { | ||||
|             this.settings.disableAuth = true; | ||||
|             this.saveSettings(); | ||||
|  | ||||
|             // Need current password to disable auth | ||||
|             // Set it to empty if done | ||||
|             this.saveSettings(() => { | ||||
|                 this.password.currentPassword = ""; | ||||
|             }, this.password.currentPassword); | ||||
|         }, | ||||
|  | ||||
|         enableAuth() { | ||||
|   | ||||
| @@ -131,10 +131,18 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         saveSettings() { | ||||
|             this.$root.getSocket().emit("setSettings", this.settings, (res) => { | ||||
|         /** | ||||
|          * Save Settings | ||||
|          * @param currentPassword (Optional) Only need for disableAuth to true | ||||
|          */ | ||||
|         saveSettings(callback, currentPassword) { | ||||
|             this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => { | ||||
|                 this.$root.toastRes(res); | ||||
|                 this.loadSettings(); | ||||
|  | ||||
|                 if (callback) { | ||||
|                     callback(); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user