Browse Source

feat: copy secret to register otp #551

Jacky 7 months ago
parent
commit
cb3599e721

+ 206 - 223
api/user/otp.go

@@ -1,245 +1,228 @@
 package user
 
 import (
-	"bytes"
-	"crypto/sha1"
-	"encoding/base64"
-	"encoding/hex"
-	"fmt"
-	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/internal/crypto"
-	"github.com/0xJacky/Nginx-UI/internal/user"
-	"github.com/0xJacky/Nginx-UI/model"
-	"github.com/0xJacky/Nginx-UI/query"
-	"github.com/0xJacky/Nginx-UI/settings"
-	"github.com/gin-gonic/gin"
-	"github.com/pquerna/otp"
-	"github.com/pquerna/otp/totp"
-	"image/jpeg"
-	"net/http"
-	"strings"
+    "bytes"
+    "crypto/sha1"
+    "encoding/base64"
+    "encoding/hex"
+    "fmt"
+    "github.com/0xJacky/Nginx-UI/api"
+    "github.com/0xJacky/Nginx-UI/internal/crypto"
+    "github.com/0xJacky/Nginx-UI/internal/user"
+    "github.com/0xJacky/Nginx-UI/model"
+    "github.com/0xJacky/Nginx-UI/query"
+    "github.com/0xJacky/Nginx-UI/settings"
+    "github.com/gin-gonic/gin"
+    "github.com/pquerna/otp"
+    "github.com/pquerna/otp/totp"
+    "image/jpeg"
+    "net/http"
+    "strings"
 )
 
 func GenerateTOTP(c *gin.Context) {
-	u := api.CurrentUser(c)
-
-	issuer := fmt.Sprintf("Nginx UI %s", settings.ServerSettings.Name)
-	issuer = strings.TrimSpace(issuer)
-
-	otpOpts := totp.GenerateOpts{
-		Issuer:      issuer,
-		AccountName: u.Name,
-		Period:      30, // seconds
-		Digits:      otp.DigitsSix,
-		Algorithm:   otp.AlgorithmSHA1,
-	}
-	otpKey, err := totp.Generate(otpOpts)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-	ciphertext, err := crypto.AesEncrypt([]byte(otpKey.Secret()))
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	qrCode, err := otpKey.Image(512, 512)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	// Encode the image to a buffer
-	var buf []byte
-	buffer := bytes.NewBuffer(buf)
-	err = jpeg.Encode(buffer, qrCode, nil)
-	if err != nil {
-		fmt.Println("Error encoding image:", err)
-		return
-	}
-
-	// Convert the buffer to a base64 string
-	base64Str := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes())
-
-	c.JSON(http.StatusOK, gin.H{
-		"secret":  base64.StdEncoding.EncodeToString(ciphertext),
-		"qr_code": base64Str,
-	})
+    u := api.CurrentUser(c)
+
+    issuer := fmt.Sprintf("Nginx UI %s", settings.ServerSettings.Name)
+    issuer = strings.TrimSpace(issuer)
+
+    otpOpts := totp.GenerateOpts{
+        Issuer:      issuer,
+        AccountName: u.Name,
+        Period:      30, // seconds
+        Digits:      otp.DigitsSix,
+        Algorithm:   otp.AlgorithmSHA1,
+    }
+    otpKey, err := totp.Generate(otpOpts)
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    qrCode, err := otpKey.Image(512, 512)
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    // Encode the image to a buffer
+    var buf []byte
+    buffer := bytes.NewBuffer(buf)
+    err = jpeg.Encode(buffer, qrCode, nil)
+    if err != nil {
+        fmt.Println("Error encoding image:", err)
+        return
+    }
+
+    // Convert the buffer to a base64 string
+    base64Str := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes())
+
+    c.JSON(http.StatusOK, gin.H{
+        "secret":  otpKey.Secret(),
+        "qr_code": base64Str,
+    })
 }
 
 func EnrollTOTP(c *gin.Context) {
-	cUser := api.CurrentUser(c)
-	if cUser.EnabledOTP() {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"message": "User already enrolled",
-		})
-		return
-	}
-
-	if settings.ServerSettings.Demo {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"message": "This feature is disabled in demo mode",
-		})
-		return
-	}
-
-	var json struct {
-		Secret   string `json:"secret" binding:"required"`
-		Passcode string `json:"passcode" binding:"required"`
-	}
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-
-	secret, err := base64.StdEncoding.DecodeString(json.Secret)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	decrypted, err := crypto.AesDecrypt(secret)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	if ok := totp.Validate(json.Passcode, string(decrypted)); !ok {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "Invalid passcode",
-		})
-		return
-	}
-
-	ciphertext, err := crypto.AesEncrypt(decrypted)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	u := query.Auth
-	_, err = u.Where(u.ID.Eq(cUser.ID)).Update(u.OTPSecret, ciphertext)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	recoveryCode := sha1.Sum(ciphertext)
-
-	c.JSON(http.StatusOK, gin.H{
-		"message":       "ok",
-		"recovery_code": hex.EncodeToString(recoveryCode[:]),
-	})
+    cUser := api.CurrentUser(c)
+    if cUser.EnabledOTP() {
+        c.JSON(http.StatusBadRequest, gin.H{
+            "message": "User already enrolled",
+        })
+        return
+    }
+
+    if settings.ServerSettings.Demo {
+        c.JSON(http.StatusBadRequest, gin.H{
+            "message": "This feature is disabled in demo mode",
+        })
+        return
+    }
+
+    var json struct {
+        Secret   string `json:"secret" binding:"required"`
+        Passcode string `json:"passcode" binding:"required"`
+    }
+    if !api.BindAndValid(c, &json) {
+        return
+    }
+
+    if ok := totp.Validate(json.Passcode, json.Secret); !ok {
+        c.JSON(http.StatusNotAcceptable, gin.H{
+            "message": "Invalid passcode",
+        })
+        return
+    }
+
+    ciphertext, err := crypto.AesEncrypt([]byte(json.Secret))
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    u := query.Auth
+    _, err = u.Where(u.ID.Eq(cUser.ID)).Update(u.OTPSecret, ciphertext)
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    recoveryCode := sha1.Sum(ciphertext)
+
+    c.JSON(http.StatusOK, gin.H{
+        "message":       "ok",
+        "recovery_code": hex.EncodeToString(recoveryCode[:]),
+    })
 }
 
 func ResetOTP(c *gin.Context) {
-	var json struct {
-		RecoveryCode string `json:"recovery_code"`
-	}
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-	recoverCode, err := hex.DecodeString(json.RecoveryCode)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-	cUser := api.CurrentUser(c)
-	k := sha1.Sum(cUser.OTPSecret)
-	if !bytes.Equal(k[:], recoverCode) {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"message": "Invalid recovery code",
-		})
-		return
-	}
-
-	u := query.Auth
-	_, err = u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null())
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
+    var json struct {
+        RecoveryCode string `json:"recovery_code"`
+    }
+    if !api.BindAndValid(c, &json) {
+        return
+    }
+    recoverCode, err := hex.DecodeString(json.RecoveryCode)
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+    cUser := api.CurrentUser(c)
+    k := sha1.Sum(cUser.OTPSecret)
+    if !bytes.Equal(k[:], recoverCode) {
+        c.JSON(http.StatusBadRequest, gin.H{
+            "message": "Invalid recovery code",
+        })
+        return
+    }
+
+    u := query.Auth
+    _, err = u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null())
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
 }
 
 func OTPStatus(c *gin.Context) {
-	status := false
-	u, ok := c.Get("user")
-	if ok {
-		status = u.(*model.Auth).EnabledOTP()
-	}
-	c.JSON(http.StatusOK, gin.H{
-		"status": status,
-	})
+    status := false
+    u, ok := c.Get("user")
+    if ok {
+        status = u.(*model.Auth).EnabledOTP()
+    }
+    c.JSON(http.StatusOK, gin.H{
+        "status": status,
+    })
 }
 
 func SecureSessionStatus(c *gin.Context) {
-	u, ok := c.Get("user")
-	if !ok || !u.(*model.Auth).EnabledOTP() {
-		c.JSON(http.StatusOK, gin.H{
-			"status": false,
-		})
-		return
-	}
-	ssid := c.GetHeader("X-Secure-Session-ID")
-	if ssid == "" {
-		ssid = c.Query("X-Secure-Session-ID")
-	}
-	if ssid == "" {
-		c.JSON(http.StatusOK, gin.H{
-			"status": false,
-		})
-		return
-	}
-
-	if user.VerifySecureSessionID(ssid, u.(*model.Auth).ID) {
-		c.JSON(http.StatusOK, gin.H{
-			"status": true,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"status": false,
-	})
+    u, ok := c.Get("user")
+    if !ok || !u.(*model.Auth).EnabledOTP() {
+        c.JSON(http.StatusOK, gin.H{
+            "status": false,
+        })
+        return
+    }
+    ssid := c.GetHeader("X-Secure-Session-ID")
+    if ssid == "" {
+        ssid = c.Query("X-Secure-Session-ID")
+    }
+    if ssid == "" {
+        c.JSON(http.StatusOK, gin.H{
+            "status": false,
+        })
+        return
+    }
+
+    if user.VerifySecureSessionID(ssid, u.(*model.Auth).ID) {
+        c.JSON(http.StatusOK, gin.H{
+            "status": true,
+        })
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "status": false,
+    })
 }
 
 func StartSecure2FASession(c *gin.Context) {
-	var json struct {
-		OTP          string `json:"otp"`
-		RecoveryCode string `json:"recovery_code"`
-	}
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-	u := api.CurrentUser(c)
-	if !u.EnabledOTP() {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"message": "User not configured with 2FA",
-		})
-		return
-	}
-
-	if json.OTP == "" && json.RecoveryCode == "" {
-		c.JSON(http.StatusBadRequest, LoginResponse{
-			Message: "The user has enabled 2FA",
-		})
-		return
-	}
-
-	if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
-		c.JSON(http.StatusBadRequest, LoginResponse{
-			Message: "Invalid 2FA or recovery code",
-		})
-		return
-	}
-
-	sessionId := user.SetSecureSessionID(u.ID)
-
-	c.JSON(http.StatusOK, gin.H{
-		"session_id": sessionId,
-	})
+    var json struct {
+        OTP          string `json:"otp"`
+        RecoveryCode string `json:"recovery_code"`
+    }
+    if !api.BindAndValid(c, &json) {
+        return
+    }
+    u := api.CurrentUser(c)
+    if !u.EnabledOTP() {
+        c.JSON(http.StatusBadRequest, gin.H{
+            "message": "User not configured with 2FA",
+        })
+        return
+    }
+
+    if json.OTP == "" && json.RecoveryCode == "" {
+        c.JSON(http.StatusBadRequest, LoginResponse{
+            Message: "The user has enabled 2FA",
+        })
+        return
+    }
+
+    if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
+        c.JSON(http.StatusBadRequest, LoginResponse{
+            Message: "Invalid 2FA or recovery code",
+        })
+        return
+    }
+
+    sessionId := user.SetSecureSessionID(u.ID)
+
+    c.JSON(http.StatusOK, gin.H{
+        "session_id": sessionId,
+    })
 }

+ 26 - 18
app/src/language/en/app.po

@@ -13,7 +13,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr ""
 
@@ -259,6 +259,10 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -452,11 +456,11 @@ msgstr ""
 msgid "Credentials"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 
@@ -753,12 +757,12 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 #, fuzzy
 msgid "Enable 2FA"
 msgstr "Enabled"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "Enabled successfully"
@@ -976,7 +980,7 @@ msgid ""
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -1013,12 +1017,12 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1631,15 +1635,15 @@ msgid "Recovered Successfully"
 msgstr "Saved successfully"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr ""
 
@@ -1779,7 +1783,7 @@ msgstr ""
 msgid "Reset"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 msgid "Reset 2FA"
 msgstr ""
 
@@ -1832,7 +1836,7 @@ msgstr "Saved successfully"
 msgid "Saved successfully"
 msgstr "Saved successfully"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -1840,6 +1844,10 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr ""
@@ -1862,8 +1870,8 @@ msgstr "Send"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -2112,7 +2120,7 @@ msgstr "Certificate Status"
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
@@ -2185,7 +2193,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2207,7 +2215,7 @@ msgstr ""
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 26 - 18
app/src/language/es/app.po

@@ -20,7 +20,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr "Configuración de 2FA"
 
@@ -249,6 +249,10 @@ msgstr "Dir CA"
 msgid "CADir"
 msgstr "Directorio CA"
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -428,12 +432,12 @@ msgstr "Credencial"
 msgid "Credentials"
 msgstr "Credenciales"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr ""
 "La cuenta actual tiene habilitada la autenticación de dos factores (2FA)."
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 "La cuenta actual no tiene habilitada la autenticación de dos factores (2FA)."
@@ -716,11 +720,11 @@ msgstr "Falló el habilitado de %{conf_name} en %{node_name}"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Habilitado exitoso de %{conf_name} en %{node_name}"
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 msgid "Enable 2FA"
 msgstr "Habilitar 2FA"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 msgid "Enable 2FA successfully"
 msgstr "Habilitar 2FA exitoso"
 
@@ -932,7 +936,7 @@ msgstr ""
 "el máximo de intentos en los minutos del umbral de prohibición, la IP será "
 "bloqueada por un período de tiempo."
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -972,12 +976,12 @@ msgstr "Error de actualización de kernel inicial"
 msgid "Initialing core upgrader"
 msgstr "Inicializando la actualización del kernel"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr "Ingrese el código de la aplicación:"
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr "Ingrese el código de recuperación:"
 
@@ -1575,15 +1579,15 @@ msgid "Recovered Successfully"
 msgstr "Recuperado con éxito"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr "Recuperación"
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr "Código de Recuperación"
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr "Código de Recuperación:"
 
@@ -1710,7 +1714,7 @@ msgstr "Pedido con parámetros incorrectos"
 msgid "Reset"
 msgstr "Limpiar"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 msgid "Reset 2FA"
 msgstr "Restablecer 2FA"
 
@@ -1761,7 +1765,7 @@ msgstr "Guardado con éxito"
 msgid "Saved successfully"
 msgstr "Guardado con éxito"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 "Escanee el código QR con su teléfono móvil para agregar la cuenta a la "
@@ -1771,6 +1775,10 @@ msgstr ""
 msgid "SDK"
 msgstr "SDK"
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "Seleccionador"
@@ -1793,8 +1801,8 @@ msgstr "Enviado"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -2037,7 +2045,7 @@ msgstr "La ruta existe, pero el archivo no es un certificado"
 msgid "The path exists, but the file is not a private key"
 msgstr "La ruta existe, pero el archivo no es una clave privada"
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
@@ -2118,7 +2126,7 @@ msgstr "Consejos"
 msgid "Title"
 msgstr "Título"
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2148,7 +2156,7 @@ msgstr ""
 "Demasiados intentos fallidos de inicio de sesión, por favor intente "
 "nuevamente más tarde"
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 26 - 18
app/src/language/fr_FR/app.po

@@ -15,7 +15,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr ""
 
@@ -261,6 +261,10 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -452,11 +456,11 @@ msgstr "Identifiant"
 msgid "Credentials"
 msgstr "Identifiants"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 
@@ -753,12 +757,12 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 #, fuzzy
 msgid "Enable 2FA"
 msgstr "Activé"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "Activé avec succès"
@@ -977,7 +981,7 @@ msgid ""
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -1016,12 +1020,12 @@ msgstr "Erreur du programme de mise à niveau initial du core"
 msgid "Initialing core upgrader"
 msgstr "Initialisation du programme de mise à niveau du core"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1637,15 +1641,15 @@ msgid "Recovered Successfully"
 msgstr "Enregistré avec succès"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr ""
 
@@ -1786,7 +1790,7 @@ msgstr ""
 msgid "Reset"
 msgstr "Réinitialiser"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "Réinitialiser"
@@ -1838,7 +1842,7 @@ msgstr "Sauvegarde réussie"
 msgid "Saved successfully"
 msgstr "Enregistré avec succès"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -1846,6 +1850,10 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "Sélecteur"
@@ -1868,8 +1876,8 @@ msgstr "Envoyer"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -2118,7 +2126,7 @@ msgstr "Chemin de la clé du certificat SSL"
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
@@ -2195,7 +2203,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2221,7 +2229,7 @@ msgstr ""
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 26 - 18
app/src/language/ko_KR/app.po

@@ -18,7 +18,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr "2FA 설정"
 
@@ -247,6 +247,10 @@ msgstr "CA 디렉토리"
 msgid "CADir"
 msgstr "CA 디렉토리"
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -431,11 +435,11 @@ msgstr "인증 정보"
 msgid "Credentials"
 msgstr "인증 정보들"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 
@@ -716,12 +720,12 @@ msgstr "%{node_name}에서 %{conf_name} 활성화 실패"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "%{node_name}에서 %{conf_name} 성공적으로 활성화됨"
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 #, fuzzy
 msgid "Enable 2FA"
 msgstr "활성화"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "성공적으로 활성화"
@@ -938,7 +942,7 @@ msgid ""
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -976,12 +980,12 @@ msgstr "초기 코어 업그레이더 오류"
 msgid "Initialing core upgrader"
 msgstr "코어 업그레이더 초기화"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1601,15 +1605,15 @@ msgid "Recovered Successfully"
 msgstr "성공적으로 제거됨"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr ""
 
@@ -1751,7 +1755,7 @@ msgstr "잘못된 매개변수로 요청됨"
 msgid "Reset"
 msgstr "재설정"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "재설정"
@@ -1805,7 +1809,7 @@ msgstr "성공적으로 저장됨"
 msgid "Saved successfully"
 msgstr "성공적으로 저장됨"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -1813,6 +1817,10 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "선택"
@@ -1835,8 +1843,8 @@ msgstr "보내기"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -2084,7 +2092,7 @@ msgstr "Certificate Status"
 msgid "The path exists, but the file is not a private key"
 msgstr "경로는 존재하지만 파일은 개인 키가 아닙니다"
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
@@ -2159,7 +2167,7 @@ msgstr "팁"
 msgid "Title"
 msgstr "제목"
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2184,7 +2192,7 @@ msgstr "토큰이 유효하지 않습니다"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 26 - 18
app/src/language/messages.pot

@@ -6,7 +6,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr ""
 
@@ -247,6 +247,10 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -428,11 +432,11 @@ msgstr ""
 msgid "Credentials"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 
@@ -718,11 +722,11 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 msgid "Enable 2FA"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 msgid "Enable 2FA successfully"
 msgstr ""
 
@@ -937,7 +941,7 @@ msgstr ""
 msgid "If the number of login failed attempts from a ip reach the max attempts in ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid "If you lose your mobile phone, you can use the recovery code to reset your 2FA."
 msgstr ""
 
@@ -971,12 +975,12 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1556,15 +1560,15 @@ msgid "Recovered Successfully"
 msgstr ""
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr ""
 
@@ -1685,7 +1689,7 @@ msgstr ""
 msgid "Reset"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 msgid "Reset 2FA"
 msgstr ""
 
@@ -1740,7 +1744,7 @@ msgstr ""
 msgid "Saved successfully"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -1748,6 +1752,10 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr ""
@@ -1772,8 +1780,8 @@ msgstr ""
 #: src/views/environment/Environment.vue:15
 #: src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83
 #: src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81
@@ -1993,7 +2001,7 @@ msgstr ""
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 
@@ -2054,7 +2062,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid "To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone."
 msgstr ""
 
@@ -2070,7 +2078,7 @@ msgstr ""
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid "TOTP is a two-factor authentication method that uses a time-based one-time password algorithm."
 msgstr ""
 

+ 26 - 18
app/src/language/ru_RU/app.po

@@ -18,7 +18,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2ФА"
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr "Настройки 2ФА"
 
@@ -250,6 +250,10 @@ msgstr "Директория корневого сертификата"
 msgid "CADir"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -429,11 +433,11 @@ msgstr "Учетные данные"
 msgid "Credentials"
 msgstr "Учетные данные"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr "Текущая учетная запись имеет включенную 2ФА."
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 "Текущая учетная запись не имеет включенной двухфакторной аутентификации."
@@ -716,11 +720,11 @@ msgstr "Включение %{conf_name} in %{node_name} нипалучилася
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Включение %{conf_name} in %{node_name} успешно"
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 msgid "Enable 2FA"
 msgstr "Включить 2ФА"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 msgid "Enable 2FA successfully"
 msgstr "Двухфакторная аутентификация успешно включена"
 
@@ -931,7 +935,7 @@ msgstr ""
 "количества попыток в течение пороговых минут блокировки, IP будет "
 "заблокирован на определенный период времени."
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -971,12 +975,12 @@ msgstr "Ошибка первоначального обновления ядр
 msgid "Initialing core upgrader"
 msgstr "Инициализация программы обновления ядра"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr "Введите код из приложения:"
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr "Введите код восстановления:"
 
@@ -1573,15 +1577,15 @@ msgid "Recovered Successfully"
 msgstr "Восстановлено успешно"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr "Восстановление"
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr "Код восстановления"
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr "Код восстановления:"
 
@@ -1709,7 +1713,7 @@ msgstr "Запрос с неправильными параметрами"
 msgid "Reset"
 msgstr "Сброс"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 msgid "Reset 2FA"
 msgstr "Сброс 2FA"
 
@@ -1760,7 +1764,7 @@ msgstr "Сохранено успешно"
 msgid "Saved successfully"
 msgstr "Успешно сохранено"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 "Отсканируйте QR-код с помощью мобильного телефона, чтобы добавить учетную "
@@ -1770,6 +1774,10 @@ msgstr ""
 msgid "SDK"
 msgstr "SDK"
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "Выбор"
@@ -1792,8 +1800,8 @@ msgstr "Отправлено"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -2034,7 +2042,7 @@ msgstr "Путь существует, но файл не является се
 msgid "The path exists, but the file is not a private key"
 msgstr "Путь существует, но файл не является приватным ключом"
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
@@ -2115,7 +2123,7 @@ msgstr "Советы"
 msgid "Title"
 msgstr "Заголовок"
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2143,7 +2151,7 @@ msgstr "Токен недействителен"
 msgid "Too many login failed attempts, please try again later"
 msgstr "Слишком много неудачных попыток входа, попробуйте позже"
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 26 - 18
app/src/language/vi_VN/app.po

@@ -13,7 +13,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr ""
 
@@ -261,6 +261,10 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -454,11 +458,11 @@ msgstr "Chứng chỉ"
 msgid "Credentials"
 msgstr "Chứng chỉ"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr ""
 
@@ -758,12 +762,12 @@ msgstr "Không thể bật %{conf_name} trên %{node_name}"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Đã bật %{conf_name} trên %{node_name}"
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 #, fuzzy
 msgid "Enable 2FA"
 msgstr "Đã bật"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "Đã bật"
@@ -983,7 +987,7 @@ msgid ""
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -1022,12 +1026,12 @@ msgstr "Không thể khởi tạo trình nâng cấp"
 msgid "Initialing core upgrader"
 msgstr "Đang khởi tạo trình nâng cấp"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1644,15 +1648,15 @@ msgid "Recovered Successfully"
 msgstr "Xoá thành công"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr ""
 
@@ -1794,7 +1798,7 @@ msgstr "Yêu cầu có chứa tham số sai"
 msgid "Reset"
 msgstr "Đặt lại"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "Đặt lại"
@@ -1848,7 +1852,7 @@ msgstr "Lưu thành công"
 msgid "Saved successfully"
 msgstr "Lưu thành công"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -1856,6 +1860,10 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "Bộ chọn"
@@ -1878,8 +1886,8 @@ msgstr "Gửi"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -2122,7 +2130,7 @@ msgstr ""
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
@@ -2194,7 +2202,7 @@ msgstr ""
 msgid "Title"
 msgstr "Tiêu đề"
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2220,7 +2228,7 @@ msgstr ""
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

BIN
app/src/language/zh_CN/app.mo


+ 26 - 18
app/src/language/zh_CN/app.po

@@ -17,7 +17,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr "2FA 设置"
 
@@ -246,6 +246,10 @@ msgstr "CA Dir"
 msgid "CADir"
 msgstr "CADir"
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr "无法扫描?使用文本密钥绑定"
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -423,11 +427,11 @@ msgstr "DNS 凭证"
 msgid "Credentials"
 msgstr "凭证"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr "当前账户已启用二步验证。"
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr "当前用户未启用二步验证。"
 
@@ -704,11 +708,11 @@ msgstr "在%{node_name}中启用%{conf_name}失败"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功启用%{node_name}中的%{conf_name}"
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 msgid "Enable 2FA"
 msgstr "启用二步验证"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 msgid "Enable 2FA successfully"
 msgstr "二步验证启用成功"
 
@@ -918,7 +922,7 @@ msgstr ""
 "如果某个 IP 的登录失败次数达到禁用阈值分钟内的最大尝试次数,该 IP 将被禁止登"
 "录一段时间。"
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -954,12 +958,12 @@ msgstr "初始化核心升级程序错误"
 msgid "Initialing core upgrader"
 msgstr "初始化核心升级器"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr "输入应用程序中的代码:"
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr "输入恢复代码:"
 
@@ -1545,15 +1549,15 @@ msgid "Recovered Successfully"
 msgstr "恢复成功"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr "恢复"
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr "恢复代码"
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr "恢复代码:"
 
@@ -1680,7 +1684,7 @@ msgstr "请求参数错误"
 msgid "Reset"
 msgstr "重置"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 msgid "Reset 2FA"
 msgstr "重置二步验证"
 
@@ -1731,7 +1735,7 @@ msgstr "保存成功"
 msgid "Saved successfully"
 msgstr "保存成功"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 
@@ -1739,6 +1743,10 @@ msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 msgid "SDK"
 msgstr "SDK"
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr "密钥已复制"
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "选择器"
@@ -1761,8 +1769,8 @@ msgstr "上传"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -1992,7 +2000,7 @@ msgstr "路径存在,但文件不是证书"
 msgid "The path exists, but the file is not a private key"
 msgstr "路径存在,但文件不是私钥"
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr "恢复密码只会显示一次,请妥善保存。"
@@ -2064,7 +2072,7 @@ msgstr "提示"
 msgid "Title"
 msgstr "标题"
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2089,7 +2097,7 @@ msgstr "Token 无效"
 msgid "Too many login failed attempts, please try again later"
 msgstr "登录失败次数过多,请稍后再试"
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 26 - 18
app/src/language/zh_TW/app.po

@@ -21,7 +21,7 @@ msgstr ""
 msgid "2FA"
 msgstr "多重要素驗證"
 
-#: src/views/preference/components/TOTP.vue:88
+#: src/views/preference/components/TOTP.vue:90
 msgid "2FA Settings"
 msgstr "多重要素驗證設定"
 
@@ -250,6 +250,10 @@ msgstr "CA Dir"
 msgid "CADir"
 msgstr "CADir"
 
+#: src/views/preference/components/TOTP.vue:150
+msgid "Can't scan? Use text key binding"
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:252
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:55
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:253
@@ -427,11 +431,11 @@ msgstr "認證"
 msgid "Credentials"
 msgstr "認證資訊"
 
-#: src/views/preference/components/TOTP.vue:96
+#: src/views/preference/components/TOTP.vue:98
 msgid "Current account is enabled 2FA."
 msgstr "當前帳戶已啟用多因素身份驗證。"
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:95
 msgid "Current account is not enabled 2FA."
 msgstr "當前帳戶未啟用多因素身份驗證。"
 
@@ -708,11 +712,11 @@ msgstr "在 %{node_name} 啟用 %{conf_name} 失敗"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功在 %{node_name} 啟用 %{conf_name}"
 
-#: src/views/preference/components/TOTP.vue:122
+#: src/views/preference/components/TOTP.vue:124
 msgid "Enable 2FA"
 msgstr "啟用多因素身份驗證"
 
-#: src/views/preference/components/TOTP.vue:52
+#: src/views/preference/components/TOTP.vue:54
 msgid "Enable 2FA successfully"
 msgstr "啟用多因素身份驗證成功"
 
@@ -922,7 +926,7 @@ msgstr ""
 "如果來自某個 IP 的登錄失敗次數在禁止閾值分鐘內達到最大嘗試次數,該 IP 將被禁"
 "止一段時間。"
 
-#: src/views/preference/components/TOTP.vue:108
+#: src/views/preference/components/TOTP.vue:110
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
@@ -958,12 +962,12 @@ msgstr "初始化核心升級程式錯誤"
 msgid "Initialing core upgrader"
 msgstr "正在初始化核心升級程式"
 
-#: src/views/preference/components/TOTP.vue:144
+#: src/views/preference/components/TOTP.vue:157
 msgid "Input the code from the app:"
 msgstr "請輸入應用程式中的代碼:"
 
 #: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:170
 msgid "Input the recovery code:"
 msgstr "輸入恢復碼:"
 
@@ -1548,15 +1552,15 @@ msgid "Recovered Successfully"
 msgstr "恢復成功"
 
 #: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:164
+#: src/views/preference/components/TOTP.vue:177
 msgid "Recovery"
 msgstr "恢復"
 
-#: src/views/preference/components/TOTP.vue:101
+#: src/views/preference/components/TOTP.vue:103
 msgid "Recovery Code"
 msgstr "恢復碼"
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:112
 msgid "Recovery Code:"
 msgstr "恢復碼:"
 
@@ -1683,7 +1687,7 @@ msgstr "請求參數錯誤"
 msgid "Reset"
 msgstr "重設"
 
-#: src/views/preference/components/TOTP.vue:130
+#: src/views/preference/components/TOTP.vue:132
 msgid "Reset 2FA"
 msgstr "重置多重因素驗證"
 
@@ -1734,7 +1738,7 @@ msgstr "儲存成功"
 msgid "Saved successfully"
 msgstr "儲存成功"
 
-#: src/views/preference/components/TOTP.vue:91
+#: src/views/preference/components/TOTP.vue:93
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "用手機掃描二維碼將賬戶添加到應用程序中。"
 
@@ -1742,6 +1746,10 @@ msgstr "用手機掃描二維碼將賬戶添加到應用程序中。"
 msgid "SDK"
 msgstr "SDK"
 
+#: src/views/preference/components/TOTP.vue:149
+msgid "Secret has been copied"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:155
 msgid "Selector"
 msgstr "選擇器"
@@ -1764,8 +1772,8 @@ msgstr "傳送"
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:42
-#: src/views/preference/components/TOTP.vue:55
+#: src/views/preference/components/TOTP.vue:44
+#: src/views/preference/components/TOTP.vue:57
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
@@ -1995,7 +2003,7 @@ msgstr "路徑存在,但檔案不是憑證"
 msgid "The path exists, but the file is not a private key"
 msgstr "路徑存在,但檔案不是金鑰"
 
-#: src/views/preference/components/TOTP.vue:109
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr "恢復碼僅顯示一次,請將其保存在安全的地方。"
@@ -2067,7 +2075,7 @@ msgstr "提示"
 msgid "Title"
 msgstr "標題"
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2092,7 +2100,7 @@ msgstr "令牌無效"
 msgid "Too many login failed attempts, please try again later"
 msgstr "登錄失敗次數過多,請稍後再試"
 
-#: src/views/preference/components/TOTP.vue:89
+#: src/views/preference/components/TOTP.vue:91
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."

+ 15 - 2
app/src/views/preference/components/TOTP.vue

@@ -1,8 +1,10 @@
 <script setup lang="ts">
 import { message } from 'ant-design-vue'
 import { CheckCircleOutlined } from '@ant-design/icons-vue'
+import { UseClipboard } from '@vueuse/components'
 import otp from '@/api/otp'
 import OTPInput from '@/components/OTPInput/OTPInput.vue'
+import { $gettext } from '@/gettext'
 
 const status = ref(false)
 const enrolling = ref(false)
@@ -131,13 +133,24 @@ function reset2FA() {
     </AButton>
 
     <template v-if="enrolling">
-      <div class="w-64 h-64 mt-4 mb-2">
+      <div class="mt-4 mb-2">
         <img
           v-if="qrCode"
-          class="w-full"
+          class="w-64 h-64"
           :src="qrCode"
           alt="qr code"
         >
+        <div class="w-64 flex justify-center">
+          <UseClipboard v-slot="{ copy, copied }">
+            <a
+              class="mr-2"
+              @click="() => copy(secret)"
+            >
+              {{ copied ? $gettext('Secret has been copied')
+                : $gettext('Can\'t scan? Use text key binding') }}
+            </a>
+          </UseClipboard>
+        </div>
       </div>
 
       <div>