mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	Merge pull request #363 from Ponkhy/2fa
Added simple TOTP Two Factor Authentication
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;
 | 
				
			||||||
@@ -56,20 +56,24 @@
 | 
				
			|||||||
        "http-graceful-shutdown": "^3.1.4",
 | 
					        "http-graceful-shutdown": "^3.1.4",
 | 
				
			||||||
        "jsonwebtoken": "^8.5.1",
 | 
					        "jsonwebtoken": "^8.5.1",
 | 
				
			||||||
        "nodemailer": "^6.6.3",
 | 
					        "nodemailer": "^6.6.3",
 | 
				
			||||||
 | 
					        "notp": "^2.0.3",
 | 
				
			||||||
        "password-hash": "^1.2.2",
 | 
					        "password-hash": "^1.2.2",
 | 
				
			||||||
        "prom-client": "^13.2.0",
 | 
					        "prom-client": "^13.2.0",
 | 
				
			||||||
        "prometheus-api-metrics": "^3.2.0",
 | 
					        "prometheus-api-metrics": "^3.2.0",
 | 
				
			||||||
 | 
					        "qrcode": "^1.4.4",
 | 
				
			||||||
        "redbean-node": "0.1.2",
 | 
					        "redbean-node": "0.1.2",
 | 
				
			||||||
        "socket.io": "^4.2.0",
 | 
					        "socket.io": "^4.2.0",
 | 
				
			||||||
        "socket.io-client": "^4.2.0",
 | 
					        "socket.io-client": "^4.2.0",
 | 
				
			||||||
        "sqlite3": "github:mapbox/node-sqlite3#593c9d",
 | 
					        "sqlite3": "github:mapbox/node-sqlite3#593c9d",
 | 
				
			||||||
        "tcp-ping": "^0.1.1",
 | 
					        "tcp-ping": "^0.1.1",
 | 
				
			||||||
 | 
					        "thirty-two": "^1.0.2",
 | 
				
			||||||
        "v-pagination-3": "^0.1.6",
 | 
					        "v-pagination-3": "^0.1.6",
 | 
				
			||||||
        "vue": "^3.2.8",
 | 
					        "vue": "^3.2.8",
 | 
				
			||||||
        "vue-chart-3": "^0.5.7",
 | 
					        "vue-chart-3": "^0.5.7",
 | 
				
			||||||
        "vue-confirm-dialog": "^1.0.2",
 | 
					        "vue-confirm-dialog": "^1.0.2",
 | 
				
			||||||
        "vue-i18n": "^9.1.7",
 | 
					        "vue-i18n": "^9.1.7",
 | 
				
			||||||
        "vue-multiselect": "^3.0.0-alpha.2",
 | 
					        "vue-multiselect": "^3.0.0-alpha.2",
 | 
				
			||||||
 | 
					        "vue-qrcode": "^1.0.0",
 | 
				
			||||||
        "vue-router": "^4.0.11",
 | 
					        "vue-router": "^4.0.11",
 | 
				
			||||||
        "vue-toastification": "^2.0.0-rc.1"
 | 
					        "vue-toastification": "^2.0.0-rc.1"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,7 @@ class Database {
 | 
				
			|||||||
    static patchList = {
 | 
					    static patchList = {
 | 
				
			||||||
        "patch-setting-value-type.sql": true,
 | 
					        "patch-setting-value-type.sql": true,
 | 
				
			||||||
        "patch-improve-performance.sql": true,
 | 
					        "patch-improve-performance.sql": true,
 | 
				
			||||||
 | 
					        "patch-2fa.sql": true,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										168
									
								
								server/server.js
									
									
									
									
									
								
							
							
						
						
									
										168
									
								
								server/server.js
									
									
									
									
									
								
							@@ -22,11 +22,15 @@ const gracefulShutdown = require("http-graceful-shutdown");
 | 
				
			|||||||
debug("Importing prometheus-api-metrics");
 | 
					debug("Importing prometheus-api-metrics");
 | 
				
			||||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
 | 
					const prometheusAPIMetrics = require("prometheus-api-metrics");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					debug("Importing 2FA Modules");
 | 
				
			||||||
 | 
					const notp = require("notp");
 | 
				
			||||||
 | 
					const base32 = require("thirty-two");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
console.log("Importing this project modules");
 | 
					console.log("Importing this project modules");
 | 
				
			||||||
debug("Importing Monitor");
 | 
					debug("Importing Monitor");
 | 
				
			||||||
const Monitor = require("./model/monitor");
 | 
					const Monitor = require("./model/monitor");
 | 
				
			||||||
debug("Importing Settings");
 | 
					debug("Importing Settings");
 | 
				
			||||||
const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
 | 
					const { getSettings, setSettings, setting, initJWTSecret, genSecret } = require("./util-server");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
debug("Importing Notification");
 | 
					debug("Importing Notification");
 | 
				
			||||||
const { Notification } = require("./notification");
 | 
					const { Notification } = require("./notification");
 | 
				
			||||||
@@ -219,12 +223,38 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
 | 
				
			|||||||
            if (user) {
 | 
					            if (user) {
 | 
				
			||||||
                afterLogin(socket, user)
 | 
					                afterLogin(socket, user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                callback({
 | 
					                if (user.twofaStatus == 0) {
 | 
				
			||||||
                    ok: true,
 | 
					                    callback({
 | 
				
			||||||
                    token: jwt.sign({
 | 
					                        ok: true,
 | 
				
			||||||
                        username: data.username,
 | 
					                        token: jwt.sign({
 | 
				
			||||||
                    }, jwtSecret),
 | 
					                            username: data.username,
 | 
				
			||||||
                })
 | 
					                        }, jwtSecret),
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (user.twofaStatus == 1 && !data.token) {
 | 
				
			||||||
 | 
					                    callback({
 | 
				
			||||||
 | 
					                        tokenRequired: true,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (data.token) {
 | 
				
			||||||
 | 
					                    let verify = notp.totp.verify(data.token, user.twofa_secret);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (verify && verify.delta == 0) {
 | 
				
			||||||
 | 
					                        callback({
 | 
				
			||||||
 | 
					                            ok: true,
 | 
				
			||||||
 | 
					                            token: jwt.sign({
 | 
				
			||||||
 | 
					                                username: data.username,
 | 
				
			||||||
 | 
					                            }, jwtSecret),
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        callback({
 | 
				
			||||||
 | 
					                            ok: false,
 | 
				
			||||||
 | 
					                            msg: "Invalid Token!",
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                callback({
 | 
					                callback({
 | 
				
			||||||
                    ok: false,
 | 
					                    ok: false,
 | 
				
			||||||
@@ -240,6 +270,130 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
 | 
				
			|||||||
            callback();
 | 
					            callback();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        socket.on("prepare2FA", async (callback) => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                checkLogin(socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let user = await R.findOne("user", " id = ? AND active = 1 ", [
 | 
				
			||||||
 | 
					                    socket.userID,
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (user.twofa_status == 0) {
 | 
				
			||||||
 | 
					                    let newSecret = await genSecret()
 | 
				
			||||||
 | 
					                    let encodedSecret = base32.encode(newSecret);
 | 
				
			||||||
 | 
					                    let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [
 | 
				
			||||||
 | 
					                        newSecret,
 | 
				
			||||||
 | 
					                        socket.userID,
 | 
				
			||||||
 | 
					                    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    callback({
 | 
				
			||||||
 | 
					                        ok: true,
 | 
				
			||||||
 | 
					                        uri: uri,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    callback({
 | 
				
			||||||
 | 
					                        ok: false,
 | 
				
			||||||
 | 
					                        msg: "2FA is already enabled.",
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: false,
 | 
				
			||||||
 | 
					                    msg: "Error while trying to prepare 2FA.",
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        socket.on("save2FA", async (callback) => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                checkLogin(socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
 | 
				
			||||||
 | 
					                    socket.userID,
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: true,
 | 
				
			||||||
 | 
					                    msg: "2FA Enabled.",
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: false,
 | 
				
			||||||
 | 
					                    msg: "Error while trying to change 2FA.",
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        socket.on("disable2FA", async (callback) => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                checkLogin(socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
 | 
				
			||||||
 | 
					                    socket.userID,
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: true,
 | 
				
			||||||
 | 
					                    msg: "2FA Disabled.",
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: false,
 | 
				
			||||||
 | 
					                    msg: "Error while trying to change 2FA.",
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        socket.on("verifyToken", async (token, callback) => {
 | 
				
			||||||
 | 
					            let user = await R.findOne("user", " id = ? AND active = 1 ", [
 | 
				
			||||||
 | 
					                socket.userID,
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let verify = notp.totp.verify(token, user.twofa_secret);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (verify && verify.delta == 0) {
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: true,
 | 
				
			||||||
 | 
					                    valid: true,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: false,
 | 
				
			||||||
 | 
					                    msg: "Invalid Token.",
 | 
				
			||||||
 | 
					                    valid: false,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        socket.on("twoFAStatus", async (callback) => {
 | 
				
			||||||
 | 
					            checkLogin(socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                let user = await R.findOne("user", " id = ? AND active = 1 ", [
 | 
				
			||||||
 | 
					                    socket.userID,
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (user.twofa_status == 1) {
 | 
				
			||||||
 | 
					                    callback({
 | 
				
			||||||
 | 
					                        ok: true,
 | 
				
			||||||
 | 
					                        status: true,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    callback({
 | 
				
			||||||
 | 
					                        ok: true,
 | 
				
			||||||
 | 
					                        status: false,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                callback({
 | 
				
			||||||
 | 
					                    ok: false,
 | 
				
			||||||
 | 
					                    msg: "Error while trying to get 2FA status.",
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        socket.on("needSetup", async (callback) => {
 | 
					        socket.on("needSetup", async (callback) => {
 | 
				
			||||||
            callback(needSetup);
 | 
					            callback(needSetup);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -271,3 +271,13 @@ exports.getTotalClientInRoom = (io, roomName) => {
 | 
				
			|||||||
        return 0;
 | 
					        return 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports.genSecret = () => {
 | 
				
			||||||
 | 
					    let secret = "";
 | 
				
			||||||
 | 
					    let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 | 
				
			||||||
 | 
					    let charsLength = chars.length;
 | 
				
			||||||
 | 
					    for ( let i = 0; i < 64; i++ ) {
 | 
				
			||||||
 | 
					        secret += chars.charAt(Math.floor(Math.random() * charsLength));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return secret;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,16 +4,23 @@
 | 
				
			|||||||
            <form @submit.prevent="submit">
 | 
					            <form @submit.prevent="submit">
 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal" />
 | 
					                <h1 class="h3 mb-3 fw-normal" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-floating">
 | 
					                <div v-if="!tokenRequired" class="form-floating">
 | 
				
			||||||
                    <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
 | 
					                    <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
 | 
				
			||||||
                    <label for="floatingInput">{{ $t("Username") }}</label>
 | 
					                    <label for="floatingInput">{{ $t("Username") }}</label>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-floating mt-3">
 | 
					                <div v-if="!tokenRequired" class="form-floating mt-3">
 | 
				
			||||||
                    <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
 | 
					                    <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
 | 
				
			||||||
                    <label for="floatingPassword">{{ $t("Password") }}</label>
 | 
					                    <label for="floatingPassword">{{ $t("Password") }}</label>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div v-if="tokenRequired">
 | 
				
			||||||
 | 
					                    <div class="form-floating mt-3">
 | 
				
			||||||
 | 
					                        <input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
 | 
				
			||||||
 | 
					                        <label for="floatingToken">{{ $t("Token") }}</label>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
 | 
					                <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
 | 
				
			||||||
                    <div class="form-check">
 | 
					                    <div class="form-check">
 | 
				
			||||||
                        <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
 | 
					                        <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
 | 
				
			||||||
@@ -42,16 +49,24 @@ export default {
 | 
				
			|||||||
            processing: false,
 | 
					            processing: false,
 | 
				
			||||||
            username: "",
 | 
					            username: "",
 | 
				
			||||||
            password: "",
 | 
					            password: "",
 | 
				
			||||||
 | 
					            token: "",
 | 
				
			||||||
            res: null,
 | 
					            res: null,
 | 
				
			||||||
 | 
					            tokenRequired: false,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
        submit() {
 | 
					        submit() {
 | 
				
			||||||
            this.processing = true;
 | 
					            this.processing = true;
 | 
				
			||||||
            this.$root.login(this.username, this.password, (res) => {
 | 
					
 | 
				
			||||||
 | 
					            this.$root.login(this.username, this.password, this.token, (res) => {
 | 
				
			||||||
                this.processing = false;
 | 
					                this.processing = false;
 | 
				
			||||||
                this.res = res;
 | 
					                console.log(res)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (res.tokenRequired) {
 | 
				
			||||||
 | 
					                    this.tokenRequired = true;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    this.res = res;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
@@ -128,5 +128,19 @@ export default {
 | 
				
			|||||||
    backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
 | 
					    backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
 | 
				
			||||||
    alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
 | 
					    alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
 | 
				
			||||||
    alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
 | 
					    alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
 | 
				
			||||||
 | 
					    twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
 | 
				
			||||||
 | 
					    "Verify Token": "Token verifizieren",
 | 
				
			||||||
 | 
					    "Setup 2FA": "2FA Einrichten",
 | 
				
			||||||
 | 
					    "Enable 2FA": "2FA Aktivieren",
 | 
				
			||||||
 | 
					    "Disable 2FA": "2FA deaktivieren",
 | 
				
			||||||
 | 
					    "2FA Settings": "2FA Einstellungen",
 | 
				
			||||||
 | 
					    confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
 | 
				
			||||||
 | 
					    confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
 | 
				
			||||||
 | 
					    tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
 | 
				
			||||||
 | 
					    "Two Factor Authentication": "Zwei Faktor Authentifizierung",
 | 
				
			||||||
 | 
					    Active: "Aktiv",
 | 
				
			||||||
 | 
					    Inactive: "Inaktiv",
 | 
				
			||||||
 | 
					    Token: "Token",
 | 
				
			||||||
 | 
					    "Show URI": "URI Anzeigen",
 | 
				
			||||||
    "Clear all statistics": "Lösche alle Statistiken"
 | 
					    "Clear all statistics": "Lösche alle Statistiken"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,10 @@ export default {
 | 
				
			|||||||
    clearEventsMsg: "Are you sure want to delete all events for this monitor?",
 | 
					    clearEventsMsg: "Are you sure want to delete all events for this monitor?",
 | 
				
			||||||
    clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
 | 
					    clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
 | 
				
			||||||
    confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
 | 
					    confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
 | 
				
			||||||
 | 
					    twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
 | 
				
			||||||
 | 
					    tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
 | 
				
			||||||
 | 
					    confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
 | 
				
			||||||
 | 
					    confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
 | 
				
			||||||
    Settings: "Settings",
 | 
					    Settings: "Settings",
 | 
				
			||||||
    Dashboard: "Dashboard",
 | 
					    Dashboard: "Dashboard",
 | 
				
			||||||
    "New Update": "New Update",
 | 
					    "New Update": "New Update",
 | 
				
			||||||
@@ -128,5 +132,15 @@ export default {
 | 
				
			|||||||
    backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
 | 
					    backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
 | 
				
			||||||
    alertNoFile: "Please select a file to import.",
 | 
					    alertNoFile: "Please select a file to import.",
 | 
				
			||||||
    alertWrongFileType: "Please select a JSON file.",
 | 
					    alertWrongFileType: "Please select a JSON file.",
 | 
				
			||||||
 | 
					    "Verify Token": "Verify Token",
 | 
				
			||||||
 | 
					    "Setup 2FA": "Setup 2FA",
 | 
				
			||||||
 | 
					    "Enable 2FA": "Enable 2FA",
 | 
				
			||||||
 | 
					    "Disable 2FA": "Disable 2FA",
 | 
				
			||||||
 | 
					    "2FA Settings": "2FA Settings",
 | 
				
			||||||
 | 
					    "Two Factor Authentication": "Two Factor Authentication",
 | 
				
			||||||
 | 
					    Active: "Active",
 | 
				
			||||||
 | 
					    Inactive: "Inactive",
 | 
				
			||||||
 | 
					    Token: "Token",
 | 
				
			||||||
 | 
					    "Show URI": "Show URI",
 | 
				
			||||||
    "Clear all statistics": "Clear all Statistics"
 | 
					    "Clear all statistics": "Clear all Statistics"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -203,11 +203,15 @@ export default {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        login(username, password, callback) {
 | 
					        login(username, password, token, callback) {
 | 
				
			||||||
            socket.emit("login", {
 | 
					            socket.emit("login", {
 | 
				
			||||||
                username,
 | 
					                username,
 | 
				
			||||||
                password,
 | 
					                password,
 | 
				
			||||||
 | 
					                token,
 | 
				
			||||||
            }, (res) => {
 | 
					            }, (res) => {
 | 
				
			||||||
 | 
					                if (res.tokenRequired) {
 | 
				
			||||||
 | 
					                    callback(res)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (res.ok) {
 | 
					                if (res.ok) {
 | 
				
			||||||
                    this.storage().token = res.token;
 | 
					                    this.storage().token = res.token;
 | 
				
			||||||
@@ -242,6 +246,26 @@ export default {
 | 
				
			|||||||
            this.clearData()
 | 
					            this.clearData()
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        prepare2FA(callback) {
 | 
				
			||||||
 | 
					            socket.emit("prepare2FA", callback)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        save2FA(secret, callback) {
 | 
				
			||||||
 | 
					            socket.emit("save2FA", callback)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        disable2FA(callback) {
 | 
				
			||||||
 | 
					            socket.emit("disable2FA", callback)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        verifyToken(token, callback) {
 | 
				
			||||||
 | 
					            socket.emit("verifyToken", token, callback)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        twoFAStatus(callback) {
 | 
				
			||||||
 | 
					            socket.emit("twoFAStatus", callback)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        add(monitor, callback) {
 | 
					        add(monitor, callback) {
 | 
				
			||||||
            socket.emit("add", monitor, callback)
 | 
					            socket.emit("add", monitor, callback)
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -120,6 +120,14 @@
 | 
				
			|||||||
                                </form>
 | 
					                                </form>
 | 
				
			||||||
                            </template>
 | 
					                            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <h2 class="mt-5 mb-2">
 | 
				
			||||||
 | 
					                                {{ $t("Two Factor Authentication") }}
 | 
				
			||||||
 | 
					                            </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <div class="mb-3">
 | 
				
			||||||
 | 
					                                <button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            <h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
 | 
					                            <h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            <p>
 | 
					                            <p>
 | 
				
			||||||
@@ -186,6 +194,7 @@
 | 
				
			|||||||
            </footer>
 | 
					            </footer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <NotificationDialog ref="notificationDialog" />
 | 
					            <NotificationDialog ref="notificationDialog" />
 | 
				
			||||||
 | 
					            <TwoFADialog ref="TwoFADialog" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
 | 
					            <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
 | 
				
			||||||
                <template v-if="$i18n.locale === 'es-ES' ">
 | 
					                <template v-if="$i18n.locale === 'es-ES' ">
 | 
				
			||||||
@@ -269,6 +278,7 @@ import dayjs from "dayjs";
 | 
				
			|||||||
import utc from "dayjs/plugin/utc"
 | 
					import utc from "dayjs/plugin/utc"
 | 
				
			||||||
import timezone from "dayjs/plugin/timezone"
 | 
					import timezone from "dayjs/plugin/timezone"
 | 
				
			||||||
import NotificationDialog from "../components/NotificationDialog.vue";
 | 
					import NotificationDialog from "../components/NotificationDialog.vue";
 | 
				
			||||||
 | 
					import TwoFADialog from "../components/TwoFADialog.vue";
 | 
				
			||||||
dayjs.extend(utc)
 | 
					dayjs.extend(utc)
 | 
				
			||||||
dayjs.extend(timezone)
 | 
					dayjs.extend(timezone)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -279,6 +289,7 @@ const toast = useToast()
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
    components: {
 | 
					    components: {
 | 
				
			||||||
        NotificationDialog,
 | 
					        NotificationDialog,
 | 
				
			||||||
 | 
					        TwoFADialog,
 | 
				
			||||||
        Confirm,
 | 
					        Confirm,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data() {
 | 
					    data() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,7 +87,7 @@ export default {
 | 
				
			|||||||
                if (res.ok) {
 | 
					                if (res.ok) {
 | 
				
			||||||
                    this.processing = true;
 | 
					                    this.processing = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.$root.login(this.username, this.password, (res) => {
 | 
					                    this.$root.login(this.username, this.password, "", (res) => {
 | 
				
			||||||
                        this.processing = false;
 | 
					                        this.processing = false;
 | 
				
			||||||
                        this.$router.push("/")
 | 
					                        this.$router.push("/")
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user