diff --git a/db/patch-2fa.sql b/db/patch-2fa.sql
new file mode 100644
index 000000000..35069d854
--- /dev/null
+++ b/db/patch-2fa.sql
@@ -0,0 +1,10 @@
+-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
+BEGIN TRANSACTION;
+
+ALTER TABLE user
+    ADD twofa_secret VARCHAR(64);
+
+ALTER TABLE user
+    ADD twofa_status BOOLEAN default 0 NOT NULL;
+
+COMMIT;
diff --git a/package.json b/package.json
index e1c405cf3..e71fde810 100644
--- a/package.json
+++ b/package.json
@@ -56,20 +56,24 @@
         "http-graceful-shutdown": "^3.1.4",
         "jsonwebtoken": "^8.5.1",
         "nodemailer": "^6.6.3",
+        "notp": "^2.0.3",
         "password-hash": "^1.2.2",
         "prom-client": "^13.2.0",
         "prometheus-api-metrics": "^3.2.0",
+        "qrcode": "^1.4.4",
         "redbean-node": "0.1.2",
         "socket.io": "^4.2.0",
         "socket.io-client": "^4.2.0",
         "sqlite3": "github:mapbox/node-sqlite3#593c9d",
         "tcp-ping": "^0.1.1",
+        "thirty-two": "^1.0.2",
         "v-pagination-3": "^0.1.6",
         "vue": "^3.2.8",
         "vue-chart-3": "^0.5.7",
         "vue-confirm-dialog": "^1.0.2",
         "vue-i18n": "^9.1.7",
         "vue-multiselect": "^3.0.0-alpha.2",
+        "vue-qrcode": "^1.0.0",
         "vue-router": "^4.0.11",
         "vue-toastification": "^2.0.0-rc.1"
     },
diff --git a/server/database.js b/server/database.js
index e0bb0c9b8..4b3ad443e 100644
--- a/server/database.js
+++ b/server/database.js
@@ -30,6 +30,7 @@ class Database {
     static patchList = {
         "patch-setting-value-type.sql": true,
         "patch-improve-performance.sql": true,
+        "patch-2fa.sql": true,
     }
 
     /**
diff --git a/server/server.js b/server/server.js
index e4ad586aa..08225ebb2 100644
--- a/server/server.js
+++ b/server/server.js
@@ -22,11 +22,15 @@ const gracefulShutdown = require("http-graceful-shutdown");
 debug("Importing prometheus-api-metrics");
 const prometheusAPIMetrics = require("prometheus-api-metrics");
 
+debug("Importing 2FA Modules");
+const notp = require("notp");
+const base32 = require("thirty-two");
+
 console.log("Importing this project modules");
 debug("Importing Monitor");
 const Monitor = require("./model/monitor");
 debug("Importing Settings");
-const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
+const { getSettings, setSettings, setting, initJWTSecret, genSecret } = require("./util-server");
 
 debug("Importing Notification");
 const { Notification } = require("./notification");
@@ -219,12 +223,38 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
             if (user) {
                 afterLogin(socket, user)
 
-                callback({
-                    ok: true,
-                    token: jwt.sign({
-                        username: data.username,
-                    }, jwtSecret),
-                })
+                if (user.twofaStatus == 0) {
+                    callback({
+                        ok: true,
+                        token: jwt.sign({
+                            username: data.username,
+                        }, jwtSecret),
+                    })
+                }
+
+                if (user.twofaStatus == 1 && !data.token) {
+                    callback({
+                        tokenRequired: true,
+                    })
+                }
+
+                if (data.token) {
+                    let verify = notp.totp.verify(data.token, user.twofa_secret);
+
+                    if (verify && verify.delta == 0) {
+                        callback({
+                            ok: true,
+                            token: jwt.sign({
+                                username: data.username,
+                            }, jwtSecret),
+                        })
+                    } else {
+                        callback({
+                            ok: false,
+                            msg: "Invalid Token!",
+                        })
+                    }
+                }
             } else {
                 callback({
                     ok: false,
@@ -240,6 +270,130 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
             callback();
         });
 
+        socket.on("prepare2FA", async (callback) => {
+            try {
+                checkLogin(socket)
+
+                let user = await R.findOne("user", " id = ? AND active = 1 ", [
+                    socket.userID,
+                ])
+
+                if (user.twofa_status == 0) {
+                    let newSecret = await genSecret()
+                    let encodedSecret = base32.encode(newSecret);
+                    let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
+
+                    await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [
+                        newSecret,
+                        socket.userID,
+                    ]);
+
+                    callback({
+                        ok: true,
+                        uri: uri,
+                    })
+                } else {
+                    callback({
+                        ok: false,
+                        msg: "2FA is already enabled.",
+                    })
+                }
+            } catch (error) {
+                callback({
+                    ok: false,
+                    msg: "Error while trying to prepare 2FA.",
+                })
+            }
+        });
+
+        socket.on("save2FA", async (callback) => {
+            try {
+                checkLogin(socket)
+
+                await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [
+                    socket.userID,
+                ]);
+
+                callback({
+                    ok: true,
+                    msg: "2FA Enabled.",
+                })
+            } catch (error) {
+                callback({
+                    ok: false,
+                    msg: "Error while trying to change 2FA.",
+                })
+            }
+        });
+
+        socket.on("disable2FA", async (callback) => {
+            try {
+                checkLogin(socket)
+
+                await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
+                    socket.userID,
+                ]);
+
+                callback({
+                    ok: true,
+                    msg: "2FA Disabled.",
+                })
+            } catch (error) {
+                callback({
+                    ok: false,
+                    msg: "Error while trying to change 2FA.",
+                })
+            }
+        });
+
+        socket.on("verifyToken", async (token, callback) => {
+            let user = await R.findOne("user", " id = ? AND active = 1 ", [
+                socket.userID,
+            ])
+
+            let verify = notp.totp.verify(token, user.twofa_secret);
+
+            if (verify && verify.delta == 0) {
+                callback({
+                    ok: true,
+                    valid: true,
+                })
+            } else {
+                callback({
+                    ok: false,
+                    msg: "Invalid Token.",
+                    valid: false,
+                })
+            }
+        });
+
+        socket.on("twoFAStatus", async (callback) => {
+            checkLogin(socket)
+
+            try {
+                let user = await R.findOne("user", " id = ? AND active = 1 ", [
+                    socket.userID,
+                ])
+
+                if (user.twofa_status == 1) {
+                    callback({
+                        ok: true,
+                        status: true,
+                    })
+                } else {
+                    callback({
+                        ok: true,
+                        status: false,
+                    })
+                }
+            } catch (error) {
+                callback({
+                    ok: false,
+                    msg: "Error while trying to get 2FA status.",
+                })
+            }
+        });
+
         socket.on("needSetup", async (callback) => {
             callback(needSetup);
         });
diff --git a/server/util-server.js b/server/util-server.js
index a2fef0656..079bd82f3 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -271,3 +271,13 @@ exports.getTotalClientInRoom = (io, roomName) => {
         return 0;
     }
 }
+
+exports.genSecret = () => {
+    let secret = "";
+    let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    let charsLength = chars.length;
+    for ( let i = 0; i < 64; i++ ) {
+        secret += chars.charAt(Math.floor(Math.random() * charsLength));
+    }
+    return secret;
+}
diff --git a/src/components/Login.vue b/src/components/Login.vue
index bd51759c7..ca36fdb9f 100644
--- a/src/components/Login.vue
+++ b/src/components/Login.vue
@@ -4,16 +4,23 @@
             <form @submit.prevent="submit">
                 <h1 class="h3 mb-3 fw-normal" />
 
-                <div class="form-floating">
+                <div v-if="!tokenRequired" class="form-floating">
                     <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
                     <label for="floatingInput">{{ $t("Username") }}</label>
                 </div>
 
-                <div class="form-floating mt-3">
+                <div v-if="!tokenRequired" class="form-floating mt-3">
                     <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
                     <label for="floatingPassword">{{ $t("Password") }}</label>
                 </div>
 
+                <div v-if="tokenRequired">
+                    <div class="form-floating mt-3">
+                        <input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
+                        <label for="floatingToken">{{ $t("Token") }}</label>
+                    </div>
+                </div>
+
                 <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
                     <div class="form-check">
                         <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
@@ -42,16 +49,24 @@ export default {
             processing: false,
             username: "",
             password: "",
-
+            token: "",
             res: null,
+            tokenRequired: false,
         }
     },
     methods: {
         submit() {
             this.processing = true;
-            this.$root.login(this.username, this.password, (res) => {
+
+            this.$root.login(this.username, this.password, this.token, (res) => {
                 this.processing = false;
-                this.res = res;
+                console.log(res)
+
+                if (res.tokenRequired) {
+                    this.tokenRequired = true;
+                } else {
+                    this.res = res;
+                }
             })
         },
     },
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index ffb7ba71d..d689b0c91 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -410,7 +410,7 @@
 
                             <div class="form-check form-switch">
                                 <input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
-                                <label class="form-check-label">{{ $t("Also apply to existing monitors") }}</label>
+                                <label class="form-check-label">{{ $t("Apply on all existing monitors") }}</label>
                             </div>
                         </div>
                     </div>
diff --git a/src/components/TwoFADialog.vue b/src/components/TwoFADialog.vue
new file mode 100644
index 000000000..b7b9668d8
--- /dev/null
+++ b/src/components/TwoFADialog.vue
@@ -0,0 +1,178 @@
+<template>
+    <form @submit.prevent="submit">
+        <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">
+                            {{ $t("Setup 2FA") }}
+                            <span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
+                            <span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
+                        </h5>
+                        <button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
+                    </div>
+                    <div class="modal-body">
+                        <div class="mb-3">
+                            <div v-if="uri && twoFAStatus == false" class="mx-auto text-center" style="width: 210px;">
+                                <vue-qrcode :key="uri" :value="uri" type="image/png" :quality="1" :color="{ light: '#ffffffff' }" />
+                                <button v-show="!showURI" type="button" class="btn btn-outline-primary btn-sm mt-2" @click="showURI = true">{{ $t("Show URI") }}</button>
+                            </div>
+                            <p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
+
+                            <button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
+                                {{ $t("Enable 2FA") }}
+                            </button>
+
+                            <button v-if="twoFAStatus == true" class="btn btn-danger" type="button" :disabled="processing" @click="confirmDisableTwoFA()">
+                                {{ $t("Disable 2FA") }}
+                            </button>
+
+                            <div v-if="uri && twoFAStatus == false" class="mt-3">
+                                <label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
+                                <div class="input-group">
+                                    <input v-model="token" type="text" maxlength="6" class="form-control">
+                                    <button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
+                                </div>
+                                <p v-show="tokenValid" class="mt-2" style="color: green">{{ $t("tokenValidSettingsMsg") }}</p>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div v-if="uri && twoFAStatus == false" class="modal-footer">
+                        <button type="submit" class="btn btn-primary" :disabled="processing || tokenValid == false" @click="confirmEnableTwoFA()">
+                            <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
+                            {{ $t("Save") }}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+
+    <Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
+        {{ $t("confirmEnableTwoFAMsg") }}
+    </Confirm>
+
+    <Confirm ref="confirmDisableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disable2FA">
+        {{ $t("confirmDisableTwoFAMsg") }}
+    </Confirm>
+</template>
+
+<script lang="ts">
+import { Modal } from "bootstrap"
+import Confirm from "./Confirm.vue";
+import VueQrcode from "vue-qrcode"
+import { useToast } from "vue-toastification"
+const toast = useToast()
+
+export default {
+    components: {
+        Confirm,
+        VueQrcode,
+    },
+    props: {},
+    data() {
+        return {
+            processing: false,
+            uri: null,
+            tokenValid: false,
+            twoFAStatus: null,
+            token: null,
+            showURI: false,
+        }
+    },
+    mounted() {
+        this.modal = new Modal(this.$refs.modal)
+        this.getStatus();
+    },
+    methods: {
+        show() {
+            this.modal.show()
+        },
+
+        confirmEnableTwoFA() {
+            this.$refs.confirmEnableTwoFA.show()
+        },
+
+        confirmDisableTwoFA() {
+            this.$refs.confirmDisableTwoFA.show()
+        },
+
+        prepare2FA() {
+            this.processing = true;
+
+            this.$root.getSocket().emit("prepare2FA", (res) => {
+                this.processing = false;
+
+                if (res.ok) {
+                    this.uri = res.uri;
+                } else {
+                    toast.error(res.msg);
+                }
+            })
+        },
+
+        save2FA() {
+            this.processing = true;
+
+            this.$root.getSocket().emit("save2FA", (res) => {
+                this.processing = false;
+
+                if (res.ok) {
+                    this.$root.toastRes(res)
+                    this.getStatus();
+                    this.modal.hide();
+                } else {
+                    toast.error(res.msg);
+                }
+            })
+        },
+
+        disable2FA() {
+            this.processing = true;
+
+            this.$root.getSocket().emit("disable2FA", (res) => {
+                this.processing = false;
+
+                if (res.ok) {
+                    this.$root.toastRes(res)
+                    this.getStatus();
+                    this.modal.hide();
+                } else {
+                    toast.error(res.msg);
+                }
+            })
+        },
+
+        verifyToken() {
+            this.$root.getSocket().emit("verifyToken", this.token, (res) => {
+                if (res.ok) {
+                    this.tokenValid = res.valid;
+                } else {
+                    toast.error(res.msg);
+                }
+            })
+        },
+
+        getStatus() {
+            this.$root.getSocket().emit("twoFAStatus", (res) => {
+                if (res.ok) {
+                    this.twoFAStatus = res.status;
+                } else {
+                    toast.error(res.msg);
+                }
+            })
+        },
+    },
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../assets/vars.scss";
+
+.dark {
+    .modal-dialog .form-text, .modal-dialog p {
+        color: $dark-font-color;
+    }
+}
+</style>
diff --git a/src/languages/da-DK.js b/src/languages/da-DK.js
index 2859655e8..6e8069aa1 100644
--- a/src/languages/da-DK.js
+++ b/src/languages/da-DK.js
@@ -17,8 +17,8 @@ export default {
     Down: "Inaktiv",
     Pending: "Afventer",
     Unknown: "Ukendt",
-    Pause: "Pause",
-    pauseDashboardHome: "Pauset",
+    Pause: "Stands",
+    pauseDashboardHome: "Standset",
     Name: "Navn",
     Status: "Status",
     DateTime: "Dato / Tid",
@@ -36,8 +36,8 @@ export default {
     hour: "Timer",
     "-hour": "-Timer",
     checkEverySecond: "Tjek hvert {0} sekund",
-    "Avg.": "Gennemsnit",
-    Response: " Respons",
+    "Avg.": "Gns.",
+    Response: "Respons",
     Ping: "Ping",
     "Monitor Type": "Overvåger Type",
     Keyword: "Nøgleord",
@@ -103,29 +103,29 @@ export default {
     "Resolver Server": "Navne-server",
     rrtypeDescription: "Vælg den type RR, du vil overvåge.",
     "Last Result": "Seneste resultat",
-    pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?",
+    pauseMonitorMsg: "Er du sikker på, at du vil standse Overvågeren?",
     "Create your admin account": "Opret din administratorkonto",
     "Repeat Password": "Gentag adgangskoden",
     "Resource Record Type": "Resource Record Type",
-    respTime: "Resp. Time (ms)",
+    respTime: "Resp. Tid (ms)",
     notAvailableShort: "N/A",
-    Create: "Create",
-    clearEventsMsg: "Are you sure want to delete all events for this monitor?",
-    clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
-    confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
-    "Clear Data": "Clear Data",
+    Create: "Opret",
+    clearEventsMsg: "Er du sikker på vil slette alle events for denne Overvåger?",
+    clearHeartbeatsMsg: "Er du sikker på vil slette alle heartbeats for denne Overvåger?",
+    confirmClearStatisticsMsg: "Vil du helt sikkert slette ALLE statistikker?",
+    "Clear Data": "Ryd Data",
     Events: "Events",
     Heartbeats: "Heartbeats",
-    "Auto Get": "Auto Get",
-    enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
-    "Default enabled": "Default enabled",
-    "Also apply to existing monitors": "Also apply to existing monitors",
-    "Import/Export Backup": "Import/Export Backup",
-    Export: "Export",
+    "Auto Get": "Auto-hent",
+    enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
+    "Default enabled": "Standard aktiveret",
+    "Also apply to existing monitors": "Anvend også på eksisterende overvågere",
+    "Import/Export Backup": " Importér/Eksportér sikkerhedskopi",
+    Export: "Eksport",
     Import: "Import",
-    backupDescription: "You can backup all monitors and all notifications into a JSON file.",
-    backupDescription2: "PS: History and event data is not included.",
-    backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
-    alertNoFile: "Please select a file to import.",
-    alertWrongFileType: "Please select a JSON file."
+    backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
+    backupDescription2: "PS: Historik og hændelsesdata er ikke inkluderet.",
+    backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
+    alertNoFile: "Vælg en fil der skal importeres.",
+    alertWrongFileType: "Vælg venligst en JSON-fil."
 }
diff --git a/src/languages/de-DE.js b/src/languages/de-DE.js
index 083d939a4..e401f2bf8 100644
--- a/src/languages/de-DE.js
+++ b/src/languages/de-DE.js
@@ -36,8 +36,8 @@ export default {
     hour: "Stunde",
     "-hour": "-Stunden",
     checkEverySecond: "Überprüfe alle {0} Sekunden",
-    "Avg.": "Durchschn. ",
-    Response: " Antwortzeit",
+    "Avg.": "Durchschn.",
+    Response: "Antwortzeit",
     Ping: "Ping",
     "Monitor Type": "Monitor Typ",
     Keyword: "Schlüsselwort",
@@ -119,7 +119,7 @@ export default {
     respTime: "Antw. Zeit (ms)",
     notAvailableShort: "N/A",
     "Default enabled": "Standardmäßig aktiviert",
-    "Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren",
+    "Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
     enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
     Create: "Erstellen",
     "Auto Get": "Auto Get",
@@ -135,4 +135,19 @@ export default {
     "Import Options": "Import Optionen",
     confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.",
     "Keep both": "Beide behalten",
+    twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
+    "Verify Token": "Token verifizieren",
+    "Setup 2FA": "2FA Einrichten",
+    "Enable 2FA": "2FA Aktivieren",
+    "Disable 2FA": "2FA deaktivieren",
+    "2FA Settings": "2FA Einstellungen",
+    confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
+    confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
+    tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
+    "Two Factor Authentication": "Zwei Faktor Authentifizierung",
+    Active: "Aktiv",
+    Inactive: "Inaktiv",
+    Token: "Token",
+    "Show URI": "URI Anzeigen",
+    "Clear all statistics": "Lösche alle Statistiken"
 }
diff --git a/src/languages/en.js b/src/languages/en.js
index 0644610de..c81db3105 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "English",
     checkEverySecond: "Check every {0} seconds.",
-    "Avg.": "Avg. ",
+    "Avg.": "Avg.",
     retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
     ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
     upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
@@ -22,6 +22,10 @@ export default {
     confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
     importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
     confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
+    twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
+    tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
+    confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
+    confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
     Settings: "Settings",
     Dashboard: "Dashboard",
     "New Update": "New Update",
@@ -119,7 +123,7 @@ export default {
     respTime: "Resp. Time (ms)",
     notAvailableShort: "N/A",
     "Default enabled": "Default enabled",
-    "Also apply to existing monitors": "Also apply to existing monitors",
+    "Apply on all existing monitors": "Apply on all existing monitors",
     Create: "Create",
     "Clear Data": "Clear Data",
     Events: "Events",
@@ -135,4 +139,15 @@ export default {
     "Overwrite": "Overwrite",
     "Import Options": "Import Options",
     "Keep both": "Keep both",
+    "Verify Token": "Verify Token",
+    "Setup 2FA": "Setup 2FA",
+    "Enable 2FA": "Enable 2FA",
+    "Disable 2FA": "Disable 2FA",
+    "2FA Settings": "2FA Settings",
+    "Two Factor Authentication": "Two Factor Authentication",
+    Active: "Active",
+    Inactive: "Inactive",
+    Token: "Token",
+    "Show URI": "Show URI",
+    "Clear all statistics": "Clear all Statistics"
 }
diff --git a/src/languages/es-ES.js b/src/languages/es-ES.js
index ba216dcff..b52c1654f 100644
--- a/src/languages/es-ES.js
+++ b/src/languages/es-ES.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Español",
     checkEverySecond: "Comprobar cada {0} segundos.",
-    "Avg.": "Media. ",
+    "Avg.": "Media.",
     retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
     ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
     upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
diff --git a/src/languages/et-EE.js b/src/languages/et-EE.js
index 3992e9025..fca24a33d 100644
--- a/src/languages/et-EE.js
+++ b/src/languages/et-EE.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "eesti",
     checkEverySecond: "Kontrolli {0} sekundilise vahega.",
-    "Avg.": "≈ ",
+    "Avg.": "≈",
     retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
     ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
     upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
@@ -10,7 +10,7 @@ export default {
     passwordNotMatchMsg: "Salasõnad ei kattu.",
     notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
     keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
-    pauseDashboardHome: "Seiskamine",
+    pauseDashboardHome: "Seismas",
     deleteMonitorMsg: "Kas soovid eemaldada seire?",
     deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
     resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
@@ -109,23 +109,23 @@ export default {
     "Repeat Password": "korda salasõna",
     respTime: "Reageerimisaeg (ms)",
     notAvailableShort: "N/A",
-    enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
-    clearEventsMsg: "Are you sure want to delete all events for this monitor?",
-    clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
-    confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
-    "Import/Export Backup": "Import/Export Backup",
-    Export: "Export",
+    enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.",
+    clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
+    clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
+    confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
+    "Import/Export Backup": "Impordi/Ekspordi varukoopia",
+    Export: "Eksport",
     Import: "Import",
-    "Default enabled": "Default enabled",
-    "Also apply to existing monitors": "Also apply to existing monitors",
-    Create: "Create",
-    "Clear Data": "Clear Data",
-    Events: "Events",
-    Heartbeats: "Heartbeats",
-    "Auto Get": "Auto Get",
-    backupDescription: "You can backup all monitors and all notifications into a JSON file.",
-    backupDescription2: "PS: History and event data is not included.",
-    backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
-    alertNoFile: "Please select a file to import.",
-    alertWrongFileType: "Please select a JSON file."
+    "Default enabled": "Kasuta vaikimisi",
+    "Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel",
+    Create: "Loo konto",
+    "Clear Data": "Eemalda andmed",
+    Events: "Sündmused",
+    Heartbeats: "Tuksed",
+    "Auto Get": "Hangi automaatselt",
+    backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
+    backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
+    backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.",
+    alertNoFile: "Palun lisa fail, mida importida.",
+    alertWrongFileType: "Palun lisa JSON-formaadis fail."
 }
diff --git a/src/languages/ja.js b/src/languages/ja.js
index 4ef10a5a0..6d0693d5a 100644
--- a/src/languages/ja.js
+++ b/src/languages/ja.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "日本語",
     checkEverySecond: "{0}秒ごとにチェックします。",
-    "Avg.": "平均 ",
+    "Avg.": "平均",
     retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
     ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
     upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
diff --git a/src/languages/ko-KR.js b/src/languages/ko-KR.js
index da1491a5e..68479ade1 100644
--- a/src/languages/ko-KR.js
+++ b/src/languages/ko-KR.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "한국어",
     checkEverySecond: "{0} 초마다 체크해요.",
-    "Avg.": "평균 ",
+    "Avg.": "평균",
     retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
     ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
     upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
diff --git a/src/languages/nl-NL.js b/src/languages/nl-NL.js
index 2c5f10104..48e3b3a29 100644
--- a/src/languages/nl-NL.js
+++ b/src/languages/nl-NL.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Nederlands",
     checkEverySecond: "Controleer elke {0} seconden.",
-    "Avg.": "Gem. ",
+    "Avg.": "Gem.",
     retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
     ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
     upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
diff --git a/src/languages/pl.js b/src/languages/pl.js
index 4863cccbd..3029b3497 100644
--- a/src/languages/pl.js
+++ b/src/languages/pl.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Polski",
     checkEverySecond: "Sprawdzaj co {0} sekund.",
-    "Avg.": "Średnia ",
+    "Avg.": "Średnia",
     retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie",
     ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS",
     upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.",
diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js
index 2dbe254e4..7f5eae760 100644
--- a/src/languages/ru-RU.js
+++ b/src/languages/ru-RU.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Русский",
     checkEverySecond: "Проверять каждые {0} секунд.",
-    "Avg.": "Средн. ",
+    "Avg.": "Средн.",
     retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
     ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
     upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
diff --git a/src/languages/sr-latn.js b/src/languages/sr-latn.js
index a149cb2c8..3dd73d2cc 100644
--- a/src/languages/sr-latn.js
+++ b/src/languages/sr-latn.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Srpski",
     checkEverySecond: "Proveri svakih {0} sekundi.",
-    "Avg.": "Prosečni ",
+    "Avg.": "Prosečni",
     retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.",
     ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.",
     upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.",
diff --git a/src/languages/sr.js b/src/languages/sr.js
index f3e24a662..6931d272e 100644
--- a/src/languages/sr.js
+++ b/src/languages/sr.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Српски",
     checkEverySecond: "Провери сваких {0} секунди.",
-    "Avg.": "Просечни ",
+    "Avg.": "Просечни",
     retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
     ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
     upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",
diff --git a/src/languages/sv-SE.js b/src/languages/sv-SE.js
index 0b4d02ca6..f8749e289 100644
--- a/src/languages/sv-SE.js
+++ b/src/languages/sv-SE.js
@@ -1,7 +1,7 @@
 export default {
     languageName: "Svenska",
     checkEverySecond: "Uppdatera var {0} sekund.",
-    "Avg.": "Genomsnittligt ",
+    "Avg.": "Genomsnittligt",
     retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas",
     ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS",
     upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.",
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index e0088a59f..22d6c510a 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -203,11 +203,15 @@ export default {
             }
         },
 
-        login(username, password, callback) {
+        login(username, password, token, callback) {
             socket.emit("login", {
                 username,
                 password,
+                token,
             }, (res) => {
+                if (res.tokenRequired) {
+                    callback(res)
+                }
 
                 if (res.ok) {
                     this.storage().token = res.token;
@@ -242,6 +246,26 @@ export default {
             this.clearData()
         },
 
+        prepare2FA(callback) {
+            socket.emit("prepare2FA", callback)
+        },
+
+        save2FA(secret, callback) {
+            socket.emit("save2FA", callback)
+        },
+
+        disable2FA(callback) {
+            socket.emit("disable2FA", callback)
+        },
+
+        verifyToken(token, callback) {
+            socket.emit("verifyToken", token, callback)
+        },
+
+        twoFAStatus(callback) {
+            socket.emit("twoFAStatus", callback)
+        },
+
         add(monitor, callback) {
             socket.emit("add", monitor, callback)
         },
diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue
index 9f9cda1de..186184ec9 100644
--- a/src/pages/DashboardHome.vue
+++ b/src/pages/DashboardHome.vue
@@ -26,7 +26,7 @@
                 </div>
             </div>
 
-            <div class="shadow-box table-shadow-box" style="overflow-x: scroll;">
+            <div class="shadow-box table-shadow-box" style="overflow-x: hidden;">
                 <table class="table table-borderless table-hover">
                     <thead>
                         <tr>
@@ -178,5 +178,10 @@ table {
     tr {
         transition: all ease-in-out 0.2ms;
     }
+
+    @media (max-width: 550px) {
+        table-layout: fixed;
+        overflow-wrap: break-word;
+    }
 }
 </style>
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index 776f1c1dd..9092b1792 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -55,7 +55,7 @@
                         </span>
                     </div>
                     <div class="col">
-                        <h4>{{ $t("Avg.") }}{{ pingTitle }}</h4>
+                        <h4>{{ $t("Avg.") }} {{ pingTitle }}</h4>
                         <p>(24{{ $t("-hour") }})</p>
                         <span class="num"><CountUp :value="avgPing" /></span>
                     </div>
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
index 9502f11ce..84c3f8b75 100644
--- a/src/pages/Settings.vue
+++ b/src/pages/Settings.vue
@@ -120,6 +120,14 @@
                                 </form>
                             </template>
 
+                            <h2 class="mt-5 mb-2">
+                                {{ $t("Two Factor Authentication") }}
+                            </h2>
+
+                            <div class="mb-3">
+                                <button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button>
+                            </div>
+
                             <h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
 
                             <p>
@@ -163,10 +171,10 @@
                             <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
 
                             <div class="mb-3">
-                                <button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
-                                <button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
-                                <button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button>
-                                <button class="btn btn-outline-danger me-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button>
+                                <button v-if="settings.disableAuth" class="btn btn-outline-primary me-1 mb-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
+                                <button v-if="! settings.disableAuth" class="btn btn-primary me-1 mb-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
+                                <button v-if="! settings.disableAuth" class="btn btn-danger me-1 mb-1" @click="$root.logout">{{ $t("Logout") }}</button>
+                                <button class="btn btn-outline-danger me-1 mb-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button>
                             </div>
                         </template>
                     </div>
@@ -205,6 +213,7 @@
             </footer>
 
             <NotificationDialog ref="notificationDialog" />
+            <TwoFADialog ref="TwoFADialog" />
 
             <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
                 <template v-if="$i18n.locale === 'es-ES' ">
@@ -291,6 +300,7 @@ import dayjs from "dayjs";
 import utc from "dayjs/plugin/utc"
 import timezone from "dayjs/plugin/timezone"
 import NotificationDialog from "../components/NotificationDialog.vue";
+import TwoFADialog from "../components/TwoFADialog.vue";
 dayjs.extend(utc)
 dayjs.extend(timezone)
 
@@ -301,6 +311,7 @@ const toast = useToast()
 export default {
     components: {
         NotificationDialog,
+        TwoFADialog,
         Confirm,
     },
     data() {
diff --git a/src/pages/Setup.vue b/src/pages/Setup.vue
index 9cb7c5ecb..7966f9e9b 100644
--- a/src/pages/Setup.vue
+++ b/src/pages/Setup.vue
@@ -87,7 +87,7 @@ export default {
                 if (res.ok) {
                     this.processing = true;
 
-                    this.$root.login(this.username, this.password, (res) => {
+                    this.$root.login(this.username, this.password, "", (res) => {
                         this.processing = false;
                         this.$router.push("/")
                     })