Browse Source

Merge pull request #859 from 0xJacky/refactor/otp

refactor: new recovery codes and OTP
Jacky 5 months ago
parent
commit
46d30743b6

+ 11 - 6
api/user/2fa.go

@@ -2,6 +2,10 @@ package user
 
 import (
 	"encoding/base64"
+	"net/http"
+	"strings"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/cache"
 	"github.com/0xJacky/Nginx-UI/internal/passkey"
@@ -12,15 +16,14 @@ import (
 	"github.com/go-webauthn/webauthn/webauthn"
 	"github.com/google/uuid"
 	"github.com/uozi-tech/cosy"
-	"net/http"
-	"strings"
-	"time"
 )
 
 type Status2FA struct {
-	Enabled       bool `json:"enabled"`
-	OTPStatus     bool `json:"otp_status"`
-	PasskeyStatus bool `json:"passkey_status"`
+	Enabled                bool `json:"enabled"`
+	OTPStatus              bool `json:"otp_status"`
+	PasskeyStatus          bool `json:"passkey_status"`
+	RecoveryCodesGenerated bool `json:"recovery_codes_generated"`
+	RecoveryCodesViewed    bool `json:"recovery_codes_viewed"`
 }
 
 func get2FAStatus(c *gin.Context) (status Status2FA) {
@@ -31,6 +34,8 @@ func get2FAStatus(c *gin.Context) (status Status2FA) {
 		status.OTPStatus = userPtr.EnabledOTP()
 		status.PasskeyStatus = userPtr.EnabledPasskey() && passkey.Enabled()
 		status.Enabled = status.OTPStatus || status.PasskeyStatus
+		status.RecoveryCodesGenerated = userPtr.RecoveryCodeGenerated()
+		status.RecoveryCodesViewed = userPtr.RecoveryCodeViewed()
 	}
 	return
 }

+ 23 - 55
api/user/otp.go

@@ -1,22 +1,20 @@
 package user
 
 import (
-	"bytes"
-	"crypto/sha1"
-	"encoding/base64"
-	"encoding/hex"
 	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/crypto"
+	"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"
 	"github.com/uozi-tech/cosy"
-	"image/jpeg"
-	"net/http"
-	"strings"
 )
 
 func GenerateTOTP(c *gin.Context) {
@@ -38,27 +36,9 @@ func GenerateTOTP(c *gin.Context) {
 		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,
+		"secret": otpKey.Secret(),
+		"url":    otpKey.URL(),
 	})
 }
 
@@ -78,22 +58,22 @@ func EnrollTOTP(c *gin.Context) {
 		return
 	}
 
-	var json struct {
+	var twoFA struct {
 		Secret   string `json:"secret" binding:"required"`
 		Passcode string `json:"passcode" binding:"required"`
 	}
-	if !cosy.BindAndValid(c, &json) {
+	if !cosy.BindAndValid(c, &twoFA) {
 		return
 	}
 
-	if ok := totp.Validate(json.Passcode, json.Secret); !ok {
+	if ok := totp.Validate(twoFA.Passcode, twoFA.Secret); !ok {
 		c.JSON(http.StatusNotAcceptable, gin.H{
 			"message": "Invalid passcode",
 		})
 		return
 	}
 
-	ciphertext, err := crypto.AesEncrypt([]byte(json.Secret))
+	ciphertext, err := crypto.AesEncrypt([]byte(twoFA.Secret))
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
@@ -106,37 +86,25 @@ func EnrollTOTP(c *gin.Context) {
 		return
 	}
 
-	recoveryCode := sha1.Sum(ciphertext)
+	t := time.Now().Unix()
+	recoveryCodes := model.RecoveryCodes{Codes: generateRecoveryCodes(16), LastViewed: &t}
+	cUser.RecoveryCodes = recoveryCodes
+	_, err = u.Where(u.ID.Eq(cUser.ID)).Updates(cUser)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
 
-	c.JSON(http.StatusOK, gin.H{
-		"message":       "ok",
-		"recovery_code": hex.EncodeToString(recoveryCode[:]),
+	c.JSON(http.StatusOK, RecoveryCodesResponse{
+		Message:       "ok",
+		RecoveryCodes: recoveryCodes,
 	})
 }
 
 func ResetOTP(c *gin.Context) {
-	var json struct {
-		RecoveryCode string `json:"recovery_code"`
-	}
-	if !cosy.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.User
-	_, err = u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null())
+	_, err := u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null(), u.RecoveryCodes.Null())
 	if err != nil {
 		api.ErrHandler(c, err)
 		return

+ 72 - 0
api/user/recovery.go

@@ -0,0 +1,72 @@
+package user
+
+import (
+	"fmt"
+	"math/rand"
+	"net/http"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+)
+
+type RecoveryCodesResponse struct {
+	Message string `json:"message"`
+	model.RecoveryCodes
+}
+
+func generateRecoveryCode() string {
+	// generate recovery code, 10 hex numbers with a dash in the middle
+	return fmt.Sprintf("%05x-%05x", rand.Intn(0x100000), rand.Intn(0x100000))
+}
+
+func generateRecoveryCodes(count int) []*model.RecoveryCode {
+	recoveryCodes := make([]*model.RecoveryCode, count)
+	for i := 0; i < count; i++ {
+		recoveryCodes[i] = &model.RecoveryCode{
+			Code: generateRecoveryCode(),
+		}
+	}
+	return recoveryCodes
+}
+
+func ViewRecoveryCodes(c *gin.Context) {
+	user := api.CurrentUser(c)
+
+	// update last viewed time
+	u := query.User
+	t := time.Now().Unix()
+	user.RecoveryCodes.LastViewed = &t
+	_, err := u.Where(u.ID.Eq(user.ID)).Updates(user)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, RecoveryCodesResponse{
+		Message:       "ok",
+		RecoveryCodes: user.RecoveryCodes,
+	})
+}
+
+func GenerateRecoveryCodes(c *gin.Context) {
+	user := api.CurrentUser(c)
+
+	t := time.Now().Unix()
+	recoveryCodes := model.RecoveryCodes{Codes: generateRecoveryCodes(16), LastViewed: &t}
+	user.RecoveryCodes = recoveryCodes
+
+	u := query.User
+	_, err := u.Where(u.ID.Eq(user.ID)).Updates(user)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, RecoveryCodesResponse{
+		Message:       "ok",
+		RecoveryCodes: recoveryCodes,
+	})
+}

+ 8 - 1
api/user/router.go

@@ -27,7 +27,6 @@ func InitUserRouter(r *gin.RouterGroup) {
 
 	r.GET("/otp_secret", GenerateTOTP)
 	r.POST("/otp_enroll", EnrollTOTP)
-	r.POST("/otp_reset", ResetOTP)
 
 	r.GET("/begin_passkey_register", BeginPasskeyRegistration)
 	r.POST("/finish_passkey_register", FinishPasskeyRegistration)
@@ -35,4 +34,12 @@ func InitUserRouter(r *gin.RouterGroup) {
 	r.GET("/passkeys", GetPasskeyList)
 	r.POST("/passkeys/:id", UpdatePasskey)
 	r.DELETE("/passkeys/:id", DeletePasskey)
+
+	o := r.Group("", middleware.RequireSecureSession())
+	{
+		o.GET("/otp_reset", ResetOTP)
+
+		o.GET("/recovery_codes", ViewRecoveryCodes)
+		o.GET("/recovery_codes_generate", GenerateRecoveryCodes)
+	}
 }

+ 1 - 0
app/components.d.ts

@@ -49,6 +49,7 @@ declare module 'vue' {
     APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
     APopover: typeof import('ant-design-vue/es')['Popover']
     AProgress: typeof import('ant-design-vue/es')['Progress']
+    AQrcode: typeof import('ant-design-vue/es')['QRCode']
     ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
     ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
     AResult: typeof import('ant-design-vue/es')['Result']

+ 4 - 2
app/src/api/2fa.ts

@@ -1,14 +1,16 @@
 import type { AuthenticationResponseJSON } from '@simplewebauthn/browser'
 import http from '@/lib/http'
 
-export interface TwoFAStatusResponse {
+export interface TwoFAStatus {
   enabled: boolean
   otp_status: boolean
   passkey_status: boolean
+  recovery_codes_generated: boolean
+  recovery_codes_viewed?: boolean
 }
 
 const twoFA = {
-  status(): Promise<TwoFAStatusResponse> {
+  status(): Promise<TwoFAStatus> {
     return http.get('/2fa_status')
   },
   start_secure_session_by_otp(passcode: string, recovery_code: string): Promise<{ session_id: string }> {

+ 5 - 4
app/src/api/otp.ts

@@ -1,19 +1,20 @@
+import type { RecoveryCodesResponse } from '@/api/recovery'
 import http from '@/lib/http'
 
 export interface OTPGenerateSecretResponse {
   secret: string
-  qr_code: string
+  url: string
 }
 
 const otp = {
   generate_secret(): Promise<OTPGenerateSecretResponse> {
     return http.get('/otp_secret')
   },
-  enroll_otp(secret: string, passcode: string): Promise<{ recovery_code: string }> {
+  enroll_otp(secret: string, passcode: string): Promise<RecoveryCodesResponse> {
     return http.post('/otp_enroll', { secret, passcode })
   },
-  reset(recovery_code: string) {
-    return http.post('/otp_reset', { recovery_code })
+  reset() {
+    return http.get('/otp_reset')
   },
 }
 

+ 27 - 0
app/src/api/recovery.ts

@@ -0,0 +1,27 @@
+import http from '@/lib/http'
+
+export interface RecoveryCode {
+  code: string
+  used_time?: number
+}
+
+export interface RecoveryCodes {
+  codes: RecoveryCode[]
+  last_viewed?: number
+  last_downloaded?: number
+}
+
+export interface RecoveryCodesResponse extends RecoveryCodes {
+  message: string
+}
+
+const recovery = {
+  generate(): Promise<RecoveryCodesResponse> {
+    return http.get('/recovery_codes_generate')
+  },
+  view(): Promise<RecoveryCodesResponse> {
+    return http.get('/recovery_codes')
+  },
+}
+
+export default recovery

+ 37 - 38
app/src/components/TwoFA/Authorization.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import type { TwoFAStatusResponse } from '@/api/2fa'
+import type { TwoFAStatus } from '@/api/2fa'
 import twoFA from '@/api/2fa'
 import OTPInput from '@/components/OTPInput/OTPInput.vue'
 import { useUserStore } from '@/pinia'
@@ -7,7 +7,7 @@ import { KeyOutlined } from '@ant-design/icons-vue'
 import { startAuthentication } from '@simplewebauthn/browser'
 
 defineProps<{
-  twoFAStatus: TwoFAStatusResponse
+  twoFAStatus: TwoFAStatus
 }>()
 
 const emit = defineEmits(['submitOTP', 'submitSecureSessionID'])
@@ -65,42 +65,30 @@ onMounted(() => {
 
 <template>
   <div>
-    <div v-if="twoFAStatus.otp_status">
-      <div v-if="!useRecoveryCode">
-        <p>{{ $gettext('Please enter the OTP code:') }}</p>
-        <OTPInput
-          ref="refOTP"
-          v-model="passcode"
-          class="justify-center mb-6"
-          @on-complete="onSubmit"
-        />
-      </div>
-      <div
-        v-else
-        class="mt-2 mb-4"
-      >
-        <p>{{ $gettext('Input the recovery code:') }}</p>
-        <AInputGroup compact>
-          <AInput v-model:value="recoveryCode" />
-          <AButton
-            type="primary"
-            @click="onSubmit"
-          >
-            {{ $gettext('Recovery') }}
-          </AButton>
-        </AInputGroup>
-      </div>
-
-      <div class="flex justify-center">
-        <a
-          v-if="!useRecoveryCode"
-          @click="clickUseRecoveryCode"
-        >{{ $gettext('Use recovery code') }}</a>
-        <a
-          v-else
-          @click="clickUseOTP"
-        >{{ $gettext('Use OTP') }}</a>
-      </div>
+    <div
+      v-if="useRecoveryCode"
+      class="mt-2 mb-4"
+    >
+      <p>{{ $gettext('Input the recovery code:') }}</p>
+      <AInputGroup compact>
+        <AInput v-model:value="recoveryCode" placeholder="xxxxx-xxxxx" />
+        <AButton
+          type="primary"
+          @click="onSubmit"
+        >
+          {{ $gettext('Recovery') }}
+        </AButton>
+      </AInputGroup>
+    </div>
+
+    <div v-if="twoFAStatus.otp_status && !useRecoveryCode">
+      <p>{{ $gettext('Please enter the OTP code:') }}</p>
+      <OTPInput
+        ref="refOTP"
+        v-model="passcode"
+        class="justify-center mb-6"
+        @on-complete="onSubmit"
+      />
     </div>
 
     <div
@@ -121,6 +109,17 @@ onMounted(() => {
         {{ $gettext('Authenticate with a passkey') }}
       </AButton>
     </div>
+
+    <div v-if="twoFAStatus.otp_status || twoFAStatus.recovery_codes_generated" class="flex justify-center mt-3">
+      <a
+        v-if="!useRecoveryCode"
+        @click="clickUseRecoveryCode"
+      >{{ $gettext('Use recovery code') }}</a>
+      <a
+        v-else-if="twoFAStatus.otp_status"
+        @click="clickUseOTP"
+      >{{ $gettext('Use OTP') }}</a>
+    </div>
   </div>
 </template>
 

+ 198 - 89
app/src/language/ar/app.po

@@ -17,7 +17,7 @@ msgstr ""
 msgid "2FA"
 msgstr "المصادقة الثنائية"
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr "إعدادات المصادقة الثنائية"
 
@@ -40,7 +40,7 @@ msgstr "مستخدم ACME"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -106,7 +106,11 @@ msgstr "بعد ذلك، قم بتحديث هذه الصفحة وانقر فوق
 msgid "All"
 msgstr "الكل"
 
-#: src/views/preference/OpenAISettings.vue:44
+#: src/language/constants.ts:57
+msgid "All Recovery Codes Have Been Used"
+msgstr ""
+
+#: src/views/preference/OpenAISettings.vue:32
 msgid "API Base Url"
 msgstr "عنوان URL الأساسي لAPI"
 
@@ -114,15 +118,15 @@ msgstr "عنوان URL الأساسي لAPI"
 msgid "API Document"
 msgstr "ملف API"
 
-#: src/views/preference/OpenAISettings.vue:57
+#: src/views/preference/OpenAISettings.vue:46
 msgid "API Proxy"
 msgstr "وسيط API"
 
-#: src/views/preference/OpenAISettings.vue:69
+#: src/views/preference/OpenAISettings.vue:58
 msgid "API Token"
 msgstr "رمز API"
 
-#: src/views/preference/OpenAISettings.vue:78
+#: src/views/preference/OpenAISettings.vue:67
 #, fuzzy
 msgid "API Type"
 msgstr "رمز API"
@@ -140,7 +144,7 @@ msgstr "تم التكرار بنجاح"
 msgid "Arch"
 msgstr "بنية"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "هل أنت متأكد من حذف عنوان IP المحظور هذا على الفور؟"
 
@@ -148,6 +152,16 @@ msgstr "هل أنت متأكد من حذف عنوان IP المحظور هذا 
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "هل أنت متأكد من حذف مفتاح المرور هذا على الفور؟"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "هل أنت متأكد أنك تريد استرداد هذا العنصر؟"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "هل أنت متأكد أنك تريد الحذف؟"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -204,7 +218,7 @@ msgstr "المساعد"
 msgid "Attempt to fix"
 msgstr "محاولات"
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr "محاولات"
 
@@ -212,11 +226,11 @@ msgstr "محاولات"
 msgid "Auth"
 msgstr "مصادقة"
 
-#: src/components/TwoFA/Authorization.vue:121
+#: src/components/TwoFA/Authorization.vue:109
 msgid "Authenticate with a passkey"
 msgstr "المصادقة باستخدام مفتاح المرور"
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr "إعدادات المصادقة"
 
@@ -253,15 +267,15 @@ msgstr "العودة إلى الصفحة الرئيسية"
 msgid "Back to list"
 msgstr "العودة إلى القائمة"
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "دقائق حد الحظر"
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr "عناوين IP المحظورة"
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr "محظور حتى"
 
@@ -315,10 +329,6 @@ msgstr "مجلد سلطة التصديق"
 msgid "CADir"
 msgstr "مجلد سلطة التصديق"
 
-#: src/views/preference/components/TOTP.vue:129
-msgid "Can't scan? Use text key binding"
-msgstr "لا يمكن المسح؟ استخدم ربط مفتاح النص"
-
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -461,6 +471,10 @@ msgstr "مسح"
 msgid "Cleared successfully"
 msgstr "تم المسح بنجاح"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr "أمر"
@@ -508,6 +522,7 @@ msgstr "محتوى"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr "تم النسخ"
 
@@ -515,6 +530,11 @@ msgstr "تم النسخ"
 msgid "Copy"
 msgstr "نسخ"
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+#, fuzzy
+msgid "Copy Codes"
+msgstr "رمز الاسترداد"
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "ترقية نواة"
@@ -565,11 +585,11 @@ msgstr "بيان الاعتماد"
 msgid "Credentials"
 msgstr "بيانات الاعتماد"
 
-#: src/views/preference/components/TOTP.vue:77
+#: src/views/preference/components/TOTP.vue:72
 msgid "Current account is enabled TOTP."
 msgstr "TOTP مفعل للحساب الحالي."
 
-#: src/views/preference/components/TOTP.vue:74
+#: src/views/preference/components/TOTP.vue:70
 msgid "Current account is not enabled TOTP."
 msgstr "TOTP معطل للحساب الحالي."
 
@@ -911,7 +931,7 @@ msgstr "فشل تفعيل %{conf_name} في %{node_name}"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "تم تفعيل %{conf_name} في %{node_name} بنجاح"
 
-#: src/views/preference/components/TOTP.vue:38
+#: src/views/preference/components/TOTP.vue:45
 msgid "Enable 2FA successfully"
 msgstr "تم تفعيل المصادقة الثنائية بنجاح"
 
@@ -947,7 +967,7 @@ msgstr "تم التفعيل بنجاح"
 msgid "Enable TLS"
 msgstr "تفعيل TLS"
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:81
 msgid "Enable TOTP"
 msgstr "تفعيل TOTP"
 
@@ -1043,8 +1063,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "فشل في التفعيل %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1071,6 +1092,10 @@ msgstr "تصفيه"
 msgid "Finished"
 msgstr "انتهى"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1106,6 +1131,22 @@ msgstr "شهادة عامة"
 msgid "Generate"
 msgstr "توليد"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate New Recovery Codes"
+msgstr "رمز الاسترداد"
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "رمز الاسترداد"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "تم الاسترداد بنجاح"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "توليد مفتاح خاص لتسجيل الحساب"
@@ -1154,7 +1195,7 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr "إذا تُرك فارغًا، سيتم استخدام دليل CA الافتراضي."
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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."
@@ -1162,14 +1203,6 @@ msgstr ""
 "إذا وصل عدد محاولات تسجيل الدخول الفاشلة من عنوان IP إلى الحد الأقصى "
 "للمحاولات في حد دقائق الحظر، سيتم حظر عنوان IP لفترة من الوقت."
 
-#: src/views/preference/components/TOTP.vue:89
-msgid ""
-"If you lose your mobile phone, you can use the recovery code to reset your "
-"2FA."
-msgstr ""
-"إذا فقدت هاتفك المحمول، يمكنك استخدام رمز الاسترداد لإعادة تعيين المصادقة "
-"الثنائية."
-
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr "إذا كان متصفحك يدعم WebAuthn Passkey، ستظهر نافذة حوار."
@@ -1203,12 +1236,11 @@ msgstr "خطأ في ترقية النواة الأولية"
 msgid "Initialing core upgrader"
 msgstr "بدء ترقية النواة"
 
-#: src/views/preference/components/TOTP.vue:136
+#: src/views/preference/components/TOTP.vue:119
 msgid "Input the code from the app:"
 msgstr "أدخل الرمز من التطبيق:"
 
-#: src/components/TwoFA/Authorization.vue:82
-#: src/views/preference/components/TOTP.vue:149
+#: src/components/TwoFA/Authorization.vue:72
 msgid "Input the recovery code:"
 msgstr "أدخل رمز الاسترداد:"
 
@@ -1256,7 +1288,7 @@ msgstr "رمز 2FA أو الاسترداد غير صالح"
 msgid "Invalid request format"
 msgstr "رمز 2FA أو الاسترداد غير صالح"
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr "IP"
 
@@ -1280,6 +1312,12 @@ msgstr "المُصدر: %{issuer}"
 msgid "Jwt Secret"
 msgstr "سر JWT"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1297,7 +1335,7 @@ msgstr "آخر استخدام في"
 msgid "Leave blank for no change"
 msgstr "اتركه فارغًا لعدم التغيير"
 
-#: src/views/preference/OpenAISettings.vue:53
+#: src/views/preference/OpenAISettings.vue:41
 msgid "Leave blank for the default: https://api.openai.com/"
 msgstr "اتركه فارغًا للإعداد الافتراضي: /https://api.openai.com"
 
@@ -1360,7 +1398,7 @@ msgstr "أماكن"
 msgid "Log"
 msgstr "سجل"
 
-#: src/routes/index.ts:318 src/views/other/Login.vue:222
+#: src/routes/index.ts:318 src/views/other/Login.vue:223
 msgid "Login"
 msgstr "تسجيل الدخول"
 
@@ -1421,7 +1459,7 @@ msgstr "إدارة المستخدمين"
 msgid "Managed Certificate"
 msgstr "شهادة مُدارة"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr "الحد الأقصى للمحاولات"
 
@@ -1438,7 +1476,7 @@ msgstr "الذاكرة والتخزين"
 msgid "Minutes"
 msgstr "دقائق"
 
-#: src/views/preference/OpenAISettings.vue:32
+#: src/views/preference/OpenAISettings.vue:20
 msgid "Model"
 msgstr "نموذج"
 
@@ -1607,7 +1645,7 @@ msgstr "تم إعادة تشغيل Nginx بنجاح"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1722,10 +1760,14 @@ msgstr "متصل"
 msgid "OpenAI"
 msgstr "أوبن أي آي"
 
-#: src/components/TwoFA/Authorization.vue:112 src/views/other/Login.vue:231
+#: src/components/TwoFA/Authorization.vue:100 src/views/other/Login.vue:232
 msgid "Or"
 msgstr "أو"
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr "الاسم الأصلي"
@@ -1825,7 +1867,7 @@ msgid ""
 msgstr ""
 "يرجى إدخال اسم لمفتاح المرور الذي ترغب في إنشائه ثم انقر على زر موافق أدناه."
 
-#: src/components/TwoFA/Authorization.vue:70
+#: src/components/TwoFA/Authorization.vue:85
 msgid "Please enter the OTP code:"
 msgstr "يرجى إدخال رمز OTP:"
 
@@ -1847,6 +1889,12 @@ msgstr ""
 "يرجى أولاً إضافة بيانات الاعتماد في الشهادات > بيانات اعتماد DNS، ثم اختيار "
 "أحد بيانات الاعتماد أدناه لطلب API لمزود DNS."
 
+#: src/language/constants.ts:58
+msgid ""
+"Please generate new recovery codes in the preferences immediately to prevent "
+"lockout."
+msgstr ""
+
 #: src/views/config/components/Rename.vue:63
 #: src/views/config/ConfigEditor.vue:249
 msgid "Please input a filename"
@@ -1964,18 +2012,20 @@ msgstr "استعادة"
 msgid "Recovered Successfully"
 msgstr "تم الاسترداد بنجاح"
 
-#: src/components/TwoFA/Authorization.vue:89
-#: src/views/preference/components/TOTP.vue:156
+#: src/components/TwoFA/Authorization.vue:79
 msgid "Recovery"
 msgstr "استرداد"
 
-#: src/views/preference/components/TOTP.vue:82
-msgid "Recovery Code"
+#: src/views/preference/components/RecoveryCodes.vue:68
+#, fuzzy
+msgid "Recovery Codes"
 msgstr "رمز الاسترداد"
 
-#: src/views/preference/components/TOTP.vue:91
-msgid "Recovery Code:"
-msgstr "رمز الاسترداد:"
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
 msgid "Recursive Nameservers"
@@ -2040,7 +2090,7 @@ msgstr "إعادة التحميل"
 msgid "Reloading nginx"
 msgstr "إعادة تحميل nginx"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr "إزالة"
 
@@ -2052,7 +2102,7 @@ msgstr "خطأ في إزالة الموقع %{site} من %{node}، الاستج
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "تمت إزالة الموقع %{site} من %{node} بنجاح"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 msgid "Remove successfully"
 msgstr "إزالة بنجاح"
@@ -2073,8 +2123,8 @@ msgstr "إعادة تسمية"
 msgid ""
 "Rename %{orig_path} to %{new_path} on %{env_name} failed, response: %{resp}"
 msgstr ""
-"فشل إعادة تسمية %{orig_path} إلى %{new_path} على %{env_name}، الاستجابة: %"
-"{resp}"
+"فشل إعادة تسمية %{orig_path} إلى %{new_path} على %{env_name}، الاستجابة: "
+"%{resp}"
 
 #: src/components/Notification/config.ts:20
 msgid "Rename %{orig_path} to %{new_path} on %{env_name} successfully"
@@ -2099,8 +2149,8 @@ msgstr "تم إعادة تسمية الموقع البعيد بنجاح"
 #: src/components/Notification/config.ts:95
 msgid "Rename Site %{site} to %{new_site} on %{node} error, response: %{resp}"
 msgstr ""
-"خطأ في إعادة تسمية الموقع %{site} إلى %{new_site} على %{node}، الاستجابة: %"
-"{resp}"
+"خطأ في إعادة تسمية الموقع %{site} إلى %{new_site} على %{node}، الاستجابة: "
+"%{resp}"
 
 #: src/components/Notification/config.ts:87
 msgid "Rename Site %{site} to %{new_site} on %{node} successfully"
@@ -2145,7 +2195,7 @@ msgstr "تم الطلب باستخدام عوامل خاطئة"
 msgid "Reset"
 msgstr "إعادة تعيين"
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:93
 msgid "Reset 2FA"
 msgstr "إعادة تعيين التحقق بخطوتين"
 
@@ -2157,15 +2207,15 @@ msgstr "إعادة تشغيل"
 msgid "Restarting"
 msgstr "إعادة التشغيل"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr "اسم العرض RP"
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr "أصول RP"
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr "معرّف الجهاز عن بُعد"
 
@@ -2232,7 +2282,7 @@ msgstr "تم الحفظ بنجاح"
 msgid "Saved successfully"
 msgstr "تم الحفظ بنجاح"
 
-#: src/views/preference/components/TOTP.vue:72
+#: src/views/preference/components/TOTP.vue:69
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "امسح رمز الاستجابة السريعة بهاتفك المحمول لإضافة الحساب إلى التطبيق."
 
@@ -2240,7 +2290,7 @@ msgstr "امسح رمز الاستجابة السريعة بهاتفك المح
 msgid "SDK"
 msgstr "حزمة تطوير البرمجيات SDK"
 
-#: src/views/preference/components/TOTP.vue:128
+#: src/views/preference/components/TOTP.vue:109
 msgid "Secret has been copied"
 msgstr "تم نسخ السر"
 
@@ -2274,8 +2324,9 @@ msgid "ServerIdx out of range"
 msgstr ""
 
 #: src/constants/errors/user.ts:11
+#, fuzzy
 msgid "Session not found"
-msgstr ""
+msgstr "لم يتم العثور على الملف"
 
 #: src/views/preference/CertSettings.vue:33
 msgid ""
@@ -2299,21 +2350,21 @@ msgstr "تعيين موفر تحدي HTTP01"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Show"
 msgstr "عرض"
 
-#: src/views/other/Login.vue:240
+#: src/views/other/Login.vue:241
 msgid "Sign in with a passkey"
 msgstr "تسجيل الدخول باستخدام مفتاح المرور"
 
@@ -2349,12 +2400,14 @@ msgid "Sites List"
 msgstr "قائمة المواقع"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "مجلد"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "مجلد"
 
 #: src/views/certificate/CertificateEditor.vue:211
 msgid "SSL Certificate Content"
@@ -2404,8 +2457,9 @@ msgid "Streams Directory"
 msgstr "مجلد"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "مجلد"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2562,7 +2616,7 @@ msgid ""
 "The log path is not under the paths in settings.NginxSettings.LogDirWhiteList"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:23
 #, fuzzy
 msgid ""
 "The model name should only contain letters, unicode, numbers, hyphens, "
@@ -2595,11 +2649,6 @@ msgstr ""
 "يجب أن يحتوي اسم العقدة على حروف وأحرف يونيكود وأرقام وشرطات وعلامات وصل "
 "ونقاط فقط."
 
-#: src/views/preference/components/TOTP.vue:90
-msgid ""
-"The recovery code is only displayed once, please save it in a safe place."
-msgstr "رمز الاسترداد يُعرض مرة واحدة فقط، يرجى حفظه في مكان آمن."
-
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2622,8 +2671,8 @@ msgstr ""
 msgid "The url is invalid"
 msgstr "عنوان URL غير صالح"
 
-#: src/views/preference/OpenAISettings.vue:47
-#: src/views/preference/OpenAISettings.vue:60
+#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:49
 msgid "The url is invalid."
 msgstr "عنوان URL غير صالح."
 
@@ -2631,6 +2680,13 @@ msgstr "عنوان URL غير صالح."
 msgid "The username or password is incorrect"
 msgstr "اسم المستخدم أو كلمة المرور غير صحيحة"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr "هذا العنصر في الشهادة التلقائية غير صالح، يرجى إزالته."
@@ -2672,11 +2728,11 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr "سيتم ترقية أو إعادة تثبيت Nginx UI على %{nodeNames} إلى %{version}."
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr "كبح"
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2686,7 +2742,7 @@ msgstr "نصائح"
 msgid "Title"
 msgstr "عنوان"
 
-#: src/views/preference/components/TOTP.vue:71
+#: src/views/preference/components/TOTP.vue:68
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2714,7 +2770,7 @@ msgstr ""
 "توجيه الطلب من السلطة إلى الخلفية، ونحتاج إلى حفظ هذا الملف وإعادة تحميل "
 "Nginx. هل أنت متأكد أنك تريد المتابعة؟"
 
-#: src/views/preference/OpenAISettings.vue:48
+#: src/views/preference/OpenAISettings.vue:36
 #, fuzzy
 msgid ""
 "To use a local large model, deploy it with ollama, vllm or lmdeploy. They "
@@ -2725,7 +2781,7 @@ msgstr ""
 "نهاية API متوافقة مع OpenAI، لذا قم فقط بتعيين baseUrl إلىAPI المحلية الخاصة "
 "بك."
 
-#: src/views/preference/OpenAISettings.vue:72
+#: src/views/preference/OpenAISettings.vue:61
 msgid "Token is not valid"
 msgstr "الرمز غير صالح"
 
@@ -2739,11 +2795,11 @@ msgstr[3] ""
 msgstr[4] ""
 msgstr[5] ""
 
-#: src/views/preference/components/TOTP.vue:69
+#: src/views/preference/components/TOTP.vue:66
 msgid "TOTP"
 msgstr "كلمة مرور لمرة واحدة تعتمد على الوقت"
 
-#: src/views/preference/components/TOTP.vue:70
+#: src/views/preference/components/TOTP.vue:67
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2813,11 +2869,11 @@ msgstr "مدة التشغيل:"
 msgid "URL"
 msgstr "عنوان URL"
 
-#: src/components/TwoFA/Authorization.vue:102
+#: src/components/TwoFA/Authorization.vue:121
 msgid "Use OTP"
 msgstr "استخدم كلمة المرور لمرة واحدة"
 
-#: src/components/TwoFA/Authorization.vue:98
+#: src/components/TwoFA/Authorization.vue:117
 msgid "Use recovery code"
 msgstr "استخدم رمز الاسترداد"
 
@@ -2869,6 +2925,16 @@ msgstr "عرض التفاصيل"
 msgid "View Mode"
 msgstr "وضع العرض"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "رمز الاسترداد"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "عرض"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2890,7 +2956,7 @@ msgstr ""
 "سنقوم بإزالة تكوين HTTPChallenge من هذا الملف وإعادة تحميل Nginx. هل أنت "
 "متأكد أنك تريد المتابعة؟"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr "ويب أوثن"
 
@@ -2916,6 +2982,12 @@ msgstr ""
 "عند تفعيل/تعطيل، حذف، أو حفظ هذا الموقع، سيتم مزامنة العقد المحددة في فئة "
 "الموقع والعقد المحددة أدناه."
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -2929,7 +3001,7 @@ msgstr "كتابة مفتاح الشهادة الخاص إلى القرص"
 msgid "Writing certificate to disk"
 msgstr "كتابة الشهادة إلى القرص"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -2950,10 +3022,47 @@ msgid ""
 "passkey."
 msgstr "لم تقم بتكوين إعدادات Webauthn، لذا لا يمكنك إضافة مفتاح مرور."
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr "مفاتيح المرور الخاصة بك"
 
+#~ msgid ""
+#~ "If you lose your mobile phone, you can use the recovery code to reset "
+#~ "your 2FA."
+#~ msgstr ""
+#~ "إذا فقدت هاتفك المحمول، يمكنك استخدام رمز الاسترداد لإعادة تعيين المصادقة "
+#~ "الثنائية."
+
+#~ msgid "Recovery Code:"
+#~ msgstr "رمز الاسترداد:"
+
+#~ msgid ""
+#~ "The recovery code is only displayed once, please save it in a safe place."
+#~ msgstr "رمز الاسترداد يُعرض مرة واحدة فقط، يرجى حفظه في مكان آمن."
+
+#~ msgid "Can't scan? Use text key binding"
+#~ msgstr "لا يمكن المسح؟ استخدم ربط مفتاح النص"
+
 #~ msgid "Directory"
 #~ msgstr "مجلد"
 

+ 3 - 0
app/src/language/constants.ts

@@ -53,4 +53,7 @@ export const msg = [
   $gettext('Enable Remote Site Error'),
   $gettext('Rename Remote Site Success'),
   $gettext('Rename Remote Site Error'),
+
+  $gettext('All Recovery Codes Have Been Used'),
+  $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.'),
 ]

+ 228 - 109
app/src/language/en/app.po

@@ -13,7 +13,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr ""
 
@@ -22,8 +22,9 @@ msgid "About"
 msgstr "About"
 
 #: src/routes/index.ts:210 src/views/site/ngx_conf/LogEntry.vue:75
+#, fuzzy
 msgid "Access Logs"
-msgstr ""
+msgstr "Sites List"
 
 #: src/routes/index.ts:148 src/views/certificate/ACMEUser.vue:113
 #: src/views/certificate/ACMEUserSelector.vue:85
@@ -37,7 +38,7 @@ msgstr "Username"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -107,23 +108,28 @@ msgstr ""
 msgid "All"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:44
+#: src/language/constants.ts:57
+msgid "All Recovery Codes Have Been Used"
+msgstr ""
+
+#: src/views/preference/OpenAISettings.vue:32
 msgid "API Base Url"
 msgstr ""
 
 #: src/views/certificate/DNSChallenge.vue:83
+#, fuzzy
 msgid "API Document"
-msgstr ""
+msgstr "Comments"
 
-#: src/views/preference/OpenAISettings.vue:57
+#: src/views/preference/OpenAISettings.vue:46
 msgid "API Proxy"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:69
+#: src/views/preference/OpenAISettings.vue:58
 msgid "API Token"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:78
+#: src/views/preference/OpenAISettings.vue:67
 msgid "API Type"
 msgstr ""
 
@@ -140,7 +146,7 @@ msgstr "Saved successfully"
 msgid "Arch"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 #, fuzzy
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Are you sure you want to remove this directive?"
@@ -150,6 +156,16 @@ msgstr "Are you sure you want to remove this directive?"
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "Are you sure you want to remove this directive?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "Are you sure you want to remove this directive?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "Are you sure you want to remove this directive?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -213,7 +229,7 @@ msgstr ""
 msgid "Attempt to fix"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr ""
 
@@ -221,11 +237,11 @@ msgstr ""
 msgid "Auth"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:121
+#: src/components/TwoFA/Authorization.vue:109
 msgid "Authenticate with a passkey"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr ""
 
@@ -263,15 +279,15 @@ msgstr "Back"
 msgid "Back to list"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr ""
 
@@ -327,10 +343,6 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:129
-msgid "Can't scan? Use text key binding"
-msgstr ""
-
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -471,6 +483,10 @@ msgstr ""
 msgid "Cleared successfully"
 msgstr "Disabled successfully"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 #, fuzzy
 msgid "Command"
@@ -520,6 +536,7 @@ msgstr "Content"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr ""
 
@@ -527,6 +544,10 @@ msgstr ""
 msgid "Copy"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+msgid "Copy Codes"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr ""
@@ -581,11 +602,11 @@ msgstr ""
 msgid "Credentials"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:77
+#: src/views/preference/components/TOTP.vue:72
 msgid "Current account is enabled TOTP."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:74
+#: src/views/preference/components/TOTP.vue:70
 msgid "Current account is not enabled TOTP."
 msgstr ""
 
@@ -666,12 +687,14 @@ msgid "Deploy"
 msgstr ""
 
 #: src/views/stream/components/Deploy.vue:57
+#, fuzzy
 msgid "Deploy %{conf_name} to %{node_name} failed"
-msgstr ""
+msgstr "Saved successfully"
 
 #: src/views/stream/components/Deploy.vue:36
+#, fuzzy
 msgid "Deploy %{conf_name} to %{node_name} successfully"
-msgstr ""
+msgstr "Saved successfully"
 
 #: src/views/stream/components/Deploy.vue:34
 #, fuzzy
@@ -790,8 +813,9 @@ msgstr[0] "Are you sure you want to remove this directive?"
 msgstr[1] "Are you sure you want to remove this directive?"
 
 #: src/views/site/cert/components/ObtainCert.vue:136
+#, fuzzy
 msgid "Do you want to disable auto-cert renewal?"
-msgstr ""
+msgstr "Are you sure you want to remove this directive?"
 
 #: src/views/site/site_edit/RightSettings.vue:51
 #, fuzzy
@@ -870,8 +894,9 @@ msgstr ""
 #: src/views/site/site_list/SiteList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:121
 #: src/views/stream/StreamList.vue:160
+#, fuzzy
 msgid "Duplicate"
-msgstr ""
+msgstr "Enable failed"
 
 #: src/views/stream/components/StreamDuplicate.vue:82
 #, fuzzy
@@ -934,14 +959,16 @@ msgid "Enable"
 msgstr "Enabled"
 
 #: src/views/stream/components/Deploy.vue:47
+#, fuzzy
 msgid "Enable %{conf_name} in %{node_name} failed"
-msgstr ""
+msgstr "Saved successfully"
 
 #: src/views/stream/components/Deploy.vue:43
+#, fuzzy
 msgid "Enable %{conf_name} in %{node_name} successfully"
-msgstr ""
+msgstr "Saved successfully"
 
-#: src/views/preference/components/TOTP.vue:38
+#: src/views/preference/components/TOTP.vue:45
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "Enabled successfully"
@@ -983,7 +1010,7 @@ msgstr "Enabled successfully"
 msgid "Enable TLS"
 msgstr "Enable TLS"
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:81
 #, fuzzy
 msgid "Enable TOTP"
 msgstr "Enable TLS"
@@ -1075,16 +1102,18 @@ msgid "Failed to enable %{msg}"
 msgstr "Failed to enable %{msg}"
 
 #: src/language/constants.ts:5
+#, fuzzy
 msgid "Failed to get certificate information"
-msgstr ""
+msgstr "Certificate is valid"
 
 #: src/constants/errors/self_check.ts:4
 msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "Failed to enable %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1111,6 +1140,10 @@ msgstr ""
 msgid "Finished"
 msgstr "Finished"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1149,6 +1182,21 @@ msgstr "Certificate is valid"
 msgid "Generate"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate New Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "Invalid E-mail!"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "Saved successfully"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr ""
@@ -1198,18 +1246,12 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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:89
-msgid ""
-"If you lose your mobile phone, you can use the recovery code to reset your "
-"2FA."
-msgstr ""
-
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1242,12 +1284,11 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:136
+#: src/views/preference/components/TOTP.vue:119
 msgid "Input the code from the app:"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:82
-#: src/views/preference/components/TOTP.vue:149
+#: src/components/TwoFA/Authorization.vue:72
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1276,8 +1317,9 @@ msgid "Invalid filename"
 msgstr "Invalid E-mail!"
 
 #: src/views/config/components/Mkdir.vue:57
+#, fuzzy
 msgid "Invalid folder name"
-msgstr ""
+msgstr "Invalid E-mail!"
 
 #: src/constants/errors/user.ts:4
 #, fuzzy
@@ -1289,14 +1331,16 @@ msgid "Invalid passcode or recovery code"
 msgstr ""
 
 #: src/constants/errors/user.ts:5
+#, fuzzy
 msgid "Invalid recovery code"
-msgstr ""
+msgstr "Invalid E-mail!"
 
 #: src/constants/errors/middleware.ts:2
+#, fuzzy
 msgid "Invalid request format"
-msgstr ""
+msgstr "Invalid E-mail!"
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr ""
 
@@ -1323,14 +1367,21 @@ msgstr ""
 msgid "Jwt Secret"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
 msgstr ""
 
 #: src/views/system/Upgrade.vue:179
+#, fuzzy
 msgid "Last checked at"
-msgstr ""
+msgstr "Created at"
 
 #: src/views/preference/components/Passkey.vue:96
 #, fuzzy
@@ -1341,7 +1392,7 @@ msgstr "Created at"
 msgid "Leave blank for no change"
 msgstr "Leave blank for no change"
 
-#: src/views/preference/OpenAISettings.vue:53
+#: src/views/preference/OpenAISettings.vue:41
 msgid "Leave blank for the default: https://api.openai.com/"
 msgstr ""
 
@@ -1412,7 +1463,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Login"
 
-#: src/routes/index.ts:318 src/views/other/Login.vue:222
+#: src/routes/index.ts:318 src/views/other/Login.vue:223
 msgid "Login"
 msgstr "Login"
 
@@ -1470,7 +1521,7 @@ msgstr "Manage Users"
 msgid "Managed Certificate"
 msgstr "Certificate is valid"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr ""
 
@@ -1487,7 +1538,7 @@ msgstr "Memory and Storage"
 msgid "Minutes"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:32
+#: src/views/preference/OpenAISettings.vue:20
 #, fuzzy
 msgid "Model"
 msgstr "Advance Mode"
@@ -1634,8 +1685,9 @@ msgid "Nginx Log"
 msgstr ""
 
 #: src/views/preference/NginxSettings.vue:18
+#, fuzzy
 msgid "Nginx Log Directory Whitelist"
-msgstr ""
+msgstr "Configuration Name"
 
 #: src/views/preference/NginxSettings.vue:27
 msgid "Nginx PID Path"
@@ -1667,7 +1719,7 @@ msgstr "Saved successfully"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1682,8 +1734,9 @@ msgid "Node name"
 msgstr "Username"
 
 #: src/views/preference/BasicSettings.vue:23
+#, fuzzy
 msgid "Node Secret"
-msgstr ""
+msgstr "Username"
 
 #: src/views/certificate/CertificateList/certColumns.tsx:91
 msgid "Not After"
@@ -1724,8 +1777,9 @@ msgid "Obtain certificate"
 msgstr "Certificate is valid"
 
 #: src/language/constants.ts:15
+#, fuzzy
 msgid "Obtaining certificate"
-msgstr ""
+msgstr "Certificate is valid"
 
 #: src/views/site/cert/components/AutoCertStepOne.vue:95
 msgid "OCSP Must Staple"
@@ -1782,10 +1836,14 @@ msgstr ""
 msgid "OpenAI"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:112 src/views/other/Login.vue:231
+#: src/components/TwoFA/Authorization.vue:100 src/views/other/Login.vue:232
 msgid "Or"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr ""
@@ -1864,8 +1922,9 @@ msgid "Perform"
 msgstr ""
 
 #: src/language/constants.ts:28
+#, fuzzy
 msgid "Perform core upgrade error"
-msgstr ""
+msgstr "Certificate has expired"
 
 #: src/language/constants.ts:27
 msgid "Performing core upgrade"
@@ -1881,7 +1940,7 @@ msgid ""
 "button below."
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:70
+#: src/components/TwoFA/Authorization.vue:85
 msgid "Please enter the OTP code:"
 msgstr ""
 
@@ -1901,6 +1960,12 @@ msgid ""
 "select one of the credentialsbelow to request the API of the DNS provider."
 msgstr ""
 
+#: src/language/constants.ts:58
+msgid ""
+"Please generate new recovery codes in the preferences immediately to prevent "
+"lockout."
+msgstr ""
+
 #: src/views/config/components/Rename.vue:63
 #: src/views/config/ConfigEditor.vue:249
 #, fuzzy
@@ -2023,17 +2088,19 @@ msgstr ""
 msgid "Recovered Successfully"
 msgstr "Saved successfully"
 
-#: src/components/TwoFA/Authorization.vue:89
-#: src/views/preference/components/TOTP.vue:156
+#: src/components/TwoFA/Authorization.vue:79
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:82
-msgid "Recovery Code"
-msgstr ""
+#: src/views/preference/components/RecoveryCodes.vue:68
+#, fuzzy
+msgid "Recovery Codes"
+msgstr "Invalid E-mail!"
 
-#: src/views/preference/components/TOTP.vue:91
-msgid "Recovery Code:"
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
 msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
@@ -2045,8 +2112,9 @@ msgid "Regenerate response"
 msgstr ""
 
 #: src/views/certificate/ACMEUser.vue:137
+#, fuzzy
 msgid "Register"
-msgstr ""
+msgstr "Enable failed"
 
 #: src/views/certificate/ACMEUser.vue:56
 msgid ""
@@ -2074,8 +2142,9 @@ msgid "Register successfully"
 msgstr "Enabled successfully"
 
 #: src/language/constants.ts:14
+#, fuzzy
 msgid "Registering user"
-msgstr ""
+msgstr "Enable failed"
 
 #: src/views/certificate/ACMEUser.vue:120
 msgid "Registration Status"
@@ -2103,7 +2172,7 @@ msgstr ""
 msgid "Reloading nginx"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr ""
 
@@ -2117,7 +2186,7 @@ msgstr "Saved successfully"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "Saved successfully"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 #, fuzzy
 msgid "Remove successfully"
@@ -2223,7 +2292,7 @@ msgstr ""
 msgid "Reset"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:93
 msgid "Reset 2FA"
 msgstr ""
 
@@ -2235,15 +2304,15 @@ msgstr ""
 msgid "Restarting"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2316,7 +2385,7 @@ msgstr "Saved successfully"
 msgid "Saved successfully"
 msgstr "Saved successfully"
 
-#: src/views/preference/components/TOTP.vue:72
+#: src/views/preference/components/TOTP.vue:69
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -2324,13 +2393,14 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:128
+#: src/views/preference/components/TOTP.vue:109
 msgid "Secret has been copied"
 msgstr ""
 
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:189
+#, fuzzy
 msgid "Selector"
-msgstr ""
+msgstr "Directive"
 
 #: src/routes/index.ts:283 src/views/system/SelfCheck/SelfCheck.vue:42
 msgid "Self Check"
@@ -2382,21 +2452,21 @@ msgstr ""
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Show"
 msgstr ""
 
-#: src/views/other/Login.vue:240
+#: src/views/other/Login.vue:241
 msgid "Sign in with a passkey"
 msgstr ""
 
@@ -2433,12 +2503,14 @@ msgid "Sites List"
 msgstr "Sites List"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "Directive"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "Directive"
 
 #: src/views/certificate/CertificateEditor.vue:211
 #, fuzzy
@@ -2494,8 +2566,9 @@ msgid "Streams Directory"
 msgstr "Directive"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "Directive"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2594,8 +2667,9 @@ msgid "Sync strategy"
 msgstr "Certificate is valid"
 
 #: src/views/certificate/CertificateEditor.vue:204
+#, fuzzy
 msgid "Sync to"
-msgstr ""
+msgstr "Certificate is valid"
 
 #: src/views/site/site_edit/RightSettings.vue:110
 msgid "Synchronization"
@@ -2647,8 +2721,9 @@ msgid ""
 msgstr ""
 
 #: src/views/certificate/CertificateEditor.vue:214
+#, fuzzy
 msgid "The input is not a SSL Certificate"
-msgstr ""
+msgstr "Certificate Status"
 
 #: src/views/certificate/CertificateEditor.vue:227
 #, fuzzy
@@ -2660,7 +2735,7 @@ msgid ""
 "The log path is not under the paths in settings.NginxSettings.LogDirWhiteList"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:23
 msgid ""
 "The model name should only contain letters, unicode, numbers, hyphens, "
 "dashes, colons, and dots."
@@ -2678,8 +2753,9 @@ msgid "The path exists, but the file is not a certificate"
 msgstr "Certificate Status"
 
 #: src/views/certificate/CertificateEditor.vue:194
+#, fuzzy
 msgid "The path exists, but the file is not a private key"
-msgstr ""
+msgstr "Certificate Status"
 
 #: src/views/preference/BasicSettings.vue:66
 msgid ""
@@ -2687,11 +2763,6 @@ msgid ""
 "hyphens, dashes, colons, and dots."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:90
-msgid ""
-"The recovery code is only displayed once, please save it in a safe place."
-msgstr ""
-
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2713,13 +2784,21 @@ msgstr ""
 msgid "The url is invalid"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:47
-#: src/views/preference/OpenAISettings.vue:60
+#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:49
 msgid "The url is invalid."
 msgstr ""
 
 #: src/language/constants.ts:2
+#, fuzzy
 msgid "The username or password is incorrect"
+msgstr "Password"
+
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
 msgstr ""
 
 #: src/views/certificate/CertificateEditor.vue:102
@@ -2759,15 +2838,16 @@ msgid "This value is already taken"
 msgstr ""
 
 #: src/views/environment/BatchUpgrader.vue:182
+#, fuzzy
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
-msgstr ""
+msgstr "Saved successfully"
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2777,7 +2857,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:71
+#: src/views/preference/components/TOTP.vue:68
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2798,14 +2878,14 @@ msgid ""
 "continue?"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:48
+#: src/views/preference/OpenAISettings.vue:36
 msgid ""
 "To use a local large model, deploy it with ollama, vllm or lmdeploy. They "
 "provide an OpenAI-compatible API endpoint, so just set the baseUrl to your "
 "local API."
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:72
+#: src/views/preference/OpenAISettings.vue:61
 msgid "Token is not valid"
 msgstr ""
 
@@ -2815,11 +2895,11 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/views/preference/components/TOTP.vue:69
+#: src/views/preference/components/TOTP.vue:66
 msgid "TOTP"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:70
+#: src/views/preference/components/TOTP.vue:67
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2891,11 +2971,11 @@ msgstr "Uptime:"
 msgid "URL"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:102
+#: src/components/TwoFA/Authorization.vue:121
 msgid "Use OTP"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:98
+#: src/components/TwoFA/Authorization.vue:117
 msgid "Use recovery code"
 msgstr ""
 
@@ -2924,8 +3004,9 @@ msgstr "Username (*)"
 #: src/views/certificate/ACMEUser.vue:124
 #: src/views/certificate/CertificateList/certColumns.tsx:81
 #: src/views/site/cert/CertInfo.vue:24
+#, fuzzy
 msgid "Valid"
-msgstr ""
+msgstr "Invalid E-mail!"
 
 #: src/views/environment/envColumns.tsx:31
 msgid "Version"
@@ -2933,8 +3014,9 @@ msgstr ""
 
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:488
 #: src/views/site/ngx_conf/config_template/ConfigTemplate.vue:103
+#, fuzzy
 msgid "View"
-msgstr ""
+msgstr "Basic Mode"
 
 #: src/components/Notification/Notification.vue:187
 #, fuzzy
@@ -2950,6 +3032,16 @@ msgstr ""
 msgid "View Mode"
 msgstr "Basic Mode"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "Invalid E-mail!"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "Basic Mode"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2969,7 +3061,7 @@ msgid ""
 "Nginx. Are you sure you want to continue?"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -2990,6 +3082,12 @@ msgid ""
 "site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -3000,10 +3098,11 @@ msgid "Writing certificate private key to disk"
 msgstr ""
 
 #: src/language/constants.ts:16
+#, fuzzy
 msgid "Writing certificate to disk"
-msgstr ""
+msgstr "Certificate is valid"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3024,6 +3123,26 @@ msgid ""
 "passkey."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr ""

+ 200 - 92
app/src/language/es/app.po

@@ -20,7 +20,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr "Configuración de 2FA"
 
@@ -43,7 +43,7 @@ msgstr "Usuario ACME"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -111,7 +111,11 @@ msgstr ""
 msgid "All"
 msgstr "Todo"
 
-#: src/views/preference/OpenAISettings.vue:44
+#: src/language/constants.ts:57
+msgid "All Recovery Codes Have Been Used"
+msgstr ""
+
+#: src/views/preference/OpenAISettings.vue:32
 msgid "API Base Url"
 msgstr "URL Base de la API"
 
@@ -119,15 +123,15 @@ msgstr "URL Base de la API"
 msgid "API Document"
 msgstr "Documento de la API"
 
-#: src/views/preference/OpenAISettings.vue:57
+#: src/views/preference/OpenAISettings.vue:46
 msgid "API Proxy"
 msgstr "Proxy de la API"
 
-#: src/views/preference/OpenAISettings.vue:69
+#: src/views/preference/OpenAISettings.vue:58
 msgid "API Token"
 msgstr "Token de la API"
 
-#: src/views/preference/OpenAISettings.vue:78
+#: src/views/preference/OpenAISettings.vue:67
 #, fuzzy
 msgid "API Type"
 msgstr "Token de la API"
@@ -145,7 +149,7 @@ msgstr "Duplicado con éxito"
 msgid "Arch"
 msgstr "Arquitectura"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "¿Está seguro de eliminar esta IP bloqueada inmediatamente?"
 
@@ -153,6 +157,16 @@ msgstr "¿Está seguro de eliminar esta IP bloqueada inmediatamente?"
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "¿Está seguro de eliminar esta llave de acceso inmediatamente?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "¿Está seguro de que quiere recuperar este elemento?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "¿Está seguro de que quiere borrar?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -209,7 +223,7 @@ msgstr "Asistente"
 msgid "Attempt to fix"
 msgstr "Intentos"
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr "Intentos"
 
@@ -217,11 +231,11 @@ msgstr "Intentos"
 msgid "Auth"
 msgstr "Autenticación"
 
-#: src/components/TwoFA/Authorization.vue:121
+#: src/components/TwoFA/Authorization.vue:109
 msgid "Authenticate with a passkey"
 msgstr "Autenticarse con una llave de acceso"
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr "Configuración de autenticación"
 
@@ -258,15 +272,15 @@ msgstr "Volver al Inicio"
 msgid "Back to list"
 msgstr "Volver a la lista"
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "Umbral de Prohibición en Minutos"
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr "IPs prohibidas"
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr "Bloqueado hasta"
 
@@ -322,10 +336,6 @@ msgstr "Dir CA"
 msgid "CADir"
 msgstr "Directorio CA"
 
-#: src/views/preference/components/TOTP.vue:129
-msgid "Can't scan? Use text key binding"
-msgstr "¿No puede escanear? Utilice la vinculación con una llave de texto"
-
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -460,6 +470,10 @@ msgstr "Borrar"
 msgid "Cleared successfully"
 msgstr "Limpiado exitoso"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr "Comando"
@@ -507,6 +521,7 @@ msgstr "Contenido"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr "Copiado"
 
@@ -514,6 +529,11 @@ msgstr "Copiado"
 msgid "Copy"
 msgstr "Copiar"
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+#, fuzzy
+msgid "Copy Codes"
+msgstr "Código de Recuperación"
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "Actualización del kernel"
@@ -564,11 +584,11 @@ msgstr "Credencial"
 msgid "Credentials"
 msgstr "Credenciales"
 
-#: src/views/preference/components/TOTP.vue:77
+#: src/views/preference/components/TOTP.vue:72
 msgid "Current account is enabled TOTP."
 msgstr "La cuenta actual tiene habilitada TOTP."
 
-#: src/views/preference/components/TOTP.vue:74
+#: src/views/preference/components/TOTP.vue:70
 msgid "Current account is not enabled TOTP."
 msgstr "La cuenta actual no tiene habilitada TOTP."
 
@@ -908,7 +928,7 @@ 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:38
+#: src/views/preference/components/TOTP.vue:45
 msgid "Enable 2FA successfully"
 msgstr "Habilitar 2FA exitoso"
 
@@ -948,7 +968,7 @@ msgstr "Habilitado con Éxito"
 msgid "Enable TLS"
 msgstr "Habilitar TLS"
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:81
 #, fuzzy
 msgid "Enable TOTP"
 msgstr "Habilitar TLS"
@@ -1045,8 +1065,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "Error al habilitar %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1074,6 +1095,10 @@ msgstr "Filtro"
 msgid "Finished"
 msgstr "Terminado"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1111,6 +1136,22 @@ msgstr "Certificado General"
 msgid "Generate"
 msgstr "Generar"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate New Recovery Codes"
+msgstr "Código de Recuperación"
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "Código de Recuperación"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "Recuperado con éxito"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "Generando clave privada para registrar cuenta"
@@ -1159,7 +1200,7 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr "Si se deja en blanco, se utilizará el directorio CA predeterminado."
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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."
@@ -1168,14 +1209,6 @@ 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:89
-msgid ""
-"If you lose your mobile phone, you can use the recovery code to reset your "
-"2FA."
-msgstr ""
-"Si pierde su teléfono móvil, puede usar el código de recuperación para "
-"restablecer su 2FA."
-
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1210,12 +1243,11 @@ 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:136
+#: src/views/preference/components/TOTP.vue:119
 msgid "Input the code from the app:"
 msgstr "Ingrese el código de la aplicación:"
 
-#: src/components/TwoFA/Authorization.vue:82
-#: src/views/preference/components/TOTP.vue:149
+#: src/components/TwoFA/Authorization.vue:72
 msgid "Input the recovery code:"
 msgstr "Ingrese el código de recuperación:"
 
@@ -1263,7 +1295,7 @@ msgstr "Código 2FA o de recuperación inválido"
 msgid "Invalid request format"
 msgstr "Código 2FA o de recuperación inválido"
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr "IP"
 
@@ -1287,6 +1319,12 @@ msgstr "Emisor: %{issuer}"
 msgid "Jwt Secret"
 msgstr "Secreto Jwt"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1305,7 +1343,7 @@ msgstr "Comprobado por última vez el"
 msgid "Leave blank for no change"
 msgstr "Para no modificar dejar en blanco"
 
-#: src/views/preference/OpenAISettings.vue:53
+#: src/views/preference/OpenAISettings.vue:41
 msgid "Leave blank for the default: https://api.openai.com/"
 msgstr "Dejar en blanco para el valor predeterminado: https://api.openai.com/"
 
@@ -1369,7 +1407,7 @@ msgstr "Ubicaciones"
 msgid "Log"
 msgstr "Registro"
 
-#: src/routes/index.ts:318 src/views/other/Login.vue:222
+#: src/routes/index.ts:318 src/views/other/Login.vue:223
 msgid "Login"
 msgstr "Acceso"
 
@@ -1431,7 +1469,7 @@ msgstr "Administrar usuarios"
 msgid "Managed Certificate"
 msgstr "Certificado Administrado"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr "Intentos máximos"
 
@@ -1448,7 +1486,7 @@ msgstr "Memoria y almacenamiento"
 msgid "Minutes"
 msgstr "Minutos"
 
-#: src/views/preference/OpenAISettings.vue:32
+#: src/views/preference/OpenAISettings.vue:20
 msgid "Model"
 msgstr "Modelo"
 
@@ -1620,7 +1658,7 @@ msgstr "Nginx reiniciado con éxito"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1737,10 +1775,14 @@ msgstr "En línea"
 msgid "OpenAI"
 msgstr "OpenAI"
 
-#: src/components/TwoFA/Authorization.vue:112 src/views/other/Login.vue:231
+#: src/components/TwoFA/Authorization.vue:100 src/views/other/Login.vue:232
 msgid "Or"
 msgstr "O"
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr "Nombre original"
@@ -1842,7 +1884,7 @@ msgstr ""
 "Ingrese un nombre para la llave de acceso que desea crear y a continuación "
 "haga clic en el botón Aceptar."
 
-#: src/components/TwoFA/Authorization.vue:70
+#: src/components/TwoFA/Authorization.vue:85
 #, fuzzy
 msgid "Please enter the OTP code:"
 msgstr "Por favor, ingrese el código 2FA:"
@@ -1868,6 +1910,12 @@ msgstr ""
 "luego seleccione una de las credenciales de aquí debajo para llamar a la API "
 "del proveedor de DNS."
 
+#: src/language/constants.ts:58
+msgid ""
+"Please generate new recovery codes in the preferences immediately to prevent "
+"lockout."
+msgstr ""
+
 #: src/views/config/components/Rename.vue:63
 #: src/views/config/ConfigEditor.vue:249
 msgid "Please input a filename"
@@ -1996,18 +2044,20 @@ msgstr "Recuperar"
 msgid "Recovered Successfully"
 msgstr "Recuperado con éxito"
 
-#: src/components/TwoFA/Authorization.vue:89
-#: src/views/preference/components/TOTP.vue:156
+#: src/components/TwoFA/Authorization.vue:79
 msgid "Recovery"
 msgstr "Recuperación"
 
-#: src/views/preference/components/TOTP.vue:82
-msgid "Recovery Code"
+#: src/views/preference/components/RecoveryCodes.vue:68
+#, fuzzy
+msgid "Recovery Codes"
 msgstr "Código de Recuperación"
 
-#: src/views/preference/components/TOTP.vue:91
-msgid "Recovery Code:"
-msgstr "Código de Recuperación:"
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
 msgid "Recursive Nameservers"
@@ -2076,7 +2126,7 @@ msgstr "Recargando"
 msgid "Reloading nginx"
 msgstr "Recargando Nginx"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr "Eliminar"
 
@@ -2090,7 +2140,7 @@ msgstr "Eliminar sitio: %{site_name}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "Duplicado con éxito de %{conf_name} a %{node_name}"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 msgid "Remove successfully"
 msgstr "Eliminado con éxito"
@@ -2185,7 +2235,7 @@ msgstr "Pedido con parámetros incorrectos"
 msgid "Reset"
 msgstr "Limpiar"
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:93
 msgid "Reset 2FA"
 msgstr "Restablecer 2FA"
 
@@ -2197,15 +2247,15 @@ msgstr "Reiniciar"
 msgid "Restarting"
 msgstr "Reiniciando"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr "Nombre RP"
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr "Orígenes RP"
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr "RPID"
 
@@ -2256,8 +2306,8 @@ msgstr "Renombrar Configuración Remota Exitosa"
 #, fuzzy
 msgid "Save site %{site} to %{node} error, response: %{resp}"
 msgstr ""
-"Sincronización del Certificado %{cert_name} a %{env_name} falló, respuesta: %"
-"{resp}"
+"Sincronización del Certificado %{cert_name} a %{env_name} falló, respuesta: "
+"%{resp}"
 
 #: src/components/Notification/config.ts:35
 #, fuzzy
@@ -2278,7 +2328,7 @@ msgstr "Guardado con éxito"
 msgid "Saved successfully"
 msgstr "Guardado con éxito"
 
-#: src/views/preference/components/TOTP.vue:72
+#: src/views/preference/components/TOTP.vue:69
 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 "
@@ -2288,7 +2338,7 @@ msgstr ""
 msgid "SDK"
 msgstr "SDK"
 
-#: src/views/preference/components/TOTP.vue:128
+#: src/views/preference/components/TOTP.vue:109
 msgid "Secret has been copied"
 msgstr "El secreto ha sido copiado"
 
@@ -2348,21 +2398,21 @@ msgstr "Usando el proveedor de desafíos HTTP01"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Show"
 msgstr "Mostrar"
 
-#: src/views/other/Login.vue:240
+#: src/views/other/Login.vue:241
 msgid "Sign in with a passkey"
 msgstr "Iniciar sesión con una llave de acceso"
 
@@ -2398,12 +2448,14 @@ msgid "Sites List"
 msgstr "Lista de sitios"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "Directorio"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "Directorio"
 
 #: src/views/certificate/CertificateEditor.vue:211
 msgid "SSL Certificate Content"
@@ -2453,8 +2505,9 @@ msgid "Streams Directory"
 msgstr "Directorio"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "Directorio"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2505,8 +2558,8 @@ msgstr ""
 #: src/components/Notification/cert.ts:14
 msgid "Sync Certificate %{cert_name} to %{env_name} failed, response: %{resp}"
 msgstr ""
-"Sincronización del Certificado %{cert_name} a %{env_name} falló, respuesta: %"
-"{resp}"
+"Sincronización del Certificado %{cert_name} a %{env_name} falló, respuesta: "
+"%{resp}"
 
 #: src/components/Notification/cert.ts:4
 msgid "Sync Certificate %{cert_name} to %{env_name} successfully"
@@ -2619,7 +2672,7 @@ msgid ""
 "The log path is not under the paths in settings.NginxSettings.LogDirWhiteList"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:23
 #, fuzzy
 msgid ""
 "The model name should only contain letters, unicode, numbers, hyphens, "
@@ -2654,13 +2707,6 @@ msgstr ""
 "El nombre del servidor solo debe contener letras, Unicode, números, guiones, "
 "rayas y puntos."
 
-#: src/views/preference/components/TOTP.vue:90
-msgid ""
-"The recovery code is only displayed once, please save it in a safe place."
-msgstr ""
-"El código de recuperación se muestra solo una vez, por favor guárdalo en un "
-"lugar seguro."
-
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2684,8 +2730,8 @@ msgstr ""
 msgid "The url is invalid"
 msgstr "La URL es inválida"
 
-#: src/views/preference/OpenAISettings.vue:47
-#: src/views/preference/OpenAISettings.vue:60
+#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:49
 msgid "The url is invalid."
 msgstr "La URL no es válida."
 
@@ -2693,6 +2739,13 @@ msgstr "La URL no es válida."
 msgid "The username or password is incorrect"
 msgstr "El nombre de usuario o contraseña son incorrectos"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr "Este elemento de Auto Cert es inválido, elimínelo por favor."
@@ -2738,14 +2791,14 @@ msgstr "Este valor ya está elegido"
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
-"Esto actualizará o reinstalará la interfaz de usuario de Nginx en %"
-"{nodeNames} a %{version}."
+"Esto actualizará o reinstalará la interfaz de usuario de Nginx en "
+"%{nodeNames} a %{version}."
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr "Acelerador"
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2755,7 +2808,7 @@ msgstr "Consejos"
 msgid "Title"
 msgstr "Título"
 
-#: src/views/preference/components/TOTP.vue:71
+#: src/views/preference/components/TOTP.vue:68
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2785,7 +2838,7 @@ msgstr ""
 "de la autoridad al backend, y debemos guardar este archivo y volver a cargar "
 "Nginx. ¿Estás seguro de que quieres continuar?"
 
-#: src/views/preference/OpenAISettings.vue:48
+#: src/views/preference/OpenAISettings.vue:36
 #, fuzzy
 msgid ""
 "To use a local large model, deploy it with ollama, vllm or lmdeploy. They "
@@ -2796,7 +2849,7 @@ msgstr ""
 "Estos proporcionan un API endpoint compatible con OpenAI, por lo que solo "
 "debe configurar la baseUrl en su API local."
 
-#: src/views/preference/OpenAISettings.vue:72
+#: src/views/preference/OpenAISettings.vue:61
 msgid "Token is not valid"
 msgstr "El token no es válido"
 
@@ -2806,11 +2859,11 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/views/preference/components/TOTP.vue:69
+#: src/views/preference/components/TOTP.vue:66
 msgid "TOTP"
 msgstr "TOTP"
 
-#: src/views/preference/components/TOTP.vue:70
+#: src/views/preference/components/TOTP.vue:67
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2881,11 +2934,11 @@ msgstr "Tiempo encendido:"
 msgid "URL"
 msgstr "URL"
 
-#: src/components/TwoFA/Authorization.vue:102
+#: src/components/TwoFA/Authorization.vue:121
 msgid "Use OTP"
 msgstr "Usar OTP"
 
-#: src/components/TwoFA/Authorization.vue:98
+#: src/components/TwoFA/Authorization.vue:117
 msgid "Use recovery code"
 msgstr "Usar código de recuperación"
 
@@ -2937,6 +2990,16 @@ msgstr "Ver detalles"
 msgid "View Mode"
 msgstr "Modo de vista"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "Código de Recuperación"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "Ver"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2960,7 +3023,7 @@ msgstr ""
 "Eliminaremos la configuración de HTTPChallenge de este archivo y "
 "recargaremos Nginx. ¿Estás seguro de que quieres continuar?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr "Webauthn"
 
@@ -2987,6 +3050,12 @@ msgstr ""
 "configurados en la categoría del sitio y los nodos seleccionados a "
 "continuación se sincronizarán."
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -3000,7 +3069,7 @@ msgstr "Escribir la clave privada del certificado a disco"
 msgid "Writing certificate to disk"
 msgstr "Escribir certificado a disco"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3023,10 +3092,49 @@ msgstr ""
 "No ha configurado los ajustes de Webauthn, por lo que no puede agregar una "
 "llave de acceso."
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr "Sus llaves de acceso"
 
+#~ msgid ""
+#~ "If you lose your mobile phone, you can use the recovery code to reset "
+#~ "your 2FA."
+#~ msgstr ""
+#~ "Si pierde su teléfono móvil, puede usar el código de recuperación para "
+#~ "restablecer su 2FA."
+
+#~ msgid "Recovery Code:"
+#~ msgstr "Código de Recuperación:"
+
+#~ msgid ""
+#~ "The recovery code is only displayed once, please save it in a safe place."
+#~ msgstr ""
+#~ "El código de recuperación se muestra solo una vez, por favor guárdalo en "
+#~ "un lugar seguro."
+
+#~ msgid "Can't scan? Use text key binding"
+#~ msgstr "¿No puede escanear? Utilice la vinculación con una llave de texto"
+
 #~ msgid "Directory"
 #~ msgstr "Directorio"
 

+ 191 - 92
app/src/language/fr_FR/app.po

@@ -15,7 +15,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr ""
 
@@ -39,7 +39,7 @@ msgstr "Nom d'utilisateur"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -109,7 +109,11 @@ msgstr ""
 msgid "All"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:44
+#: src/language/constants.ts:57
+msgid "All Recovery Codes Have Been Used"
+msgstr ""
+
+#: src/views/preference/OpenAISettings.vue:32
 msgid "API Base Url"
 msgstr "URL de base de l'API"
 
@@ -118,15 +122,15 @@ msgstr "URL de base de l'API"
 msgid "API Document"
 msgstr "Jeton d'API"
 
-#: src/views/preference/OpenAISettings.vue:57
+#: src/views/preference/OpenAISettings.vue:46
 msgid "API Proxy"
 msgstr "Proxy d'API"
 
-#: src/views/preference/OpenAISettings.vue:69
+#: src/views/preference/OpenAISettings.vue:58
 msgid "API Token"
 msgstr "Jeton d'API"
 
-#: src/views/preference/OpenAISettings.vue:78
+#: src/views/preference/OpenAISettings.vue:67
 #, fuzzy
 msgid "API Type"
 msgstr "Jeton d'API"
@@ -145,7 +149,7 @@ msgstr "Dupliqué avec succès"
 msgid "Arch"
 msgstr "Arch"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 #, fuzzy
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Etes-vous sûr que vous voulez supprimer ?"
@@ -155,6 +159,16 @@ msgstr "Etes-vous sûr que vous voulez supprimer ?"
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "Etes-vous sûr que vous voulez supprimer ?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "Voulez-vous vraiment supprimer cette directive ?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "Etes-vous sûr que vous voulez supprimer ?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -216,7 +230,7 @@ msgstr ""
 msgid "Attempt to fix"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr ""
 
@@ -225,11 +239,11 @@ msgstr ""
 msgid "Auth"
 msgstr "Autheur"
 
-#: src/components/TwoFA/Authorization.vue:121
+#: src/components/TwoFA/Authorization.vue:109
 msgid "Authenticate with a passkey"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr ""
 
@@ -266,15 +280,15 @@ msgstr "Retour au menu principal"
 msgid "Back to list"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr ""
 
@@ -330,10 +344,6 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:129
-msgid "Can't scan? Use text key binding"
-msgstr ""
-
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -475,6 +485,10 @@ msgstr "Effacer"
 msgid "Cleared successfully"
 msgstr "Désactivé avec succès"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 #, fuzzy
 msgid "Command"
@@ -523,6 +537,7 @@ msgstr "Contenu"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr ""
 
@@ -530,6 +545,10 @@ msgstr ""
 msgid "Copy"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+msgid "Copy Codes"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "Mise à jour du core"
@@ -584,11 +603,11 @@ msgstr "Identifiant"
 msgid "Credentials"
 msgstr "Identifiants"
 
-#: src/views/preference/components/TOTP.vue:77
+#: src/views/preference/components/TOTP.vue:72
 msgid "Current account is enabled TOTP."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:74
+#: src/views/preference/components/TOTP.vue:70
 msgid "Current account is not enabled TOTP."
 msgstr ""
 
@@ -670,12 +689,14 @@ msgid "Deploy"
 msgstr ""
 
 #: src/views/stream/components/Deploy.vue:57
+#, fuzzy
 msgid "Deploy %{conf_name} to %{node_name} failed"
-msgstr ""
+msgstr "Dupliqué avec succès"
 
 #: src/views/stream/components/Deploy.vue:36
+#, fuzzy
 msgid "Deploy %{conf_name} to %{node_name} successfully"
-msgstr ""
+msgstr "Dupliqué avec succès"
 
 #: src/views/stream/components/Deploy.vue:34
 #, fuzzy
@@ -937,14 +958,16 @@ msgid "Enable"
 msgstr "Activé"
 
 #: src/views/stream/components/Deploy.vue:47
+#, fuzzy
 msgid "Enable %{conf_name} in %{node_name} failed"
-msgstr ""
+msgstr "Dupliqué avec succès"
 
 #: src/views/stream/components/Deploy.vue:43
+#, fuzzy
 msgid "Enable %{conf_name} in %{node_name} successfully"
-msgstr ""
+msgstr "Dupliqué avec succès"
 
-#: src/views/preference/components/TOTP.vue:38
+#: src/views/preference/components/TOTP.vue:45
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "Activé avec succès"
@@ -986,7 +1009,7 @@ msgstr "Activé avec succès"
 msgid "Enable TLS"
 msgstr "Activer TLS"
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:81
 #, fuzzy
 msgid "Enable TOTP"
 msgstr "Activer TLS"
@@ -1087,8 +1110,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "Impossible d'activer %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1117,6 +1141,10 @@ msgstr "Filtrer"
 msgid "Finished"
 msgstr "Finie"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1154,6 +1182,20 @@ msgstr "Changer de certificat"
 msgid "Generate"
 msgstr "Générer"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate New Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "Enregistré avec succès"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "Génération de clé privée pour l'enregistrement du compte"
@@ -1202,18 +1244,12 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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:89
-msgid ""
-"If you lose your mobile phone, you can use the recovery code to reset your "
-"2FA."
-msgstr ""
-
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1247,12 +1283,11 @@ 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:136
+#: src/views/preference/components/TOTP.vue:119
 msgid "Input the code from the app:"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:82
-#: src/views/preference/components/TOTP.vue:149
+#: src/components/TwoFA/Authorization.vue:72
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1278,8 +1313,9 @@ msgid "Invalid filename"
 msgstr ""
 
 #: src/views/config/components/Mkdir.vue:57
+#, fuzzy
 msgid "Invalid folder name"
-msgstr ""
+msgstr "Veuillez saisir votre nom d'utilisateur !"
 
 #: src/constants/errors/user.ts:4
 msgid "Invalid otp code"
@@ -1297,7 +1333,7 @@ msgstr ""
 msgid "Invalid request format"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr ""
 
@@ -1323,6 +1359,12 @@ msgstr ""
 msgid "Jwt Secret"
 msgstr "Secret Jwt"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 #, fuzzy
@@ -1342,7 +1384,7 @@ msgstr "Dernière vérification le"
 msgid "Leave blank for no change"
 msgstr "Laisser vide pour aucun changement"
 
-#: src/views/preference/OpenAISettings.vue:53
+#: src/views/preference/OpenAISettings.vue:41
 msgid "Leave blank for the default: https://api.openai.com/"
 msgstr "Laissez vide pour la valeur par défaut : https://api.openai.com/"
 
@@ -1415,7 +1457,7 @@ msgstr "Localisations"
 msgid "Log"
 msgstr "Connexion"
 
-#: src/routes/index.ts:318 src/views/other/Login.vue:222
+#: src/routes/index.ts:318 src/views/other/Login.vue:223
 msgid "Login"
 msgstr "Connexion"
 
@@ -1473,7 +1515,7 @@ msgstr "Gérer les utilisateurs"
 msgid "Managed Certificate"
 msgstr "Changer de certificat"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr ""
 
@@ -1490,7 +1532,7 @@ msgstr "Mémoire et stockage"
 msgid "Minutes"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:32
+#: src/views/preference/OpenAISettings.vue:20
 #, fuzzy
 msgid "Model"
 msgstr "Mode d'exécution"
@@ -1635,8 +1677,9 @@ msgid "Nginx Log"
 msgstr "Journal Nginx"
 
 #: src/views/preference/NginxSettings.vue:18
+#, fuzzy
 msgid "Nginx Log Directory Whitelist"
-msgstr ""
+msgstr "Erreur d'analyse de configuration Nginx"
 
 #: src/views/preference/NginxSettings.vue:27
 #, fuzzy
@@ -1644,8 +1687,9 @@ msgid "Nginx PID Path"
 msgstr "Chemin du journal des erreurs Nginx"
 
 #: src/views/preference/NginxSettings.vue:30
+#, fuzzy
 msgid "Nginx Reload Command"
-msgstr ""
+msgstr "Commande de démarrage du terminal"
 
 #: src/components/NginxControl/NginxControl.vue:26
 msgid "Nginx reloaded successfully"
@@ -1668,7 +1712,7 @@ msgstr "Nginx a redémarré avec succès"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1783,10 +1827,14 @@ msgstr ""
 msgid "OpenAI"
 msgstr "OpenAI"
 
-#: src/components/TwoFA/Authorization.vue:112 src/views/other/Login.vue:231
+#: src/components/TwoFA/Authorization.vue:100 src/views/other/Login.vue:232
 msgid "Or"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr ""
@@ -1881,7 +1929,7 @@ msgid ""
 "button below."
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:70
+#: src/components/TwoFA/Authorization.vue:85
 msgid "Please enter the OTP code:"
 msgstr ""
 
@@ -1906,6 +1954,12 @@ msgstr ""
 "des informations d'identification ci-dessous pour demander l'API du "
 "fournisseur DNS."
 
+#: src/language/constants.ts:58
+msgid ""
+"Please generate new recovery codes in the preferences immediately to prevent "
+"lockout."
+msgstr ""
+
 #: src/views/config/components/Rename.vue:63
 #: src/views/config/ConfigEditor.vue:249
 #, fuzzy
@@ -2036,17 +2090,18 @@ msgstr ""
 msgid "Recovered Successfully"
 msgstr "Enregistré avec succès"
 
-#: src/components/TwoFA/Authorization.vue:89
-#: src/views/preference/components/TOTP.vue:156
+#: src/components/TwoFA/Authorization.vue:79
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:82
-msgid "Recovery Code"
+#: src/views/preference/components/RecoveryCodes.vue:68
+msgid "Recovery Codes"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:91
-msgid "Recovery Code:"
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
 msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
@@ -2118,7 +2173,7 @@ msgstr "Rechargement"
 msgid "Reloading nginx"
 msgstr "Rechargement de nginx"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr ""
 
@@ -2132,7 +2187,7 @@ msgstr "Supprimer le site : %{site_name}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "Dupliqué avec succès"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 #, fuzzy
 msgid "Remove successfully"
@@ -2238,7 +2293,7 @@ msgstr ""
 msgid "Reset"
 msgstr "Réinitialiser"
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:93
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "Réinitialiser"
@@ -2251,15 +2306,15 @@ msgstr "Redémarrer"
 msgid "Restarting"
 msgstr "Redémarrage"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2330,7 +2385,7 @@ msgstr "Sauvegarde réussie"
 msgid "Saved successfully"
 msgstr "Enregistré avec succès"
 
-#: src/views/preference/components/TOTP.vue:72
+#: src/views/preference/components/TOTP.vue:69
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -2338,7 +2393,7 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:128
+#: src/views/preference/components/TOTP.vue:109
 msgid "Secret has been copied"
 msgstr ""
 
@@ -2398,21 +2453,21 @@ msgstr "Utilisation du fournisseur de challenge HTTP01"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Show"
 msgstr ""
 
-#: src/views/other/Login.vue:240
+#: src/views/other/Login.vue:241
 msgid "Sign in with a passkey"
 msgstr ""
 
@@ -2448,12 +2503,14 @@ msgid "Sites List"
 msgstr "Liste des sites"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "Directive"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "Directive"
 
 #: src/views/certificate/CertificateEditor.vue:211
 #, fuzzy
@@ -2507,8 +2564,9 @@ msgid "Streams Directory"
 msgstr "Directive"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "Directive"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2608,8 +2666,9 @@ msgid "Sync strategy"
 msgstr "Changer de certificat"
 
 #: src/views/certificate/CertificateEditor.vue:204
+#, fuzzy
 msgid "Sync to"
-msgstr ""
+msgstr "Changer de certificat"
 
 #: src/views/site/site_edit/RightSettings.vue:110
 msgid "Synchronization"
@@ -2661,8 +2720,9 @@ msgid ""
 msgstr ""
 
 #: src/views/certificate/CertificateEditor.vue:214
+#, fuzzy
 msgid "The input is not a SSL Certificate"
-msgstr ""
+msgstr "Chemin de la clé du certificat SSL"
 
 #: src/views/certificate/CertificateEditor.vue:227
 #, fuzzy
@@ -2674,7 +2734,7 @@ msgid ""
 "The log path is not under the paths in settings.NginxSettings.LogDirWhiteList"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:23
 msgid ""
 "The model name should only contain letters, unicode, numbers, hyphens, "
 "dashes, colons, and dots."
@@ -2692,8 +2752,9 @@ msgid "The path exists, but the file is not a certificate"
 msgstr "Chemin de la clé du certificat SSL"
 
 #: src/views/certificate/CertificateEditor.vue:194
+#, fuzzy
 msgid "The path exists, but the file is not a private key"
-msgstr ""
+msgstr "Chemin de la clé du certificat SSL"
 
 #: src/views/preference/BasicSettings.vue:66
 msgid ""
@@ -2701,11 +2762,6 @@ msgid ""
 "hyphens, dashes, colons, and dots."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:90
-msgid ""
-"The recovery code is only displayed once, please save it in a safe place."
-msgstr ""
-
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2728,8 +2784,8 @@ msgstr ""
 msgid "The url is invalid"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:47
-#: src/views/preference/OpenAISettings.vue:60
+#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:49
 msgid "The url is invalid."
 msgstr ""
 
@@ -2737,6 +2793,13 @@ msgstr ""
 msgid "The username or password is incorrect"
 msgstr "Le pseudo ou mot de passe est incorect"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 #, fuzzy
 msgid "This Auto Cert item is invalid, please remove it."
@@ -2777,15 +2840,16 @@ msgid "This value is already taken"
 msgstr ""
 
 #: src/views/environment/BatchUpgrader.vue:182
+#, fuzzy
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
-msgstr ""
+msgstr "Dupliqué avec succès"
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2795,7 +2859,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:71
+#: src/views/preference/components/TOTP.vue:68
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2820,14 +2884,14 @@ msgstr ""
 "transmettre la demande de l'autorité au backend, et nous devons enregistrer "
 "ce fichier et recharger le Nginx. Êtes-vous sûr de vouloir continuer?"
 
-#: src/views/preference/OpenAISettings.vue:48
+#: src/views/preference/OpenAISettings.vue:36
 msgid ""
 "To use a local large model, deploy it with ollama, vllm or lmdeploy. They "
 "provide an OpenAI-compatible API endpoint, so just set the baseUrl to your "
 "local API."
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:72
+#: src/views/preference/OpenAISettings.vue:61
 msgid "Token is not valid"
 msgstr ""
 
@@ -2837,11 +2901,11 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/views/preference/components/TOTP.vue:69
+#: src/views/preference/components/TOTP.vue:66
 msgid "TOTP"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:70
+#: src/views/preference/components/TOTP.vue:67
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2911,11 +2975,11 @@ msgstr "Disponibilité :"
 msgid "URL"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:102
+#: src/components/TwoFA/Authorization.vue:121
 msgid "Use OTP"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:98
+#: src/components/TwoFA/Authorization.vue:117
 msgid "Use recovery code"
 msgstr ""
 
@@ -2971,6 +3035,15 @@ msgstr ""
 msgid "View Mode"
 msgstr "Mode simple"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+msgid "View Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "Voir"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2992,7 +3065,7 @@ msgstr ""
 "Nous allons supprimer la configuration HTTPChallenge de ce fichier et "
 "recharger le Nginx. Êtes-vous sûr de vouloir continuer?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -3013,6 +3086,12 @@ msgid ""
 "site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -3026,7 +3105,7 @@ msgstr "Écriture de la clé privée du certificat sur le disque"
 msgid "Writing certificate to disk"
 msgstr "Écriture du certificat sur le disque"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3047,6 +3126,26 @@ msgid ""
 "passkey."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr ""

+ 212 - 35
app/src/language/ko_KR/app.po

@@ -18,7 +18,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr "2FA 설정"
 
@@ -41,7 +41,7 @@ msgstr "ACME 사용자"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -141,7 +141,7 @@ msgstr "성공적으로 복제됨"
 msgid "Arch"
 msgstr "아키텍처"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "차단된 IP를 즉시 삭제하시겠습니까?"
 
@@ -150,6 +150,16 @@ msgstr "차단된 IP를 즉시 삭제하시겠습니까?"
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "차단된 IP를 즉시 삭제하시겠습니까?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "이 항목을 복구하시겠습니까?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "정말 삭제하시겠습니까?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -206,7 +216,7 @@ msgstr "조수"
 msgid "Attempt to fix"
 msgstr "시도 횟수"
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr "시도 횟수"
 
@@ -218,7 +228,7 @@ msgstr "인증"
 msgid "Authenticate with a passkey"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr ""
 
@@ -255,15 +265,15 @@ msgstr "홈으로"
 msgid "Back to list"
 msgstr "목록으로 돌아가기"
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "차단 시간(분)"
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr "차단된 IP"
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr "차단될 시간"
 
@@ -317,10 +327,13 @@ msgstr "CA 디렉토리"
 msgid "CADir"
 msgstr "CA 디렉토리"
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:129
 msgid "Can't scan? Use text key binding"
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -457,6 +470,10 @@ msgstr "클리어"
 msgid "Cleared successfully"
 msgstr "성공적으로 제거됨"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr "명령어"
@@ -504,6 +521,7 @@ msgstr "내용"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr ""
 
@@ -511,6 +529,10 @@ msgstr ""
 msgid "Copy"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+msgid "Copy Codes"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "코어 업그레이드"
@@ -564,11 +586,19 @@ msgstr "인증 정보"
 msgid "Credentials"
 msgstr "인증 정보들"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:72
+msgid "Current account is enabled TOTP."
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:70
+=======
 #: src/views/preference/components/TOTP.vue:77
 msgid "Current account is enabled TOTP."
 msgstr ""
 
 #: src/views/preference/components/TOTP.vue:74
+>>>>>>> remotes/origin/dev
 msgid "Current account is not enabled TOTP."
 msgstr ""
 
@@ -909,7 +939,11 @@ msgstr "%{node_name}에서 %{conf_name} 활성화 실패"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "%{node_name}에서 %{conf_name} 성공적으로 활성화됨"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:45
+=======
 #: src/views/preference/components/TOTP.vue:38
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "성공적으로 활성화"
@@ -950,7 +984,11 @@ msgstr "성공적으로 활성화"
 msgid "Enable TLS"
 msgstr "TLS 활성화"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:81
+=======
 #: src/views/preference/components/TOTP.vue:103
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Enable TOTP"
 msgstr "TLS 활성화"
@@ -1050,8 +1088,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "%{msg} 활성화 실패"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1078,6 +1117,10 @@ msgstr "필터"
 msgid "Finished"
 msgstr "완료됨"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1116,6 +1159,21 @@ msgstr "일반 인증서"
 msgid "Generate"
 msgstr "생성"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate New Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "유효함"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "성공적으로 제거됨"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "계정 등록을 위한 개인 키 생성 중"
@@ -1165,18 +1223,21 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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 ""
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:89
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1209,12 +1270,19 @@ msgstr "초기 코어 업그레이더 오류"
 msgid "Initialing core upgrader"
 msgstr "코어 업그레이더 초기화"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:119
+=======
 #: src/views/preference/components/TOTP.vue:136
+>>>>>>> remotes/origin/dev
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/TwoFA/Authorization.vue:82
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:149
+>>>>>>> remotes/origin/dev
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1243,8 +1311,9 @@ msgid "Invalid filename"
 msgstr "Invalid E-mail!"
 
 #: src/views/config/components/Mkdir.vue:57
+#, fuzzy
 msgid "Invalid folder name"
-msgstr ""
+msgstr "Invalid E-mail!"
 
 #: src/constants/errors/user.ts:4
 #, fuzzy
@@ -1256,14 +1325,19 @@ msgid "Invalid passcode or recovery code"
 msgstr ""
 
 #: src/constants/errors/user.ts:5
+#, fuzzy
 msgid "Invalid recovery code"
-msgstr ""
+msgstr "유효함"
 
+<<<<<<< HEAD
+#: src/views/preference/AuthSettings.vue:18
+=======
 #: src/constants/errors/middleware.ts:2
 msgid "Invalid request format"
 msgstr ""
 
 #: src/views/preference/AuthSettings.vue:14
+>>>>>>> remotes/origin/dev
 msgid "IP"
 msgstr ""
 
@@ -1290,6 +1364,12 @@ msgstr ""
 msgid "Jwt Secret"
 msgstr "Jwt 토큰"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1442,9 +1522,10 @@ msgstr "사용자 관리"
 msgid "Managed Certificate"
 msgstr "인증서 유효"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
+#, fuzzy
 msgid "Max Attempts"
-msgstr ""
+msgstr "시도 횟수"
 
 #: src/views/dashboard/ServerAnalytic.vue:222
 #: src/views/dashboard/ServerAnalytic.vue:223
@@ -1606,8 +1687,9 @@ msgid "Nginx Log"
 msgstr "Nginx 로그"
 
 #: src/views/preference/NginxSettings.vue:18
+#, fuzzy
 msgid "Nginx Log Directory Whitelist"
-msgstr ""
+msgstr "Nginx 구성 오류름"
 
 #: src/views/preference/NginxSettings.vue:27
 #, fuzzy
@@ -1615,8 +1697,9 @@ msgid "Nginx PID Path"
 msgstr "Nginx 오류 로그 경로"
 
 #: src/views/preference/NginxSettings.vue:30
+#, fuzzy
 msgid "Nginx Reload Command"
-msgstr ""
+msgstr "터미널 시작 명령"
 
 #: src/components/NginxControl/NginxControl.vue:26
 #, fuzzy
@@ -1641,7 +1724,7 @@ msgstr "Nginx가 성공적으로 재시작됨"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1760,6 +1843,10 @@ msgstr "오픈AI"
 msgid "Or"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr ""
@@ -2003,6 +2090,20 @@ msgid "Recovered Successfully"
 msgstr "성공적으로 제거됨"
 
 #: src/components/TwoFA/Authorization.vue:89
+<<<<<<< HEAD
+msgid "Recovery"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:68
+#, fuzzy
+msgid "Recovery Codes"
+msgstr "유효함"
+
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+=======
 #: src/views/preference/components/TOTP.vue:156
 msgid "Recovery"
 msgstr ""
@@ -2013,6 +2114,7 @@ msgstr ""
 
 #: src/views/preference/components/TOTP.vue:91
 msgid "Recovery Code:"
+>>>>>>> remotes/origin/dev
 msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
@@ -2085,7 +2187,7 @@ msgstr "리로딩 중"
 msgid "Reloading nginx"
 msgstr "Nginx 리로딩 중"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr ""
 
@@ -2099,7 +2201,7 @@ msgstr "사이트 삭제: %{site_name}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "%{conf_name}을(를) %{node_name}(으)로 성공적으로 복제함"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 #, fuzzy
 msgid "Remove successfully"
@@ -2205,7 +2307,11 @@ msgstr "잘못된 매개변수로 요청됨"
 msgid "Reset"
 msgstr "재설정"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:93
+=======
 #: src/views/preference/components/TOTP.vue:111
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "재설정"
@@ -2218,15 +2324,15 @@ msgstr "재시작"
 msgid "Restarting"
 msgstr "재시작 중"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2299,7 +2405,11 @@ msgstr "성공적으로 저장됨"
 msgid "Saved successfully"
 msgstr "성공적으로 저장됨"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:69
+=======
 #: src/views/preference/components/TOTP.vue:72
+>>>>>>> remotes/origin/dev
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -2307,7 +2417,11 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:109
+=======
 #: src/views/preference/components/TOTP.vue:128
+>>>>>>> remotes/origin/dev
 msgid "Secret has been copied"
 msgstr ""
 
@@ -2365,14 +2479,14 @@ msgstr "HTTP01 공급자 설정"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
@@ -2416,12 +2530,14 @@ msgid "Sites List"
 msgstr "사이트 목록"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "디렉토리"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "디렉토리"
 
 #: src/views/certificate/CertificateEditor.vue:211
 #, fuzzy
@@ -2476,8 +2592,9 @@ msgid "Streams Directory"
 msgstr "디렉토리"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "디렉토리"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2576,8 +2693,9 @@ msgid "Sync strategy"
 msgstr "인증서 갱신"
 
 #: src/views/certificate/CertificateEditor.vue:204
+#, fuzzy
 msgid "Sync to"
-msgstr ""
+msgstr "인증서 갱신"
 
 #: src/views/site/site_edit/RightSettings.vue:110
 msgid "Synchronization"
@@ -2669,11 +2787,14 @@ msgid ""
 "hyphens, dashes, colons, and dots."
 msgstr ""
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:90
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2706,6 +2827,13 @@ msgstr "유효한 URL이 아닙니다"
 msgid "The username or password is incorrect"
 msgstr "사용자 이름 또는 비밀번호가 올바르지 않습니다"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr "이 자동 인증 항목이 유효하지 않습니다. 제거해주세요."
@@ -2745,15 +2873,16 @@ msgid "This value is already taken"
 msgstr ""
 
 #: src/views/environment/BatchUpgrader.vue:182
+#, fuzzy
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
-msgstr ""
+msgstr "%{conf_name}을(를) %{node_name}(으)로 성공적으로 복제함"
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2763,7 +2892,11 @@ msgstr "팁"
 msgid "Title"
 msgstr "제목"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:68
+=======
 #: src/views/preference/components/TOTP.vue:71
+>>>>>>> remotes/origin/dev
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2804,11 +2937,19 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:66
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:67
+=======
 #: src/views/preference/components/TOTP.vue:69
 msgid "TOTP"
 msgstr ""
 
 #: src/views/preference/components/TOTP.vue:70
+>>>>>>> remotes/origin/dev
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2941,6 +3082,16 @@ msgstr "세부 사항"
 msgid "View Mode"
 msgstr "기본 모드"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "유효함"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "보기"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2964,7 +3115,7 @@ msgstr ""
 "이 파일에서 HTTPChallenge 구성을 제거하고 Nginx를 다시 로드할 예정입니다. 계"
 "속하시겠습니까?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -2985,6 +3136,12 @@ msgid ""
 "site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -2998,7 +3155,7 @@ msgstr "인증서 개인 키를 디스크에 쓰기"
 msgid "Writing certificate to disk"
 msgstr "인증서를 디스크에 쓰기"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3019,6 +3176,26 @@ msgid ""
 "passkey."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr ""

+ 140 - 71
app/src/language/messages.pot

@@ -6,7 +6,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr ""
 
@@ -31,7 +31,7 @@ msgstr ""
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76
 #: src/views/stream/StreamList.vue:49
@@ -100,7 +100,11 @@ msgstr ""
 msgid "All"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:44
+#: src/language/constants.ts:57
+msgid "All Recovery Codes Have Been Used"
+msgstr ""
+
+#: src/views/preference/OpenAISettings.vue:32
 msgid "API Base Url"
 msgstr ""
 
@@ -108,15 +112,15 @@ msgstr ""
 msgid "API Document"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:57
+#: src/views/preference/OpenAISettings.vue:46
 msgid "API Proxy"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:69
+#: src/views/preference/OpenAISettings.vue:58
 msgid "API Token"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:78
+#: src/views/preference/OpenAISettings.vue:67
 msgid "API Type"
 msgstr ""
 
@@ -132,7 +136,7 @@ msgstr ""
 msgid "Arch"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr ""
 
@@ -140,6 +144,14 @@ msgstr ""
 msgid "Are you sure to delete this passkey immediately?"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+msgid "Are you sure to generate new recovery codes?"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:85
+msgid "Are you sure to reset 2FA?"
+msgstr ""
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 msgid "Are you sure you want to apply to all selected?"
 msgstr ""
@@ -194,7 +206,7 @@ msgstr ""
 msgid "Attempt to fix"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr ""
 
@@ -202,11 +214,11 @@ msgstr ""
 msgid "Auth"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:121
+#: src/components/TwoFA/Authorization.vue:109
 msgid "Authenticate with a passkey"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr ""
 
@@ -245,15 +257,15 @@ msgstr ""
 msgid "Back to list"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr ""
 
@@ -306,10 +318,6 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:129
-msgid "Can't scan? Use text key binding"
-msgstr ""
-
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -437,6 +445,10 @@ msgstr ""
 msgid "Cleared successfully"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr ""
@@ -484,6 +496,7 @@ msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr ""
 
@@ -491,6 +504,10 @@ msgstr ""
 msgid "Copy"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+msgid "Copy Codes"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr ""
@@ -543,11 +560,11 @@ msgstr ""
 msgid "Credentials"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:77
+#: src/views/preference/components/TOTP.vue:72
 msgid "Current account is enabled TOTP."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:74
+#: src/views/preference/components/TOTP.vue:70
 msgid "Current account is not enabled TOTP."
 msgstr ""
 
@@ -879,7 +896,7 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:38
+#: src/views/preference/components/TOTP.vue:45
 msgid "Enable 2FA successfully"
 msgstr ""
 
@@ -915,7 +932,7 @@ msgstr ""
 msgid "Enable TLS"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:81
 msgid "Enable TOTP"
 msgstr ""
 
@@ -1044,6 +1061,10 @@ msgstr ""
 msgid "Finished"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid "Follow the instructions in the dialog to complete the passkey registration process."
 msgstr ""
@@ -1077,6 +1098,19 @@ msgstr ""
 msgid "Generate"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate New Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+msgid "Generate recovery codes successfully"
+msgstr ""
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr ""
@@ -1126,14 +1160,10 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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:89
-msgid "If you lose your mobile phone, you can use the recovery code to reset your 2FA."
-msgstr ""
-
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1165,12 +1195,11 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:136
+#: src/views/preference/components/TOTP.vue:119
 msgid "Input the code from the app:"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:82
-#: src/views/preference/components/TOTP.vue:149
+#: src/components/TwoFA/Authorization.vue:72
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1216,7 +1245,7 @@ msgstr ""
 msgid "Invalid request format"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr ""
 
@@ -1240,6 +1269,10 @@ msgstr ""
 msgid "Jwt Secret"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid "Keep your recovery codes as safe as your password. We recommend saving them with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1257,7 +1290,7 @@ msgstr ""
 msgid "Leave blank for no change"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:53
+#: src/views/preference/OpenAISettings.vue:41
 msgid "Leave blank for the default: https://api.openai.com/"
 msgstr ""
 
@@ -1321,7 +1354,7 @@ msgid "Log"
 msgstr ""
 
 #: src/routes/index.ts:318
-#: src/views/other/Login.vue:222
+#: src/views/other/Login.vue:223
 msgid "Login"
 msgstr ""
 
@@ -1372,7 +1405,7 @@ msgstr ""
 msgid "Managed Certificate"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr ""
 
@@ -1389,7 +1422,7 @@ msgstr ""
 msgid "Minutes"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:32
+#: src/views/preference/OpenAISettings.vue:20
 msgid "Model"
 msgstr ""
 
@@ -1562,7 +1595,7 @@ msgstr ""
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1670,11 +1703,15 @@ msgstr ""
 msgid "OpenAI"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:112
-#: src/views/other/Login.vue:231
+#: src/components/TwoFA/Authorization.vue:100
+#: src/views/other/Login.vue:232
 msgid "Or"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr ""
@@ -1765,7 +1802,7 @@ msgstr ""
 msgid "Please enter a name for the passkey you wish to create and click the OK button below."
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:70
+#: src/components/TwoFA/Authorization.vue:85
 msgid "Please enter the OTP code:"
 msgstr ""
 
@@ -1781,6 +1818,10 @@ msgstr ""
 msgid "Please first add credentials in Certification > DNS Credentials, and then select one of the credentialsbelow to request the API of the DNS provider."
 msgstr ""
 
+#: src/language/constants.ts:58
+msgid "Please generate new recovery codes in the preferences immediately to prevent lockout."
+msgstr ""
+
 #: src/views/config/components/Rename.vue:63
 #: src/views/config/ConfigEditor.vue:249
 msgid "Please input a filename"
@@ -1897,17 +1938,16 @@ msgstr ""
 msgid "Recovered Successfully"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:89
-#: src/views/preference/components/TOTP.vue:156
+#: src/components/TwoFA/Authorization.vue:79
 msgid "Recovery"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:82
-msgid "Recovery Code"
+#: src/views/preference/components/RecoveryCodes.vue:68
+msgid "Recovery Codes"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:91
-msgid "Recovery Code:"
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid "Recovery codes are used to access your account when you lose access to your 2FA device. Each code can only be used once."
 msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
@@ -1971,7 +2011,7 @@ msgstr ""
 msgid "Reloading nginx"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr ""
 
@@ -1983,7 +2023,7 @@ msgstr ""
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 msgid "Remove successfully"
 msgstr ""
@@ -2071,7 +2111,7 @@ msgstr ""
 msgid "Reset"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:93
 msgid "Reset 2FA"
 msgstr ""
 
@@ -2083,15 +2123,15 @@ msgstr ""
 msgid "Restarting"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2159,7 +2199,7 @@ msgstr ""
 msgid "Saved successfully"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:72
+#: src/views/preference/components/TOTP.vue:69
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -2167,7 +2207,7 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:128
+#: src/views/preference/components/TOTP.vue:109
 msgid "Secret has been copied"
 msgstr ""
 
@@ -2233,7 +2273,7 @@ msgstr ""
 msgid "Show"
 msgstr ""
 
-#: src/views/other/Login.vue:240
+#: src/views/other/Login.vue:241
 msgid "Sign in with a passkey"
 msgstr ""
 
@@ -2462,7 +2502,7 @@ msgstr ""
 msgid "The log path is not under the paths in settings.NginxSettings.LogDirWhiteList"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:23
 msgid "The model name should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots."
 msgstr ""
 
@@ -2482,10 +2522,6 @@ msgstr ""
 msgid "The Public Security Number should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots."
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:90
-msgid "The recovery code is only displayed once, please save it in a safe place."
-msgstr ""
-
 #: src/views/dashboard/Environments.vue:148
 msgid "The remote Nginx UI version is not compatible with the local Nginx UI version. To avoid potential errors, please upgrade the remote Nginx UI to match the local version."
 msgstr ""
@@ -2499,8 +2535,8 @@ msgstr ""
 msgid "The url is invalid"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:47
-#: src/views/preference/OpenAISettings.vue:60
+#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:49
 msgid "The url is invalid."
 msgstr ""
 
@@ -2508,6 +2544,10 @@ msgstr ""
 msgid "The username or password is incorrect"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid "These codes are the last resort for accessing your account in case you lose your password and second factors. If you cannot find these codes, you will lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr ""
@@ -2547,11 +2587,11 @@ msgstr ""
 msgid "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2561,7 +2601,7 @@ msgstr ""
 msgid "Title"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:71
+#: src/views/preference/components/TOTP.vue:68
 msgid "To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone."
 msgstr ""
 
@@ -2573,11 +2613,11 @@ msgstr ""
 msgid "To make sure the certification auto-renewal can work normally, we need to add a location which can proxy the request from authority to backend, and we need to save this file and reload the Nginx. Are you sure you want to continue?"
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:48
+#: src/views/preference/OpenAISettings.vue:36
 msgid "To use a local large model, deploy it with ollama, vllm or lmdeploy. They provide an OpenAI-compatible API endpoint, so just set the baseUrl to your local API."
 msgstr ""
 
-#: src/views/preference/OpenAISettings.vue:72
+#: src/views/preference/OpenAISettings.vue:61
 msgid "Token is not valid"
 msgstr ""
 
@@ -2587,11 +2627,11 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/views/preference/components/TOTP.vue:69
+#: src/views/preference/components/TOTP.vue:66
 msgid "TOTP"
 msgstr ""
 
-#: src/views/preference/components/TOTP.vue:70
+#: src/views/preference/components/TOTP.vue:67
 msgid "TOTP is a two-factor authentication method that uses a time-based one-time password algorithm."
 msgstr ""
 
@@ -2662,11 +2702,11 @@ msgstr ""
 msgid "URL"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:102
+#: src/components/TwoFA/Authorization.vue:121
 msgid "Use OTP"
 msgstr ""
 
-#: src/components/TwoFA/Authorization.vue:98
+#: src/components/TwoFA/Authorization.vue:117
 msgid "Use recovery code"
 msgstr ""
 
@@ -2718,6 +2758,14 @@ msgstr ""
 msgid "View Mode"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+msgid "View Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "Viewed"
+msgstr ""
+
 #: src/constants/index.ts:17
 #: src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
@@ -2734,7 +2782,7 @@ msgstr ""
 msgid "We will remove the HTTPChallenge configuration from this file and reload the Nginx. Are you sure you want to continue?"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -2750,6 +2798,10 @@ msgstr ""
 msgid "When you enable/disable, delete, or save this site, the nodes set in the site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid "When you generate new recovery codes, you must download or print the new codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -2763,7 +2815,7 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -2782,6 +2834,23 @@ msgstr ""
 msgid "You have not configured the settings of Webauthn, so you cannot add a passkey."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid "You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid "Your current recovery code might be outdated and insecure. Please generate new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr ""

+ 239 - 44
app/src/language/ru_RU/app.po

@@ -19,7 +19,7 @@ msgstr ""
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr "Настройки 2FA"
 
@@ -42,7 +42,7 @@ msgstr "Пользователь ACME"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -142,7 +142,7 @@ msgstr "Продублированно"
 msgid "Arch"
 msgstr "Архитектура"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Вы уверены, что хотите немедленно удалить этот заблокированный IP?"
 
@@ -151,6 +151,16 @@ msgstr "Вы уверены, что хотите немедленно удали
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "Вы уверены, что хотите немедленно удалить этот заблокированный IP?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "Вы уверены, что хотите восстановить этот элемент?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "Вы уверены, что хотите удалить?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -208,7 +218,7 @@ msgstr "Ассистент"
 msgid "Attempt to fix"
 msgstr "Попытки"
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr "Попытки"
 
@@ -220,7 +230,7 @@ msgstr "Авторизация"
 msgid "Authenticate with a passkey"
 msgstr "Аутентификация с помощью ключа доступа"
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr "Настройки аутентификации"
 
@@ -257,15 +267,15 @@ msgstr "Вернуться на главную"
 msgid "Back to list"
 msgstr "Возврат к списку"
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "Порог блокировки в минутах"
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr "Заблокированные IP-адреса"
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr "Заблокирован до"
 
@@ -320,10 +330,13 @@ msgstr "Директория корневого сертификата"
 msgid "CADir"
 msgstr ""
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:129
 msgid "Can't scan? Use text key binding"
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -459,6 +472,10 @@ msgstr "Очистить"
 msgid "Cleared successfully"
 msgstr "Очищено успешно"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr "Команда"
@@ -506,6 +523,7 @@ msgstr "Содержание"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr "Скопировано"
 
@@ -513,6 +531,11 @@ msgstr "Скопировано"
 msgid "Copy"
 msgstr "Копировать"
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+#, fuzzy
+msgid "Copy Codes"
+msgstr "Код восстановления"
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "Обновление ядра"
@@ -563,12 +586,20 @@ msgstr "Учетные данные"
 msgid "Credentials"
 msgstr "Учетные данные"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:72
+=======
 #: src/views/preference/components/TOTP.vue:77
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Current account is enabled TOTP."
 msgstr "Текущая учетная запись имеет включенную 2ФА."
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:70
+=======
 #: src/views/preference/components/TOTP.vue:74
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Current account is not enabled TOTP."
 msgstr ""
@@ -916,7 +947,11 @@ msgstr "Включение %{conf_name} in %{node_name} нипалучилася
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Включение %{conf_name} in %{node_name} успешно"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:45
+=======
 #: src/views/preference/components/TOTP.vue:38
+>>>>>>> remotes/origin/dev
 msgid "Enable 2FA successfully"
 msgstr "Двухфакторная аутентификация успешно включена"
 
@@ -956,7 +991,11 @@ msgstr "Включено успешно"
 msgid "Enable TLS"
 msgstr "Включить TLS"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:81
+=======
 #: src/views/preference/components/TOTP.vue:103
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Enable TOTP"
 msgstr "Включить TLS"
@@ -1053,8 +1092,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "Не удалось включить %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1081,6 +1121,10 @@ msgstr "Фильтр"
 msgid "Finished"
 msgstr "Готово"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1118,6 +1162,22 @@ msgstr "Общий сертификат"
 msgid "Generate"
 msgstr "Сгенерировать"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate New Recovery Codes"
+msgstr "Код восстановления"
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "Код восстановления"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "Восстановлено успешно"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "Генерация приватного ключа для регистрации учетной записи"
@@ -1166,7 +1226,7 @@ msgstr "ICP номер"
 msgid "If left blank, the default CA Dir will be used."
 msgstr "Если оставить пустым, будет использоваться каталог CA по умолчанию."
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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."
@@ -1175,6 +1235,8 @@ msgstr ""
 "количества попыток в течение пороговых минут блокировки, IP будет "
 "заблокирован на определенный период времени."
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:89
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
@@ -1183,6 +1245,7 @@ msgstr ""
 "Если вы потеряете свой мобильный телефон, вы можете использовать код "
 "восстановления для сброса 2FA."
 
+>>>>>>> remotes/origin/dev
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1217,12 +1280,19 @@ msgstr "Ошибка первоначального обновления ядр
 msgid "Initialing core upgrader"
 msgstr "Инициализация программы обновления ядра"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:119
+=======
 #: src/views/preference/components/TOTP.vue:136
+>>>>>>> remotes/origin/dev
 msgid "Input the code from the app:"
 msgstr "Введите код из приложения:"
 
 #: src/components/TwoFA/Authorization.vue:82
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:149
+>>>>>>> remotes/origin/dev
 msgid "Input the recovery code:"
 msgstr "Введите код восстановления:"
 
@@ -1265,12 +1335,16 @@ msgstr "Неверный пароль или код восстановления
 msgid "Invalid recovery code"
 msgstr "Неверный 2FA или код восстановления"
 
+<<<<<<< HEAD
+#: src/views/preference/AuthSettings.vue:18
+=======
 #: src/constants/errors/middleware.ts:2
 #, fuzzy
 msgid "Invalid request format"
 msgstr "Неверный 2FA или код восстановления"
 
 #: src/views/preference/AuthSettings.vue:14
+>>>>>>> remotes/origin/dev
 msgid "IP"
 msgstr "IP"
 
@@ -1294,6 +1368,12 @@ msgstr "Издатель: %{issuer}"
 msgid "Jwt Secret"
 msgstr "Jwt секрет"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1437,7 +1517,7 @@ msgstr "Пользователи"
 msgid "Managed Certificate"
 msgstr "Управление сертификатом"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr "Максимальное количество попыток"
 
@@ -1626,7 +1706,7 @@ msgstr "Nginx успешно перезапущен"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1641,8 +1721,9 @@ msgid "Node name"
 msgstr "Новое имя"
 
 #: src/views/preference/BasicSettings.vue:23
+#, fuzzy
 msgid "Node Secret"
-msgstr ""
+msgstr "Jwt секрет"
 
 #: src/views/certificate/CertificateList/certColumns.tsx:91
 msgid "Not After"
@@ -1744,6 +1825,10 @@ msgstr "OpenAI"
 msgid "Or"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr "Оригинальное имя"
@@ -1776,8 +1861,9 @@ msgid "Params"
 msgstr "Параметры"
 
 #: src/views/preference/components/Passkey.vue:59
+#, fuzzy
 msgid "Passkey"
-msgstr ""
+msgstr "Добавить ключ доступа"
 
 #: src/views/preference/components/Passkey.vue:62
 msgid ""
@@ -1961,8 +2047,9 @@ msgid "Provider"
 msgstr "Провайдер"
 
 #: src/views/certificate/ACMEUser.vue:51
+#, fuzzy
 msgid "Proxy"
-msgstr ""
+msgstr "API Прокси"
 
 #: src/views/preference/BasicSettings.vue:63
 msgid "Public Security Number"
@@ -1992,6 +2079,21 @@ msgid "Recovered Successfully"
 msgstr "Восстановлено успешно"
 
 #: src/components/TwoFA/Authorization.vue:89
+<<<<<<< HEAD
+msgid "Recovery"
+msgstr "Восстановление"
+
+#: src/views/preference/components/RecoveryCodes.vue:68
+#, fuzzy
+msgid "Recovery Codes"
+msgstr "Код восстановления"
+
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+msgstr ""
+=======
 #: src/views/preference/components/TOTP.vue:156
 msgid "Recovery"
 msgstr "Восстановление"
@@ -2003,6 +2105,7 @@ msgstr "Код восстановления"
 #: src/views/preference/components/TOTP.vue:91
 msgid "Recovery Code:"
 msgstr "Код восстановления:"
+>>>>>>> remotes/origin/dev
 
 #: src/views/preference/CertSettings.vue:37
 msgid "Recursive Nameservers"
@@ -2069,7 +2172,7 @@ msgstr "Перезагружается"
 msgid "Reloading nginx"
 msgstr "Перезагружается nginx"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr "Удалить"
 
@@ -2083,7 +2186,7 @@ msgstr "Удалить сайт: %{site_name}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "Продублированно %{conf_name} в %{node_name}"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 msgid "Remove successfully"
 msgstr "Удалено успешно"
@@ -2104,8 +2207,8 @@ msgstr "Переименовать"
 msgid ""
 "Rename %{orig_path} to %{new_path} on %{env_name} failed, response: %{resp}"
 msgstr ""
-"Переименование %{orig_path} в %{new_path} на %{env_name} не удалось, ответ: %"
-"{resp}"
+"Переименование %{orig_path} в %{new_path} на %{env_name} не удалось, ответ: "
+"%{resp}"
 
 #: src/components/Notification/config.ts:20
 msgid "Rename %{orig_path} to %{new_path} on %{env_name} successfully"
@@ -2179,7 +2282,11 @@ msgstr "Запрос с неправильными параметрами"
 msgid "Reset"
 msgstr "Сброс"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:93
+=======
 #: src/views/preference/components/TOTP.vue:111
+>>>>>>> remotes/origin/dev
 msgid "Reset 2FA"
 msgstr "Сброс 2FA"
 
@@ -2191,15 +2298,15 @@ msgstr "Перезапуск"
 msgid "Restarting"
 msgstr "Перезапускается"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2250,8 +2357,8 @@ msgstr "Переименование удаленной конфигурации
 #, fuzzy
 msgid "Save site %{site} to %{node} error, response: %{resp}"
 msgstr ""
-"Синхронизация сертификата %{cert_name} с %{env_name} не удалась, ответ: %"
-"{resp}"
+"Синхронизация сертификата %{cert_name} с %{env_name} не удалась, ответ: "
+"%{resp}"
 
 #: src/components/Notification/config.ts:35
 #, fuzzy
@@ -2272,7 +2379,11 @@ msgstr "Сохранено успешно"
 msgid "Saved successfully"
 msgstr "Успешно сохранено"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:69
+=======
 #: src/views/preference/components/TOTP.vue:72
+>>>>>>> remotes/origin/dev
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 "Отсканируйте QR-код с помощью мобильного телефона, чтобы добавить учетную "
@@ -2282,7 +2393,11 @@ msgstr ""
 msgid "SDK"
 msgstr "SDK"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:109
+=======
 #: src/views/preference/components/TOTP.vue:128
+>>>>>>> remotes/origin/dev
 msgid "Secret has been copied"
 msgstr ""
 
@@ -2342,14 +2457,14 @@ msgstr "Настройка провайдера проверки HTTP01"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
@@ -2357,16 +2472,18 @@ msgid "Show"
 msgstr "Показать"
 
 #: src/views/other/Login.vue:240
+#, fuzzy
 msgid "Sign in with a passkey"
-msgstr ""
+msgstr "Аутентификация с помощью ключа доступа"
 
 #: src/views/site/ngx_conf/directive/DirectiveAdd.vue:65
 msgid "Single Directive"
 msgstr "Одиночная Директива"
 
 #: src/routes/index.ts:71 src/views/site/site_category/SiteCategory.vue:10
+#, fuzzy
 msgid "Site Categories"
-msgstr ""
+msgstr "Категория"
 
 #: src/constants/errors/site.ts:4
 #, fuzzy
@@ -2392,12 +2509,14 @@ msgid "Sites List"
 msgstr "Список сайтов"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "Каталог"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "Каталог"
 
 #: src/views/certificate/CertificateEditor.vue:211
 msgid "SSL Certificate Content"
@@ -2447,8 +2566,9 @@ msgid "Streams Directory"
 msgstr "Каталог"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "Каталог"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2499,8 +2619,8 @@ msgstr ""
 #: src/components/Notification/cert.ts:14
 msgid "Sync Certificate %{cert_name} to %{env_name} failed, response: %{resp}"
 msgstr ""
-"Синхронизация сертификата %{cert_name} с %{env_name} не удалась, ответ: %"
-"{resp}"
+"Синхронизация сертификата %{cert_name} с %{env_name} не удалась, ответ: "
+"%{resp}"
 
 #: src/components/Notification/cert.ts:4
 msgid "Sync Certificate %{cert_name} to %{env_name} successfully"
@@ -2517,8 +2637,8 @@ msgstr "Сертификат успешно синхронизирован"
 #: src/components/Notification/config.ts:14
 msgid "Sync config %{config_name} to %{env_name} failed, response: %{resp}"
 msgstr ""
-"Синхронизация конфигурации %{config_name} с %{env_name} не удалась, ответ: %"
-"{resp}"
+"Синхронизация конфигурации %{config_name} с %{env_name} не удалась, ответ: "
+"%{resp}"
 
 #: src/components/Notification/config.ts:4
 msgid "Sync Config %{config_name} to %{env_name} successfully"
@@ -2647,6 +2767,8 @@ msgstr ""
 "Имя сервера должно содержать только буквы, юникод, цифры, дефисы, тире и "
 "точки."
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:90
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
@@ -2654,6 +2776,7 @@ msgstr ""
 "Код восстановления отображается только один раз, пожалуйста, сохраните его в "
 "безопасном месте."
 
+>>>>>>> remotes/origin/dev
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2686,6 +2809,13 @@ msgstr "URL недействителен."
 msgid "The username or password is incorrect"
 msgstr "Имя пользователя или пароль неверны"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr "Этот элемент автосертификата недействителен, удалите его.."
@@ -2731,14 +2861,14 @@ msgstr ""
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
-"Это обновит или переустановит интерфейс Nginx на %{nodeNames} до версии %"
-"{version}."
+"Это обновит или переустановит интерфейс Nginx на %{nodeNames} до версии "
+"%{version}."
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2748,7 +2878,11 @@ msgstr "Советы"
 msgid "Title"
 msgstr "Заголовок"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:68
+=======
 #: src/views/preference/components/TOTP.vue:71
+>>>>>>> remotes/origin/dev
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2792,11 +2926,19 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:66
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:67
+=======
 #: src/views/preference/components/TOTP.vue:69
 msgid "TOTP"
 msgstr ""
 
 #: src/views/preference/components/TOTP.vue:70
+>>>>>>> remotes/origin/dev
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2924,6 +3066,16 @@ msgstr "Подробно"
 msgid "View Mode"
 msgstr "Простой режим"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "Код восстановления"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "Просмотр"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2947,7 +3099,7 @@ msgstr ""
 "Мы удалим конфигурацию HTTPChallenge из этого файла и перезагрузим Nginx. Вы "
 "уверены, что хотите продолжить?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -2968,6 +3120,12 @@ msgid ""
 "site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -2981,7 +3139,7 @@ msgstr "Запись закрытого ключа сертификата на 
 msgid "Writing certificate to disk"
 msgstr "Запись сертификата на диск"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3002,9 +3160,46 @@ msgid ""
 "passkey."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
+#, fuzzy
 msgid "Your passkeys"
-msgstr ""
+msgstr "Добавить ключ доступа"
+
+#~ msgid ""
+#~ "If you lose your mobile phone, you can use the recovery code to reset "
+#~ "your 2FA."
+#~ msgstr ""
+#~ "Если вы потеряете свой мобильный телефон, вы можете использовать код "
+#~ "восстановления для сброса 2FA."
+
+#~ msgid "Recovery Code:"
+#~ msgstr "Код восстановления:"
+
+#~ msgid ""
+#~ "The recovery code is only displayed once, please save it in a safe place."
+#~ msgstr ""
+#~ "Код восстановления отображается только один раз, пожалуйста, сохраните "
+#~ "его в безопасном месте."
 
 #~ msgid "Directory"
 #~ msgstr "Каталог"

+ 226 - 31
app/src/language/tr_TR/app.po

@@ -16,7 +16,7 @@ msgstr ""
 msgid "2FA"
 msgstr "İki aşamalı kimlik doğrulaması(2FA)"
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr "2FA Ayarları"
 
@@ -39,7 +39,7 @@ msgstr "ACME Kullanıcısı"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -140,7 +140,7 @@ msgstr "Başarıyla kopyalandı"
 msgid "Arch"
 msgstr "Mimari"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Bu yasaklı IP'yi hemen sileceğinizden emin misiniz?"
 
@@ -148,6 +148,16 @@ msgstr "Bu yasaklı IP'yi hemen sileceğinizden emin misiniz?"
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "Bu geçiş anahtarını hemen silmek istediğinizden emin misiniz?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "Bu öğeyi kurtarmak istediğinizden emin misiniz?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "Silmek istediğine emin misin?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -204,7 +214,7 @@ msgstr "Asistan"
 msgid "Attempt to fix"
 msgstr "Girişimler"
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr "Girişimler"
 
@@ -216,7 +226,7 @@ msgstr "Kimlik Doğrulama"
 msgid "Authenticate with a passkey"
 msgstr "Geçiş anahtarıyla kimlik doğrulama"
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr "Kimlik Doğrulama Ayarları"
 
@@ -253,15 +263,15 @@ msgstr "Ana Sayfaya Dön"
 msgid "Back to list"
 msgstr "Listeye geri dön"
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "Yasaklama Eşiği Süresi (Dakika)"
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr "Yasaklı IP'ler"
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr "Şu Zamana Kadar Yasaklı"
 
@@ -315,10 +325,13 @@ msgstr "CA Dizini"
 msgid "CADir"
 msgstr "CADizini"
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:129
 msgid "Can't scan? Use text key binding"
 msgstr "Tarayamıyor musunuz? Metin anahtar bağlamasını kullanın"
 
+>>>>>>> remotes/origin/dev
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -453,6 +466,10 @@ msgstr "Temizle"
 msgid "Cleared successfully"
 msgstr "Başarıyla temizlendi"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr "Komut"
@@ -500,6 +517,7 @@ msgstr "İçerik"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr "Kopyalandı"
 
@@ -507,6 +525,11 @@ msgstr "Kopyalandı"
 msgid "Copy"
 msgstr "Kopya"
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+#, fuzzy
+msgid "Copy Codes"
+msgstr "Kurtarma Kodu"
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "Çekirdek Yükseltme"
@@ -557,11 +580,19 @@ msgstr "Kimlik bilgisi"
 msgid "Credentials"
 msgstr "Kimlik bilgileri"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:72
+msgid "Current account is enabled TOTP."
+msgstr "Mevcut hesap için TOTP etkinleştirildi."
+
+#: src/views/preference/components/TOTP.vue:70
+=======
 #: src/views/preference/components/TOTP.vue:77
 msgid "Current account is enabled TOTP."
 msgstr "Mevcut hesap için TOTP etkinleştirildi."
 
 #: src/views/preference/components/TOTP.vue:74
+>>>>>>> remotes/origin/dev
 msgid "Current account is not enabled TOTP."
 msgstr "Mevcut hesap için TOTP etkin değil."
 
@@ -914,7 +945,11 @@ msgstr ""
 "%{conf_name} yapılandırmasını %{node_name} düğümünde etkinleştirme başarılı "
 "oldu"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:45
+=======
 #: src/views/preference/components/TOTP.vue:38
+>>>>>>> remotes/origin/dev
 msgid "Enable 2FA successfully"
 msgstr "2FA'yı başarıyla etkinleştirildi"
 
@@ -958,7 +993,11 @@ msgstr "Başarıyla etkinleştirildi"
 msgid "Enable TLS"
 msgstr "TLS'yi Etkinleştir"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:81
+=======
 #: src/views/preference/components/TOTP.vue:103
+>>>>>>> remotes/origin/dev
 msgid "Enable TOTP"
 msgstr "TOTP'yi Etkinleştir"
 
@@ -1054,8 +1093,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "Etkinleştirilemedi %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1082,6 +1122,10 @@ msgstr "Filtre"
 msgid "Finished"
 msgstr "Bitmiş"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1119,6 +1163,22 @@ msgstr "Genel Sertifika"
 msgid "Generate"
 msgstr "Oluştur"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate New Recovery Codes"
+msgstr "Kurtarma Kodu"
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "Kurtarma Kodu"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "Başarıyla Kurtarıldı"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "Hesap kaydı için özel anahtar oluşturuluyor"
@@ -1167,7 +1227,7 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr "Boş bırakılırsa, varsayılan CA Dir kullanılır."
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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."
@@ -1176,6 +1236,8 @@ msgstr ""
 "yasaklama eşiği dakikaları içinde maksimum deneme sayısına ulaşırsa, IP "
 "adresi belirli bir süre için yasaklanacaktır."
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:89
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
@@ -1184,6 +1246,7 @@ msgstr ""
 "Cep telefonunuzu kaybederseniz, 2FA'nızı sıfırlamak için kurtarma kodunu "
 "kullanabilirsiniz."
 
+>>>>>>> remotes/origin/dev
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1219,12 +1282,19 @@ msgstr "İlk çekirdek yükseltici hatası"
 msgid "Initialing core upgrader"
 msgstr "Çekirdek yükseltici başlatılıyor"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:119
+=======
 #: src/views/preference/components/TOTP.vue:136
+>>>>>>> remotes/origin/dev
 msgid "Input the code from the app:"
 msgstr "Uygulamadan kodu girin:"
 
 #: src/components/TwoFA/Authorization.vue:82
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:149
+>>>>>>> remotes/origin/dev
 msgid "Input the recovery code:"
 msgstr "Kurtarma kodunu girin:"
 
@@ -1267,12 +1337,16 @@ msgstr "Geçersiz parola veya kurtarma kodu"
 msgid "Invalid recovery code"
 msgstr "Geçersiz 2FA veya kurtarma kodu"
 
+<<<<<<< HEAD
+#: src/views/preference/AuthSettings.vue:18
+=======
 #: src/constants/errors/middleware.ts:2
 #, fuzzy
 msgid "Invalid request format"
 msgstr "Geçersiz 2FA veya kurtarma kodu"
 
 #: src/views/preference/AuthSettings.vue:14
+>>>>>>> remotes/origin/dev
 msgid "IP"
 msgstr "IP"
 
@@ -1296,6 +1370,12 @@ msgstr "Düzenleyen: %{issuer}"
 msgid "Jwt Secret"
 msgstr "Jwt Secret"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1444,7 +1524,7 @@ msgstr "Kullanıcıları Yönet"
 msgid "Managed Certificate"
 msgstr "Yönetilen Sertifika"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 #, fuzzy
 msgid "Max Attempts"
 msgstr "Maksimum Deneme"
@@ -1626,8 +1706,9 @@ msgid "Nginx Log"
 msgstr "Nginx Günlüğü"
 
 #: src/views/preference/NginxSettings.vue:18
+#, fuzzy
 msgid "Nginx Log Directory Whitelist"
-msgstr ""
+msgstr "Nginx Yapılandırma Ayrıştırma Hatası"
 
 #: src/views/preference/NginxSettings.vue:27
 #, fuzzy
@@ -1635,8 +1716,9 @@ msgid "Nginx PID Path"
 msgstr "Nginx Hata Günlüğü Yolu"
 
 #: src/views/preference/NginxSettings.vue:30
+#, fuzzy
 msgid "Nginx Reload Command"
-msgstr ""
+msgstr "Terminal Başlatma Komutu"
 
 #: src/components/NginxControl/NginxControl.vue:26
 #, fuzzy
@@ -1661,7 +1743,7 @@ msgstr "Nginx başarıyla yeniden başlatıldı"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1798,6 +1880,10 @@ msgstr "OpenAI"
 msgid "Or"
 msgstr "Veya"
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 #, fuzzy
 msgid "Original name"
@@ -2089,20 +2175,35 @@ msgid "Recovered Successfully"
 msgstr "Başarıyla Kurtarıldı"
 
 #: src/components/TwoFA/Authorization.vue:89
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:156
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Recovery"
 msgstr "Kurtarma"
 
+<<<<<<< HEAD
+#: src/views/preference/components/RecoveryCodes.vue:68
+=======
 #: src/views/preference/components/TOTP.vue:82
+>>>>>>> remotes/origin/dev
 #, fuzzy
-msgid "Recovery Code"
+msgid "Recovery Codes"
 msgstr "Kurtarma Kodu"
 
+<<<<<<< HEAD
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+msgstr ""
+=======
 #: src/views/preference/components/TOTP.vue:91
 #, fuzzy
 msgid "Recovery Code:"
 msgstr "Kurtarma Kodu:"
+>>>>>>> remotes/origin/dev
 
 #: src/views/preference/CertSettings.vue:37
 #, fuzzy
@@ -2184,7 +2285,7 @@ msgstr "Yeniden Yükleme"
 msgid "Reloading nginx"
 msgstr "Nginx'i yeniden yükleme"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 #, fuzzy
 msgid "Remove"
 msgstr "Kaldır"
@@ -2199,7 +2300,7 @@ msgstr "Siteyi sil: %{site_name}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "%{conf_name} başarıyla %{node_name} düğümüne kopyalandı"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 #, fuzzy
 msgid "Remove successfully"
@@ -2312,7 +2413,11 @@ msgstr "Yanlış parametrelerle talep edildi"
 msgid "Reset"
 msgstr "Sıfırla"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:93
+=======
 #: src/views/preference/components/TOTP.vue:111
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "2FA'yı Sıfırla"
@@ -2327,15 +2432,15 @@ msgstr "Yeniden başlat"
 msgid "Restarting"
 msgstr "Yeniden Başlatma"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2415,7 +2520,11 @@ msgstr "Başarıyla kaydedin"
 msgid "Saved successfully"
 msgstr "Başarıyla Kaydedildi"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:69
+=======
 #: src/views/preference/components/TOTP.vue:72
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "Hesabı uygulamaya eklemek için QR kodunu cep telefonunuzla tarayın."
@@ -2425,7 +2534,11 @@ msgstr "Hesabı uygulamaya eklemek için QR kodunu cep telefonunuzla tarayın."
 msgid "SDK"
 msgstr "SDK"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:109
+=======
 #: src/views/preference/components/TOTP.vue:128
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Secret has been copied"
 msgstr "Sır kopyalandı"
@@ -2495,14 +2608,14 @@ msgstr "HTTP01 meydan okuma sağlayıcısını ayarlama"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
@@ -2550,12 +2663,14 @@ msgid "Sites List"
 msgstr "Site Listesi"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "Dizin"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "Dizin"
 
 #: src/views/certificate/CertificateEditor.vue:211
 #, fuzzy
@@ -2614,8 +2729,9 @@ msgid "Streams Directory"
 msgstr "Dizin"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "Dizin"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2838,6 +2954,8 @@ msgstr ""
 "Sunucu adı yalnızca harf, unicode, sayı, kısa çizgi, tire ve nokta "
 "içermelidir."
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:90
 #, fuzzy
 msgid ""
@@ -2846,6 +2964,7 @@ msgstr ""
 "Kurtarma kodu yalnızca bir kez görüntülenir, lütfen güvenli bir yere "
 "kaydedin."
 
+>>>>>>> remotes/origin/dev
 #: src/views/dashboard/Environments.vue:148
 #, fuzzy
 msgid ""
@@ -2883,6 +3002,13 @@ msgstr "URL geçersiz."
 msgid "The username or password is incorrect"
 msgstr "Kullanıcı adı veya şifre yanlış"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 #, fuzzy
 msgid "This Auto Cert item is invalid, please remove it."
@@ -2935,11 +3061,11 @@ msgstr ""
 "Bu, %{nodeNames} üzerindeki Nginx kullanıcı arayüzünü %{version}'e "
 "yükseltecek veya yeniden yükleyecektir."
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 #, fuzzy
@@ -2951,7 +3077,11 @@ msgstr "İpuçları"
 msgid "Title"
 msgstr "Başlık"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:68
+=======
 #: src/views/preference/components/TOTP.vue:71
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
@@ -3006,12 +3136,20 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:66
+=======
 #: src/views/preference/components/TOTP.vue:69
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "TOTP"
 msgstr "TOTP"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:67
+=======
 #: src/views/preference/components/TOTP.vue:70
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
@@ -3162,6 +3300,16 @@ msgstr "Detayları göster"
 msgid "View Mode"
 msgstr "Görünüm Modu"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "Kurtarma Kodu"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "Görünüm"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -3188,7 +3336,7 @@ msgstr ""
 "HTTPChallenge yapılandırmasını bu dosyadan kaldıracağız ve Nginx'i yeniden "
 "yükleyeceğiz. Devam etmek istediğinizden emin misiniz?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -3213,6 +3361,12 @@ msgid ""
 "site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 #, fuzzy
@@ -3229,7 +3383,7 @@ msgstr "Sertifika özel anahtarını diske yazma"
 msgid "Writing certificate to disk"
 msgstr "Sertifikayı diske yazma"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3256,11 +3410,52 @@ msgstr ""
 "Webauthn ayarlarını yapılandırmadınız, bu nedenle bir geçiş anahtarı "
 "ekleyemezsiniz."
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 #, fuzzy
 msgid "Your passkeys"
 msgstr "Geçiş anahtarlarınız"
 
+#~ msgid ""
+#~ "If you lose your mobile phone, you can use the recovery code to reset "
+#~ "your 2FA."
+#~ msgstr ""
+#~ "Cep telefonunuzu kaybederseniz, 2FA'nızı sıfırlamak için kurtarma kodunu "
+#~ "kullanabilirsiniz."
+
+#, fuzzy
+#~ msgid "Recovery Code:"
+#~ msgstr "Kurtarma Kodu:"
+
+#, fuzzy
+#~ msgid ""
+#~ "The recovery code is only displayed once, please save it in a safe place."
+#~ msgstr ""
+#~ "Kurtarma kodu yalnızca bir kez görüntülenir, lütfen güvenli bir yere "
+#~ "kaydedin."
+
+#~ msgid "Can't scan? Use text key binding"
+#~ msgstr "Tarayamıyor musunuz? Metin anahtar bağlamasını kullanın"
+
 #~ msgid "Directory"
 #~ msgstr "Dizin"
 

+ 232 - 46
app/src/language/vi_VN/app.po

@@ -13,7 +13,7 @@ msgstr ""
 msgid "2FA"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr ""
 
@@ -37,7 +37,7 @@ msgstr "Người dùng"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -112,16 +112,18 @@ msgid "API Base Url"
 msgstr ""
 
 #: src/views/certificate/DNSChallenge.vue:83
+#, fuzzy
 msgid "API Document"
-msgstr ""
+msgstr "Bình luận"
 
 #: src/views/preference/OpenAISettings.vue:57
 msgid "API Proxy"
 msgstr ""
 
 #: src/views/preference/OpenAISettings.vue:69
+#, fuzzy
 msgid "API Token"
-msgstr ""
+msgstr "Loại"
 
 #: src/views/preference/OpenAISettings.vue:78
 #, fuzzy
@@ -141,7 +143,7 @@ msgstr "Nhân bản thành công"
 msgid "Arch"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 #, fuzzy
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Bạn chắc chắn muốn xóa nó "
@@ -151,6 +153,16 @@ msgstr "Bạn chắc chắn muốn xóa nó "
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "Bạn chắc chắn muốn xóa nó "
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+#, fuzzy
+msgid "Are you sure to generate new recovery codes?"
+msgstr "Bạn chắc chắn muốn xoá directive này ?"
+
+#: src/views/preference/components/TOTP.vue:85
+#, fuzzy
+msgid "Are you sure to reset 2FA?"
+msgstr "Bạn chắc chắn muốn xóa nó "
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 #, fuzzy
 msgid "Are you sure you want to apply to all selected?"
@@ -214,7 +226,7 @@ msgstr "Trợ lý"
 msgid "Attempt to fix"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr ""
 
@@ -227,7 +239,7 @@ msgstr "Tác giả"
 msgid "Authenticate with a passkey"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr ""
 
@@ -265,15 +277,15 @@ msgstr "Quay lại"
 msgid "Back to list"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr ""
 
@@ -330,10 +342,13 @@ msgstr ""
 msgid "CADir"
 msgstr ""
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:129
 msgid "Can't scan? Use text key binding"
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -476,6 +491,10 @@ msgstr "Xoá"
 msgid "Cleared successfully"
 msgstr "Đã xóa thành công"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr ""
+
 #: src/views/preference/LogrotateSettings.vue:22
 #, fuzzy
 msgid "Command"
@@ -525,6 +544,7 @@ msgstr "Nội dung"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr ""
 
@@ -532,6 +552,10 @@ msgstr ""
 msgid "Copy"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+msgid "Copy Codes"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "Cập nhật core"
@@ -586,11 +610,19 @@ msgstr "Chứng chỉ"
 msgid "Credentials"
 msgstr "Chứng chỉ"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:72
+msgid "Current account is enabled TOTP."
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:70
+=======
 #: src/views/preference/components/TOTP.vue:77
 msgid "Current account is enabled TOTP."
 msgstr ""
 
 #: src/views/preference/components/TOTP.vue:74
+>>>>>>> remotes/origin/dev
 msgid "Current account is not enabled TOTP."
 msgstr ""
 
@@ -853,8 +885,8 @@ msgstr "Tên miền đã được tạo"
 #: src/views/certificate/CertificateEditor.vue:112
 msgid "Domains list is empty, try to reopen Auto Cert for %{config}"
 msgstr ""
-"Danh sách tên miền rỗng, hãy thử mở lại chức năng Tạo chứng chỉ tự động cho %"
-"{config}"
+"Danh sách tên miền rỗng, hãy thử mở lại chức năng Tạo chứng chỉ tự động cho "
+"%{config}"
 
 #: src/language/constants.ts:26
 msgid "Download latest release error"
@@ -949,7 +981,11 @@ 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}"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:45
+=======
 #: src/views/preference/components/TOTP.vue:38
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgstr "Đã bật"
@@ -991,7 +1027,11 @@ msgstr "Đã bật"
 msgid "Enable TLS"
 msgstr "Bật TLS"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:81
+=======
 #: src/views/preference/components/TOTP.vue:103
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Enable TOTP"
 msgstr "Bật TLS"
@@ -1092,8 +1132,9 @@ msgid "Failed to parse nginx.conf"
 msgstr ""
 
 #: src/constants/errors/self_check.ts:3
+#, fuzzy
 msgid "Failed to read nginx.conf"
-msgstr ""
+msgstr "Không thể bật %{msg}"
 
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
@@ -1120,6 +1161,10 @@ msgstr "Lọc"
 msgid "Finished"
 msgstr "Đã hoàn thành"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr ""
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1159,6 +1204,21 @@ msgstr "Chứng chỉ chung"
 msgid "Generate"
 msgstr "Tạo"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate New Recovery Codes"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+#, fuzzy
+msgid "Generate Recovery Codes"
+msgstr "Hợp lệ"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+#, fuzzy
+msgid "Generate recovery codes successfully"
+msgstr "Xoá thành công"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "Tạo khóa riêng để đăng ký tài khoản"
@@ -1185,8 +1245,9 @@ msgid "Home"
 msgstr "Trang chủ"
 
 #: src/views/preference/CertSettings.vue:12
+#, fuzzy
 msgid "HTTP Challenge Port"
-msgstr ""
+msgstr "Phương pháp xác thực"
 
 #: src/views/preference/BasicSettings.vue:11
 msgid "HTTP Host"
@@ -1208,18 +1269,21 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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 ""
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:89
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr ""
@@ -1253,12 +1317,19 @@ 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"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:119
+=======
 #: src/views/preference/components/TOTP.vue:136
+>>>>>>> remotes/origin/dev
 msgid "Input the code from the app:"
 msgstr ""
 
 #: src/components/TwoFA/Authorization.vue:82
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:149
+>>>>>>> remotes/origin/dev
 msgid "Input the recovery code:"
 msgstr ""
 
@@ -1287,8 +1358,9 @@ msgid "Invalid filename"
 msgstr "E-mail không chính xác!"
 
 #: src/views/config/components/Mkdir.vue:57
+#, fuzzy
 msgid "Invalid folder name"
-msgstr ""
+msgstr "E-mail không chính xác!"
 
 #: src/constants/errors/user.ts:4
 #, fuzzy
@@ -1300,14 +1372,19 @@ msgid "Invalid passcode or recovery code"
 msgstr ""
 
 #: src/constants/errors/user.ts:5
+#, fuzzy
 msgid "Invalid recovery code"
-msgstr ""
+msgstr "Hợp lệ"
 
+<<<<<<< HEAD
+#: src/views/preference/AuthSettings.vue:18
+=======
 #: src/constants/errors/middleware.ts:2
 msgid "Invalid request format"
 msgstr ""
 
 #: src/views/preference/AuthSettings.vue:14
+>>>>>>> remotes/origin/dev
 msgid "IP"
 msgstr ""
 
@@ -1334,6 +1411,12 @@ msgstr ""
 msgid "Jwt Secret"
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 #, fuzzy
@@ -1478,10 +1561,11 @@ msgid "Manage Users"
 msgstr "Người dùng"
 
 #: src/views/certificate/CertificateList/certColumns.tsx:31
+#, fuzzy
 msgid "Managed Certificate"
-msgstr ""
+msgstr "Thay đổi chứng chỉ"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr ""
 
@@ -1641,12 +1725,14 @@ msgid "Nginx is not running"
 msgstr ""
 
 #: src/routes/index.ts:202 src/views/nginx_log/NginxLog.vue:148
+#, fuzzy
 msgid "Nginx Log"
-msgstr ""
+msgstr "Vị trí lưu log lỗi (Error log) của Nginx"
 
 #: src/views/preference/NginxSettings.vue:18
+#, fuzzy
 msgid "Nginx Log Directory Whitelist"
-msgstr ""
+msgstr "Lỗi phân tích cú pháp cấu hình Nginx"
 
 #: src/views/preference/NginxSettings.vue:27
 #, fuzzy
@@ -1679,7 +1765,7 @@ msgstr "Restart Nginx thành công"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1694,8 +1780,9 @@ msgid "Node name"
 msgstr "Username"
 
 #: src/views/preference/BasicSettings.vue:23
+#, fuzzy
 msgid "Node Secret"
-msgstr ""
+msgstr "Username"
 
 #: src/views/certificate/CertificateList/certColumns.tsx:91
 msgid "Not After"
@@ -1798,6 +1885,10 @@ msgstr ""
 msgid "Or"
 msgstr ""
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr ""
@@ -2042,6 +2133,20 @@ msgid "Recovered Successfully"
 msgstr "Xoá thành công"
 
 #: src/components/TwoFA/Authorization.vue:89
+<<<<<<< HEAD
+msgid "Recovery"
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:68
+#, fuzzy
+msgid "Recovery Codes"
+msgstr "Hợp lệ"
+
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+=======
 #: src/views/preference/components/TOTP.vue:156
 msgid "Recovery"
 msgstr ""
@@ -2052,6 +2157,7 @@ msgstr ""
 
 #: src/views/preference/components/TOTP.vue:91
 msgid "Recovery Code:"
+>>>>>>> remotes/origin/dev
 msgstr ""
 
 #: src/views/preference/CertSettings.vue:37
@@ -2124,7 +2230,7 @@ msgstr "Đang tải lại"
 msgid "Reloading nginx"
 msgstr "Tải lại nginx"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr ""
 
@@ -2138,7 +2244,7 @@ msgstr "Xoá trang web: %{site_name}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "Nhân bản %{conf_name} thành %{node_name} thành công"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 #, fuzzy
 msgid "Remove successfully"
@@ -2244,7 +2350,11 @@ msgstr "Yêu cầu có chứa tham số sai"
 msgid "Reset"
 msgstr "Đặt lại"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:93
+=======
 #: src/views/preference/components/TOTP.vue:111
+>>>>>>> remotes/origin/dev
 #, fuzzy
 msgid "Reset 2FA"
 msgstr "Đặt lại"
@@ -2257,15 +2367,15 @@ msgstr "Khởi động lại"
 msgid "Restarting"
 msgstr "Đang khởi động lại"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr ""
 
@@ -2338,7 +2448,11 @@ msgstr "Lưu thành công"
 msgid "Saved successfully"
 msgstr "Lưu thành công"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:69
+=======
 #: src/views/preference/components/TOTP.vue:72
+>>>>>>> remotes/origin/dev
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 
@@ -2346,7 +2460,11 @@ msgstr ""
 msgid "SDK"
 msgstr ""
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:109
+=======
 #: src/views/preference/components/TOTP.vue:128
+>>>>>>> remotes/origin/dev
 msgid "Secret has been copied"
 msgstr ""
 
@@ -2405,14 +2523,14 @@ msgstr "Sử dụng HTTP01 để xác thực SSL"
 
 #: src/constants/errors/nginx_log.ts:8
 msgid ""
-"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/constants/errors/nginx_log.ts:7
 msgid ""
-"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://"
-"nginxui.com/guide/config-nginx.html for more information"
+"Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui."
+"com/guide/config-nginx.html for more information"
 msgstr ""
 
 #: src/components/SensitiveString/SensitiveString.vue:40
@@ -2456,32 +2574,39 @@ msgid "Sites List"
 msgstr "Danh sách Website"
 
 #: src/constants/errors/self_check.ts:10
+#, fuzzy
 msgid "Sites-available directory not exist"
-msgstr ""
+msgstr "Thư mục"
 
 #: src/constants/errors/self_check.ts:11
+#, fuzzy
 msgid "Sites-enabled directory not exist"
-msgstr ""
+msgstr "Thư mục"
 
 #: src/views/certificate/CertificateEditor.vue:211
+#, fuzzy
 msgid "SSL Certificate Content"
-msgstr ""
+msgstr "Trạng thái chứng chỉ"
 
 #: src/views/certificate/CertificateEditor.vue:224
+#, fuzzy
 msgid "SSL Certificate Key Content"
-msgstr ""
+msgstr "Trạng thái chứng chỉ"
 
 #: src/views/certificate/CertificateEditor.vue:190
+#, fuzzy
 msgid "SSL Certificate Key Path"
-msgstr ""
+msgstr "Trạng thái chứng chỉ"
 
 #: src/views/certificate/CertificateEditor.vue:175
+#, fuzzy
 msgid "SSL Certificate Path"
-msgstr ""
+msgstr "Trạng thái chứng chỉ"
 
 #: src/views/other/Login.vue:198
+#, fuzzy
 msgid "SSO Login"
-msgstr ""
+msgstr "Đăng nhập"
 
 #: src/views/environment/BatchUpgrader.vue:164
 #: src/views/environment/BatchUpgrader.vue:214 src/views/system/Upgrade.vue:191
@@ -2512,8 +2637,9 @@ msgid "Streams Directory"
 msgstr "Thư mục"
 
 #: src/constants/errors/self_check.ts:12
+#, fuzzy
 msgid "Streams-available directory not exist"
-msgstr ""
+msgstr "Thư mục"
 
 #: src/constants/errors/self_check.ts:13
 #, fuzzy
@@ -2612,8 +2738,9 @@ msgid "Sync strategy"
 msgstr "Gia hạn chứng chỉ SSL"
 
 #: src/views/certificate/CertificateEditor.vue:204
+#, fuzzy
 msgid "Sync to"
-msgstr ""
+msgstr "Gia hạn chứng chỉ SSL"
 
 #: src/views/site/site_edit/RightSettings.vue:110
 msgid "Synchronization"
@@ -2703,11 +2830,14 @@ msgid ""
 "hyphens, dashes, colons, and dots."
 msgstr ""
 
+<<<<<<< HEAD
+=======
 #: src/views/preference/components/TOTP.vue:90
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 
+>>>>>>> remotes/origin/dev
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2737,6 +2867,13 @@ msgstr ""
 msgid "The username or password is incorrect"
 msgstr "Tên người dùng hoặc mật khẩu không chính xác"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr "Mục Chứng chỉ tự động này không hợp lệ, vui lòng xóa nó"
@@ -2776,15 +2913,16 @@ msgid "This value is already taken"
 msgstr ""
 
 #: src/views/environment/BatchUpgrader.vue:182
+#, fuzzy
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
-msgstr ""
+msgstr "Nhân bản %{conf_name} thành %{node_name} thành công"
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr ""
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2794,7 +2932,11 @@ msgstr ""
 msgid "Title"
 msgstr "Tiêu đề"
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:68
+=======
 #: src/views/preference/components/TOTP.vue:71
+>>>>>>> remotes/origin/dev
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2836,11 +2978,19 @@ msgid_plural "Total %{total} items"
 msgstr[0] ""
 msgstr[1] ""
 
+<<<<<<< HEAD
+#: src/views/preference/components/TOTP.vue:66
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:67
+=======
 #: src/views/preference/components/TOTP.vue:69
 msgid "TOTP"
 msgstr ""
 
 #: src/views/preference/components/TOTP.vue:70
+>>>>>>> remotes/origin/dev
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2973,6 +3123,16 @@ msgstr "Chi tiết"
 msgid "View Mode"
 msgstr "Cơ bản"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+#, fuzzy
+msgid "View Recovery Codes"
+msgstr "Hợp lệ"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+#, fuzzy
+msgid "Viewed"
+msgstr "Xem"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2996,7 +3156,7 @@ msgstr ""
 "Chúng tôi sẽ xóa cấu hình HTTPChallenge khỏi tệp này và tải lại Nginx. Bạn "
 "có muốn tiếp tục không?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr ""
 
@@ -3017,6 +3177,12 @@ msgid ""
 "site category and the nodes selected below will be synchronized."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -3030,7 +3196,7 @@ msgstr "Ghi Private Key vào disk"
 msgid "Writing certificate to disk"
 msgstr "Ghi chứng chỉ vào disk"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -3051,6 +3217,26 @@ msgid ""
 "passkey."
 msgstr ""
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr ""
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr ""
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr ""

+ 175 - 78
app/src/language/zh_CN/app.po

@@ -11,13 +11,13 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=1; plural=0;\n"
 "Generated-By: easygettext\n"
-"X-Generator: Poedit 3.5\n"
+"X-Generator: Poedit 3.4.1\n"
 
 #: src/views/user/userColumns.tsx:32
 msgid "2FA"
 msgstr "2FA"
 
-#: src/views/preference/AuthSettings.vue:55
+#: src/views/preference/AuthSettings.vue:70
 msgid "2FA Settings"
 msgstr "2FA 设置"
 
@@ -40,7 +40,7 @@ msgstr "ACME 用户"
 #: src/views/config/configColumns.tsx:42
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:30
 #: src/views/site/site_category/columns.ts:29
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -106,7 +106,11 @@ msgstr "然后,刷新此页面并再次点击添加 Passkey。"
 msgid "All"
 msgstr "全部"
 
-#: src/views/preference/OpenAISettings.vue:44
+#: src/language/constants.ts:57
+msgid "All Recovery Codes Have Been Used"
+msgstr "所有恢复码都已被使用"
+
+#: src/views/preference/OpenAISettings.vue:32
 msgid "API Base Url"
 msgstr "API 地址"
 
@@ -114,15 +118,15 @@ msgstr "API 地址"
 msgid "API Document"
 msgstr "API 文档"
 
-#: src/views/preference/OpenAISettings.vue:57
+#: src/views/preference/OpenAISettings.vue:46
 msgid "API Proxy"
 msgstr "API 代理"
 
-#: src/views/preference/OpenAISettings.vue:69
+#: src/views/preference/OpenAISettings.vue:58
 msgid "API Token"
 msgstr "API Token"
 
-#: src/views/preference/OpenAISettings.vue:78
+#: src/views/preference/OpenAISettings.vue:67
 msgid "API Type"
 msgstr "API 类型"
 
@@ -138,7 +142,7 @@ msgstr "批量操作应用成功"
 msgid "Arch"
 msgstr "架构"
 
-#: src/views/preference/AuthSettings.vue:134
+#: src/views/preference/AuthSettings.vue:162
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "您确定要立即删除这个被禁用的 IP 吗?"
 
@@ -146,6 +150,14 @@ msgstr "您确定要立即删除这个被禁用的 IP 吗?"
 msgid "Are you sure to delete this passkey immediately?"
 msgstr "您确定要立即删除这个 Passkey 吗?"
 
+#: src/views/preference/components/RecoveryCodes.vue:154
+msgid "Are you sure to generate new recovery codes?"
+msgstr "您确定要生成新的恢复代码吗?"
+
+#: src/views/preference/components/TOTP.vue:85
+msgid "Are you sure to reset 2FA?"
+msgstr "您确定要重设双重身份验证?"
+
 #: src/components/StdDesign/StdDataDisplay/StdBulkActions.vue:96
 msgid "Are you sure you want to apply to all selected?"
 msgstr "您确定要应用于所有选定的对象吗?"
@@ -200,7 +212,7 @@ msgstr "助手"
 msgid "Attempt to fix"
 msgstr "尝试修复"
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:21
 msgid "Attempts"
 msgstr "尝试次数"
 
@@ -208,11 +220,11 @@ msgstr "尝试次数"
 msgid "Auth"
 msgstr "认证"
 
-#: src/components/TwoFA/Authorization.vue:121
+#: src/components/TwoFA/Authorization.vue:109
 msgid "Authenticate with a passkey"
 msgstr "通过 Passkey 认证"
 
-#: src/views/preference/AuthSettings.vue:60
+#: src/views/preference/AuthSettings.vue:88
 msgid "Authentication Settings"
 msgstr "认证设置"
 
@@ -249,15 +261,15 @@ msgstr "返回首页"
 msgid "Back to list"
 msgstr "返回列表"
 
-#: src/views/preference/AuthSettings.vue:101
+#: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "禁止阈值(分钟)"
 
-#: src/views/preference/AuthSettings.vue:122
+#: src/views/preference/AuthSettings.vue:150
 msgid "Banned IPs"
 msgstr "禁止 IP 列表"
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:24
 msgid "Banned Until"
 msgstr "禁用至"
 
@@ -310,10 +322,6 @@ msgstr "CA Dir"
 msgid "CADir"
 msgstr "CADir"
 
-#: src/views/preference/components/TOTP.vue:129
-msgid "Can't scan? Use text key binding"
-msgstr "无法扫描?使用文本密钥绑定"
-
 #: src/components/ChatGPT/ChatGPT.vue:356
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:246
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:187
@@ -443,6 +451,10 @@ msgstr "清空"
 msgid "Cleared successfully"
 msgstr "清除成功"
 
+#: src/views/preference/components/TOTP.vue:110
+msgid "Click to copy"
+msgstr "点击复制"
+
 #: src/views/preference/LogrotateSettings.vue:22
 msgid "Command"
 msgstr "命令"
@@ -490,6 +502,7 @@ msgstr "内容"
 
 #: src/components/SensitiveString/SensitiveString.vue:37
 #: src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx:150
+#: src/views/preference/components/RecoveryCodes.vue:121
 msgid "Copied"
 msgstr "已拷贝"
 
@@ -497,6 +510,10 @@ msgstr "已拷贝"
 msgid "Copy"
 msgstr "拷贝"
 
+#: src/views/preference/components/RecoveryCodes.vue:121
+msgid "Copy Codes"
+msgstr "复制代码"
+
 #: src/views/system/Upgrade.vue:146
 msgid "Core Upgrade"
 msgstr "核心升级"
@@ -547,11 +564,11 @@ msgstr "DNS 凭证"
 msgid "Credentials"
 msgstr "凭证"
 
-#: src/views/preference/components/TOTP.vue:77
+#: src/views/preference/components/TOTP.vue:72
 msgid "Current account is enabled TOTP."
 msgstr "当前账户已启用 TOTP 验证。"
 
-#: src/views/preference/components/TOTP.vue:74
+#: src/views/preference/components/TOTP.vue:70
 msgid "Current account is not enabled TOTP."
 msgstr "当前用户未启用 TOTP 验证。"
 
@@ -880,7 +897,7 @@ msgstr "在%{node_name}中启用%{conf_name}失败"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功启用%{node_name}中的%{conf_name}"
 
-#: src/views/preference/components/TOTP.vue:38
+#: src/views/preference/components/TOTP.vue:45
 msgid "Enable 2FA successfully"
 msgstr "二步验证启用成功"
 
@@ -916,7 +933,7 @@ msgstr "启用成功"
 msgid "Enable TLS"
 msgstr "启用 TLS"
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:81
 msgid "Enable TOTP"
 msgstr "启用 TOTP"
 
@@ -1039,6 +1056,10 @@ msgstr "过滤"
 msgid "Finished"
 msgstr "完成"
 
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "First View"
+msgstr "首次查看"
+
 #: src/views/preference/components/AddPasskey.vue:71
 msgid ""
 "Follow the instructions in the dialog to complete the passkey registration "
@@ -1074,6 +1095,19 @@ msgstr "普通证书"
 msgid "Generate"
 msgstr "生成"
 
+#: src/views/preference/components/RecoveryCodes.vue:138
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate New Recovery Codes"
+msgstr "生成新的恢复代码"
+
+#: src/views/preference/components/RecoveryCodes.vue:161
+msgid "Generate Recovery Codes"
+msgstr "生成恢复代码"
+
+#: src/views/preference/components/RecoveryCodes.vue:32
+msgid "Generate recovery codes successfully"
+msgstr "成功生成恢复代码"
+
 #: src/language/constants.ts:7
 msgid "Generating private key for registering account"
 msgstr "正在生成私钥用于注册账户"
@@ -1122,7 +1156,7 @@ msgstr "ICP备案号"
 msgid "If left blank, the default CA Dir will be used."
 msgstr "如果留空,则使用默认 CA Dir。"
 
-#: src/views/preference/AuthSettings.vue:117
+#: src/views/preference/AuthSettings.vue:145
 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."
@@ -1130,12 +1164,6 @@ msgstr ""
 "如果某个 IP 的登录失败次数达到禁用阈值分钟内的最大尝试次数,该 IP 将被禁止登"
 "录一段时间。"
 
-#: src/views/preference/components/TOTP.vue:89
-msgid ""
-"If you lose your mobile phone, you can use the recovery code to reset your "
-"2FA."
-msgstr "如果丢失了手机,可以使用恢复代码重置二步验证。"
-
 #: src/views/preference/components/AddPasskey.vue:70
 msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
 msgstr "如果您的浏览器支持 WebAuthn Passkey,则会出现一个对话框。"
@@ -1167,12 +1195,11 @@ msgstr "初始化核心升级程序错误"
 msgid "Initialing core upgrader"
 msgstr "初始化核心升级器"
 
-#: src/views/preference/components/TOTP.vue:136
+#: src/views/preference/components/TOTP.vue:119
 msgid "Input the code from the app:"
 msgstr "输入应用程序中的代码:"
 
-#: src/components/TwoFA/Authorization.vue:82
-#: src/views/preference/components/TOTP.vue:149
+#: src/components/TwoFA/Authorization.vue:72
 msgid "Input the recovery code:"
 msgstr "输入恢复代码:"
 
@@ -1217,7 +1244,7 @@ msgstr "无效的恢复代码"
 msgid "Invalid request format"
 msgstr "无效的请求格式"
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr "IP"
 
@@ -1241,6 +1268,13 @@ msgstr "颁发者:%{issuer}"
 msgid "Jwt Secret"
 msgstr "Jwt 密钥"
 
+#: src/views/preference/components/RecoveryCodes.vue:74
+msgid ""
+"Keep your recovery codes as safe as your password. We recommend saving them "
+"with a password manager."
+msgstr ""
+"请像保护密码一样安全地保管您的恢复代码。我们建议使用密码管理器保存它们。"
+
 #: src/views/certificate/CertificateList/certColumns.tsx:62
 #: src/views/site/cert/components/AutoCertStepOne.vue:77
 msgid "Key Type"
@@ -1258,7 +1292,7 @@ msgstr "上次使用"
 msgid "Leave blank for no change"
 msgstr "留空表示不修改"
 
-#: src/views/preference/OpenAISettings.vue:53
+#: src/views/preference/OpenAISettings.vue:41
 msgid "Leave blank for the default: https://api.openai.com/"
 msgstr "留空为默认:https://api.openai.com/"
 
@@ -1321,7 +1355,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "日志"
 
-#: src/routes/index.ts:318 src/views/other/Login.vue:222
+#: src/routes/index.ts:318 src/views/other/Login.vue:223
 msgid "Login"
 msgstr "登录"
 
@@ -1380,7 +1414,7 @@ msgstr "用户管理"
 msgid "Managed Certificate"
 msgstr "托管证书"
 
-#: src/views/preference/AuthSettings.vue:107
+#: src/views/preference/AuthSettings.vue:135
 msgid "Max Attempts"
 msgstr "最大尝试次数"
 
@@ -1397,7 +1431,7 @@ msgstr "内存与存储"
 msgid "Minutes"
 msgstr "分钟"
 
-#: src/views/preference/OpenAISettings.vue:32
+#: src/views/preference/OpenAISettings.vue:20
 msgid "Model"
 msgstr "模型"
 
@@ -1566,7 +1600,7 @@ msgstr "Nginx 重启成功"
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:524
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:538
 #: src/views/notification/Notification.vue:37
-#: src/views/preference/AuthSettings.vue:136
+#: src/views/preference/AuthSettings.vue:164
 #: src/views/preference/CertSettings.vue:70
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:97
 #: src/views/site/ngx_conf/LocationEditor.vue:88
@@ -1677,10 +1711,14 @@ msgstr "在线"
 msgid "OpenAI"
 msgstr "OpenAI"
 
-#: src/components/TwoFA/Authorization.vue:112 src/views/other/Login.vue:231
+#: src/components/TwoFA/Authorization.vue:100 src/views/other/Login.vue:232
 msgid "Or"
 msgstr "或"
 
+#: src/views/preference/components/TOTP.vue:112
+msgid "Or enter the secret: %{secret}"
+msgstr "或输入密钥:%{secret}"
+
 #: src/views/config/components/Rename.vue:68
 msgid "Original name"
 msgstr "原名"
@@ -1776,7 +1814,7 @@ msgid ""
 "button below."
 msgstr "请为您要创建的 Passkey 输入一个名称,然后单击下面的 \"确定 \"按钮。"
 
-#: src/components/TwoFA/Authorization.vue:70
+#: src/components/TwoFA/Authorization.vue:85
 msgid "Please enter the OTP code:"
 msgstr "请输入 OTP:"
 
@@ -1798,6 +1836,12 @@ msgstr ""
 "请首先在 “证书”> “DNS 凭证” 中添加凭证,然后在下方选择一个凭证,请求 DNS 提供"
 "商的 API。"
 
+#: src/language/constants.ts:58
+msgid ""
+"Please generate new recovery codes in the preferences immediately to prevent "
+"lockout."
+msgstr "请立即在偏好设置中生成新的恢复码,以防止无法访问您的账户。"
+
 #: src/views/config/components/Rename.vue:63
 #: src/views/config/ConfigEditor.vue:249
 msgid "Please input a filename"
@@ -1915,18 +1959,21 @@ msgstr "恢复"
 msgid "Recovered Successfully"
 msgstr "恢复成功"
 
-#: src/components/TwoFA/Authorization.vue:89
-#: src/views/preference/components/TOTP.vue:156
+#: src/components/TwoFA/Authorization.vue:79
 msgid "Recovery"
 msgstr "恢复"
 
-#: src/views/preference/components/TOTP.vue:82
-msgid "Recovery Code"
+#: src/views/preference/components/RecoveryCodes.vue:68
+msgid "Recovery Codes"
 msgstr "恢复代码"
 
-#: src/views/preference/components/TOTP.vue:91
-msgid "Recovery Code:"
-msgstr "恢复代码:"
+#: src/views/preference/components/RecoveryCodes.vue:73
+msgid ""
+"Recovery codes are used to access your account when you lose access to your "
+"2FA device. Each code can only be used once."
+msgstr ""
+"恢复代码用于在您无法访问双重身份验证设备时登录您的账户。每个代码只能使用一"
+"次。"
 
 #: src/views/preference/CertSettings.vue:37
 msgid "Recursive Nameservers"
@@ -1991,7 +2038,7 @@ msgstr "重载中"
 msgid "Reloading nginx"
 msgstr "正在重载 Nginx"
 
-#: src/views/preference/AuthSettings.vue:141
+#: src/views/preference/AuthSettings.vue:169
 msgid "Remove"
 msgstr "删除"
 
@@ -2003,7 +2050,7 @@ msgstr "从 %{node} 中删除站点 %{site} 错误,响应:%{resp}"
 msgid "Remove Site %{site} from %{node} successfully"
 msgstr "成功从 %{node} 中删除站点 %{site}"
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:51
 #: src/views/preference/components/Passkey.vue:46
 msgid "Remove successfully"
 msgstr "移除成功"
@@ -2093,7 +2140,7 @@ msgstr "请求参数错误"
 msgid "Reset"
 msgstr "重置"
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:93
 msgid "Reset 2FA"
 msgstr "重置二步验证"
 
@@ -2105,15 +2152,15 @@ msgstr "重启"
 msgid "Restarting"
 msgstr "重启中"
 
-#: src/views/preference/AuthSettings.vue:79
+#: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr "依赖方显示名称"
 
-#: src/views/preference/AuthSettings.vue:85
+#: src/views/preference/AuthSettings.vue:113
 msgid "RP Origins"
 msgstr "依赖方的源"
 
-#: src/views/preference/AuthSettings.vue:73
+#: src/views/preference/AuthSettings.vue:101
 msgid "RPID"
 msgstr "依赖方 ID"
 
@@ -2180,7 +2227,7 @@ msgstr "保存成功"
 msgid "Saved successfully"
 msgstr "保存成功"
 
-#: src/views/preference/components/TOTP.vue:72
+#: src/views/preference/components/TOTP.vue:69
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 
@@ -2188,7 +2235,7 @@ msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 msgid "SDK"
 msgstr "SDK"
 
-#: src/views/preference/components/TOTP.vue:128
+#: src/views/preference/components/TOTP.vue:109
 msgid "Secret has been copied"
 msgstr "密钥已复制"
 
@@ -2263,7 +2310,7 @@ msgstr ""
 msgid "Show"
 msgstr "显示"
 
-#: src/views/other/Login.vue:240
+#: src/views/other/Login.vue:241
 msgid "Sign in with a passkey"
 msgstr "使用 Passkey 登录"
 
@@ -2505,7 +2552,7 @@ msgid ""
 "The log path is not under the paths in settings.NginxSettings.LogDirWhiteList"
 msgstr "日志路径不在 settings.NginxSettings.LogDirWhiteList 中的路径之下"
 
-#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:23
 msgid ""
 "The model name should only contain letters, unicode, numbers, hyphens, "
 "dashes, colons, and dots."
@@ -2531,11 +2578,6 @@ msgid ""
 "hyphens, dashes, colons, and dots."
 msgstr "公安备案号只能包含字母、单码、数字、连字符、破折号、冒号和点。"
 
-#: src/views/preference/components/TOTP.vue:90
-msgid ""
-"The recovery code is only displayed once, please save it in a safe place."
-msgstr "恢复密码只会显示一次,请妥善保存。"
-
 #: src/views/dashboard/Environments.vue:148
 msgid ""
 "The remote Nginx UI version is not compatible with the local Nginx UI "
@@ -2556,8 +2598,8 @@ msgstr "当前配置中的 server_name 必须是获取证书所需的域名,
 msgid "The url is invalid"
 msgstr "URL无效"
 
-#: src/views/preference/OpenAISettings.vue:47
-#: src/views/preference/OpenAISettings.vue:60
+#: src/views/preference/OpenAISettings.vue:35
+#: src/views/preference/OpenAISettings.vue:49
 msgid "The url is invalid."
 msgstr "URL 无效."
 
@@ -2565,6 +2607,15 @@ msgstr "URL 无效."
 msgid "The username or password is incorrect"
 msgstr "用户名或密码错误"
 
+#: src/views/preference/components/RecoveryCodes.vue:104
+msgid ""
+"These codes are the last resort for accessing your account in case you lose "
+"your password and second factors. If you cannot find these codes, you will "
+"lose access to your account."
+msgstr ""
+"这些代码是在您丢失密码和双重身份验证方式时,访问账户的最后手段。如果找不到这"
+"些代码,您将无法再访问您的账户。"
+
 #: src/views/certificate/CertificateEditor.vue:102
 msgid "This Auto Cert item is invalid, please remove it."
 msgstr "这个证书自动续期项目是无效的,请删除。"
@@ -2606,11 +2657,11 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr "将 %{nodeNames} 上的 Nginx UI 升级或重新安装到 %{version} 版本。"
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:124
 msgid "Throttle"
 msgstr "限流"
 
-#: src/views/preference/AuthSettings.vue:116
+#: src/views/preference/AuthSettings.vue:144
 #: src/views/preference/components/AddPasskey.vue:65
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
@@ -2620,7 +2671,7 @@ msgstr "提示"
 msgid "Title"
 msgstr "标题"
 
-#: src/views/preference/components/TOTP.vue:71
+#: src/views/preference/components/TOTP.vue:68
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
@@ -2646,7 +2697,7 @@ msgstr ""
 "为了确保认证自动更新能够正常工作,我们需要添加一个能够代理从权威机构到后端的"
 "请求的 Location,并且我们需要保存这个文件并重新加载Nginx。你确定要继续吗?"
 
-#: src/views/preference/OpenAISettings.vue:48
+#: src/views/preference/OpenAISettings.vue:36
 msgid ""
 "To use a local large model, deploy it with ollama, vllm or lmdeploy. They "
 "provide an OpenAI-compatible API endpoint, so just set the baseUrl to your "
@@ -2655,7 +2706,7 @@ msgstr ""
 "要使用本地大型模型,可使用 ollama、vllm 或 lmdeploy 进行部署。它们提供了与 "
 "OpenAI 兼容的 API 端点,因此只需将 baseUrl 设置为本地 API 即可。"
 
-#: src/views/preference/OpenAISettings.vue:72
+#: src/views/preference/OpenAISettings.vue:61
 msgid "Token is not valid"
 msgstr "Token 无效"
 
@@ -2664,11 +2715,11 @@ msgid "Total %{total} item"
 msgid_plural "Total %{total} items"
 msgstr[0] "共 %{total} 个项目"
 
-#: src/views/preference/components/TOTP.vue:69
+#: src/views/preference/components/TOTP.vue:66
 msgid "TOTP"
 msgstr "TOTP"
 
-#: src/views/preference/components/TOTP.vue:70
+#: src/views/preference/components/TOTP.vue:67
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
@@ -2736,11 +2787,11 @@ msgstr "运行时间:"
 msgid "URL"
 msgstr "URL"
 
-#: src/components/TwoFA/Authorization.vue:102
+#: src/components/TwoFA/Authorization.vue:121
 msgid "Use OTP"
 msgstr "使用二步验证码"
 
-#: src/components/TwoFA/Authorization.vue:98
+#: src/components/TwoFA/Authorization.vue:117
 msgid "Use recovery code"
 msgstr "使用恢复代码"
 
@@ -2791,6 +2842,14 @@ msgstr "查看详情"
 msgid "View Mode"
 msgstr "预览模式"
 
+#: src/views/preference/components/RecoveryCodes.vue:134
+msgid "View Recovery Codes"
+msgstr "查看恢复代码"
+
+#: src/views/preference/components/RecoveryCodes.vue:70
+msgid "Viewed"
+msgstr "已查看"
+
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
@@ -2811,7 +2870,7 @@ msgid ""
 msgstr ""
 "我们将从这个文件中删除HTTPChallenge的配置,并重新加载Nginx。你确定要继续吗?"
 
-#: src/views/preference/AuthSettings.vue:69
+#: src/views/preference/AuthSettings.vue:97
 msgid "Webauthn"
 msgstr "Webauthn"
 
@@ -2836,6 +2895,12 @@ msgstr ""
 "启用/禁用、删除或保存此站点时,网站分类中设置的节点和下面选择的节点将同步执行"
 "操作。"
 
+#: src/views/preference/components/RecoveryCodes.vue:140
+msgid ""
+"When you generate new recovery codes, you must download or print the new "
+"codes."
+msgstr "当您生成新的恢复代码时,必须下载或打印新的代码。"
+
 #: src/views/dashboard/ServerAnalytic.vue:37
 #: src/views/dashboard/ServerAnalytic.vue:373
 msgid "Writes"
@@ -2849,7 +2914,7 @@ msgstr "正在将证书私钥写入磁盘"
 msgid "Writing certificate to disk"
 msgstr "正在将证书写入磁盘"
 
-#: src/views/preference/AuthSettings.vue:135
+#: src/views/preference/AuthSettings.vue:163
 #: src/views/preference/CertSettings.vue:69
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:96
 #: src/views/site/ngx_conf/LocationEditor.vue:87
@@ -2870,10 +2935,45 @@ msgid ""
 "passkey."
 msgstr "您尚未配置 Webauthn 的设置,因此无法添加 Passkey。"
 
+#: src/views/preference/components/RecoveryCodes.vue:81
+msgid ""
+"You have not enabled 2FA yet. Please enable 2FA to generate recovery codes."
+msgstr "您尚未启用双重身份验证。请启用双重身份验证以生成恢复代码。"
+
+#: src/views/preference/components/RecoveryCodes.vue:94
+msgid "You have not generated recovery codes yet."
+msgstr "您尚未生成恢复代码。"
+
+#: src/views/preference/components/RecoveryCodes.vue:91
+msgid ""
+"Your current recovery code might be outdated and insecure. Please generate "
+"new recovery codes at your earliest convenience to ensure security."
+msgstr "您的当前恢复代码可能已过期且不安全。请尽快生成新的恢复代码以确保安全。"
+
+#: src/views/preference/components/RecoveryCodes.vue:142
+#: src/views/preference/components/RecoveryCodes.vue:155
+msgid "Your old codes won't work anymore."
+msgstr "您的旧代码将不再有效。"
+
 #: src/views/preference/components/Passkey.vue:75
 msgid "Your passkeys"
 msgstr "你的 Passkeys"
 
+#~ msgid ""
+#~ "If you lose your mobile phone, you can use the recovery code to reset "
+#~ "your 2FA."
+#~ msgstr "如果丢失了手机,可以使用恢复代码重置二步验证。"
+
+#~ msgid "Recovery Code:"
+#~ msgstr "恢复代码:"
+
+#~ msgid ""
+#~ "The recovery code is only displayed once, please save it in a safe place."
+#~ msgstr "恢复密码只会显示一次,请妥善保存。"
+
+#~ msgid "Can't scan? Use text key binding"
+#~ msgstr "无法扫描?使用文本密钥绑定"
+
 #~ msgid "Directory"
 #~ msgstr "目录"
 
@@ -3052,9 +3152,6 @@ msgstr "你的 Passkeys"
 #~ msgid "404 Not Found"
 #~ msgstr "404 未找到页面"
 
-#~ msgid "Are you sure you want to restore?"
-#~ msgstr "您确定要反删除?"
-
 #~ msgid "Destroy"
 #~ msgstr "删除"
 

File diff suppressed because it is too large
+ 176 - 171
app/src/language/zh_TW/app.po


+ 1 - 0
app/src/views/other/Login.vue

@@ -205,6 +205,7 @@ async function handlePasskeyLogin() {
                   enabled: true,
                   otp_status: true,
                   passkey_status: false,
+                  recovery_codes_generated: true,
                 }"
                 @submit-o-t-p="handleOTPSubmit"
               />

+ 29 - 1
app/src/views/preference/AuthSettings.vue

@@ -1,8 +1,12 @@
 <script setup lang="tsx">
+import type { TwoFAStatus } from '@/api/2fa'
+import type { RecoveryCode } from '@/api/recovery'
 import type { BannedIP, Settings } from '@/api/settings'
 import type { CustomRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import type { Ref } from 'vue'
+import twoFA from '@/api/2fa'
 import setting from '@/api/settings'
+import RecoveryCodes from '@/views/preference/components/RecoveryCodes.vue'
 import TOTP from '@/views/preference/components/TOTP.vue'
 import { message } from 'ant-design-vue'
 import dayjs from 'dayjs'
@@ -47,6 +51,17 @@ function removeBannedIP(ip: string) {
     message.success($gettext('Remove successfully'))
   })
 }
+
+const twoFAStatus = ref<TwoFAStatus>({} as TwoFAStatus)
+const recoveryCodes = ref<RecoveryCode[]>()
+
+function get2FAStatus() {
+  twoFA.status().then(r => {
+    twoFAStatus.value = r
+  })
+}
+
+get2FAStatus()
 </script>
 
 <template>
@@ -54,7 +69,20 @@ function removeBannedIP(ip: string) {
     <div>
       <h2>{{ $gettext('2FA Settings') }}</h2>
       <PasskeyRegistration class="mb-4" />
-      <TOTP class="mb-4" />
+
+      <TOTP
+        v-model:recovery-codes="recoveryCodes"
+        class="mb-4"
+        :status="twoFAStatus?.otp_status"
+        @refresh="get2FAStatus"
+      />
+
+      <RecoveryCodes
+        class="mb-4"
+        :two-f-a-status="twoFAStatus"
+        :recovery-codes="recoveryCodes"
+        @refresh="get2FAStatus"
+      />
 
       <h2>
         {{ $gettext('Authentication Settings') }}

+ 172 - 0
app/src/views/preference/components/RecoveryCodes.vue

@@ -0,0 +1,172 @@
+<script setup lang="ts">
+import type { TwoFAStatus } from '@/api/2fa'
+import type { RecoveryCode } from '@/api/recovery'
+import recovery from '@/api/recovery'
+import use2FAModal from '@/components/TwoFA/use2FAModal'
+import { CopyOutlined, WarningOutlined } from '@ant-design/icons-vue'
+import { UseClipboard } from '@vueuse/components'
+import { message } from 'ant-design-vue'
+
+const props = defineProps<{
+  recoveryCodes?: RecoveryCode[]
+  twoFAStatus?: TwoFAStatus
+}>()
+
+const emit = defineEmits<{
+  refresh: [void]
+}>()
+
+const _codes = ref<RecoveryCode[]>()
+const codes = computed(() => _codes.value ?? props.recoveryCodes)
+const newGenerated = ref(false)
+
+const codeSource = computed(() => codes.value?.map(code => code.code).join('\n'))
+
+function clickGenerateRecoveryCodes() {
+  const otpModal = use2FAModal()
+  otpModal.open().then(() => {
+    recovery.generate().then(r => {
+      _codes.value = r.codes
+      newGenerated.value = true
+      emit('refresh')
+      message.success($gettext('Generate recovery codes successfully'))
+    })
+  })
+}
+
+function clickViewRecoveryCodes() {
+  const otpModal = use2FAModal()
+  otpModal.open().then(() => {
+    recovery.view().then(r => {
+      _codes.value = r.codes
+    })
+  })
+}
+
+const popOpen = ref(false)
+
+function popConfirm() {
+  popOpen.value = false
+  clickGenerateRecoveryCodes()
+}
+
+function handlePopOpenChange(visible: boolean) {
+  popOpen.value = visible
+  if (!visible)
+    return
+
+  if (props.twoFAStatus?.recovery_codes_generated)
+    popOpen.value = true
+  else
+    popConfirm()
+}
+</script>
+
+<template>
+  <div>
+    <h3 class="flex items-center gap-2">
+      <span>{{ $gettext('Recovery Codes') }}</span>
+      <ATag v-if="recoveryCodes || twoFAStatus?.recovery_codes_viewed" :color="newGenerated || recoveryCodes ? 'success' : 'processing'">
+        {{ newGenerated || recoveryCodes ? $gettext('First View') : $gettext('Viewed') }}
+      </ATag>
+    </h3>
+    <p>{{ $gettext('Recovery codes are used to access your account when you lose access to your 2FA device. Each code can only be used once.') }}</p>
+    <p>{{ $gettext('Keep your recovery codes as safe as your password. We recommend saving them with a password manager.') }}</p>
+
+    <AAlert
+      v-if="!twoFAStatus?.enabled"
+      class="mb-4"
+      type="info"
+      show-icon
+      :message="$gettext('You have not enabled 2FA yet. Please enable 2FA to generate recovery codes.')"
+    />
+    <AAlert
+      v-else-if="!twoFAStatus?.recovery_codes_generated"
+      class="mb-4"
+      type="warning"
+      show-icon
+    >
+      <template #message>
+        <template v-if="twoFAStatus?.otp_status">
+          {{ $gettext('Your current recovery code might be outdated and insecure. Please generate new recovery codes at your earliest convenience to ensure security.') }}
+        </template>
+        <template v-else>
+          {{ $gettext('You have not generated recovery codes yet.') }}
+        </template>
+      </template>
+    </AAlert>
+
+    <ACard v-if="twoFAStatus?.recovery_codes_generated && codes" class="codes-card mb-4">
+      <template #title>
+        <AAlert class="whitespace-normal px-6 py-4 rounded-t-[8px]" type="warning" banner :show-icon="false">
+          <template #message>
+            <WarningOutlined class="ant-alert-icon text-lg" />
+            {{ $gettext('These codes are the last resort for accessing your account in case you lose your password and second factors. If you cannot find these codes, you will lose access to your account.') }}
+          </template>
+        </AAlert>
+      </template>
+      <ul class="grid grid-cols-2 gap-2 text-lg">
+        <li v-for="(code, index) in codes" :key="index">
+          <span :class="{ 'line-through': code.used_time }">
+            {{ code.code }}
+          </span>
+        </li>
+      </ul>
+      <div class="mt-4 flex space-x-2">
+        <UseClipboard v-slot="{ copy, copied }" :source="codeSource">
+          <AButton @click="copy()">
+            <template #icon>
+              <CopyOutlined />
+            </template>
+            {{ !copied ? $gettext('Copy Codes') : $gettext('Copied') }}
+          </AButton>
+        </UseClipboard>
+      </div>
+    </ACard>
+
+    <template v-if="twoFAStatus?.enabled">
+      <AButton
+        v-if="twoFAStatus?.recovery_codes_generated && !codes"
+        type="primary"
+        ghost
+        @click="clickViewRecoveryCodes"
+      >
+        {{ $gettext('View Recovery Codes') }}
+      </AButton>
+
+      <div v-if="twoFAStatus?.recovery_codes_generated" class="mt-4">
+        <h3>{{ $gettext('Generate New Recovery Codes') }}</h3>
+        <p>
+          {{ $gettext('When you generate new recovery codes, you must download or print the new codes.') }}
+          <b>
+            {{ $gettext('Your old codes won\'t work anymore.') }}
+          </b>
+        </p>
+      </div>
+
+      <APopconfirm
+        :open="popOpen"
+        @open-change="handlePopOpenChange"
+        @confirm="popConfirm"
+        @cancel="() => popOpen = false"
+      >
+        <template #title>
+          {{ $gettext('Are you sure to generate new recovery codes?') }}<br>
+          <b>{{ $gettext('Your old codes won\'t work anymore.') }}</b>
+        </template>
+        <AButton
+          type="primary"
+          ghost
+        >
+          {{ twoFAStatus?.recovery_codes_generated ? $gettext('Generate New Recovery Codes') : $gettext('Generate Recovery Codes') }}
+        </AButton>
+      </APopconfirm>
+    </template>
+  </div>
+</template>
+
+<style scoped lang="less">
+.codes-card :deep(.ant-card-head) {
+  padding: 0;
+}
+</style>

+ 62 - 94
app/src/views/preference/components/TOTP.vue

@@ -1,21 +1,28 @@
 <script setup lang="ts">
-import twoFA from '@/api/2fa'
+import type { RecoveryCode } from '@/api/recovery'
 import otp from '@/api/otp'
 import OTPInput from '@/components/OTPInput/OTPInput.vue'
+import use2FAModal from '@/components/TwoFA/use2FAModal'
 import { CheckCircleOutlined } from '@ant-design/icons-vue'
 import { UseClipboard } from '@vueuse/components'
-
 import { message } from 'ant-design-vue'
 
-const status = ref(false)
+const { status = false } = defineProps<{
+  status?: boolean
+}>()
+
+const emit = defineEmits<{
+  refresh: [void]
+}>()
+
+const recoveryCodes = defineModel<RecoveryCode[]>('recoveryCodes')
+
 const enrolling = ref(false)
 const resetting = ref(false)
-const qrCode = ref('')
+const generatedUrl = ref('')
 const secret = ref('')
 const passcode = ref('')
 const refOtp = useTemplateRef('refOtp')
-const recoveryCode = ref('')
-const inputRecoveryCode = ref('')
 
 function clickEnable2FA() {
   enrolling.value = true
@@ -25,7 +32,7 @@ function clickEnable2FA() {
 function generateSecret() {
   otp.generate_secret().then(r => {
     secret.value = r.secret
-    qrCode.value = r.qr_code
+    generatedUrl.value = r.url
     refOtp.value?.clearInput()
   })
 }
@@ -33,33 +40,23 @@ function generateSecret() {
 function enroll(code: string) {
   otp.enroll_otp(secret.value, code).then(r => {
     enrolling.value = false
-    recoveryCode.value = r.recovery_code
-    get2FAStatus()
+    recoveryCodes.value = r.codes
+    emit('refresh')
     message.success($gettext('Enable 2FA successfully'))
   }).catch(() => {
     refOtp.value?.clearInput()
   })
 }
 
-function get2FAStatus() {
-  twoFA.status().then(r => {
-    status.value = r.otp_status
-  })
-}
-
-get2FAStatus()
-
-function clickReset2FA() {
-  resetting.value = true
-  inputRecoveryCode.value = ''
-}
-
 function reset2FA() {
-  otp.reset(inputRecoveryCode.value).then(() => {
-    resetting.value = false
-    recoveryCode.value = ''
-    get2FAStatus()
-    clickEnable2FA()
+  const otpModal = use2FAModal()
+  otpModal.open().then(() => {
+    otp.reset().then(() => {
+      resetting.value = false
+      recoveryCodes.value = undefined
+      emit('refresh')
+      clickEnable2FA()
+    })
   })
 }
 </script>
@@ -70,30 +67,11 @@ function reset2FA() {
     <p>{{ $gettext('TOTP is a two-factor authentication method that uses a time-based one-time password algorithm.') }}</p>
     <p>{{ $gettext('To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone.') }}</p>
     <p>{{ $gettext('Scan the QR code with your mobile phone to add the account to the app.') }}</p>
-    <p v-if="!status">
-      {{ $gettext('Current account is not enabled TOTP.') }}
-    </p>
+    <AAlert v-if="!status" type="warning" :message="$gettext('Current account is not enabled TOTP.')" class="mb-2" show-icon />
     <div v-else>
       <p><CheckCircleOutlined class="mr-2 text-green-600" />{{ $gettext('Current account is enabled TOTP.') }}</p>
     </div>
 
-    <AAlert
-      v-if="recoveryCode"
-      :message="$gettext('Recovery Code')"
-      class="mb-4"
-      type="info"
-      show-icon
-    >
-      <template #description>
-        <div>
-          <p>{{ $gettext('If you lose your mobile phone, you can use the recovery code to reset your 2FA.') }}</p>
-          <p>{{ $gettext('The recovery code is only displayed once, please save it in a safe place.') }}</p>
-          <p>{{ $gettext('Recovery Code:') }}</p>
-          <span class="ml-2">{{ recoveryCode }}</span>
-        </div>
-      </template>
-    </AAlert>
-
     <AButton
       v-if="!status && !enrolling"
       type="primary"
@@ -102,61 +80,51 @@ function reset2FA() {
     >
       {{ $gettext('Enable TOTP') }}
     </AButton>
-    <AButton
+    <APopconfirm
       v-if="status && !resetting"
-      type="primary"
-      ghost
-      @click="clickReset2FA"
+      :title="$gettext('Are you sure to reset 2FA?')"
+      @confirm="reset2FA"
     >
-      {{ $gettext('Reset 2FA') }}
-    </AButton>
+      <AButton
+        v-if="status && !resetting"
+        type="primary"
+        ghost
+      >
+        {{ $gettext('Reset 2FA') }}
+      </AButton>
+    </APopconfirm>
 
     <template v-if="enrolling">
-      <div class="mt-4 mb-2">
-        <img
-          v-if="qrCode"
-          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 class="flex flex-col items-center">
+        <div class="mt-4 mb-2">
+          <AQrcode
+            v-if="generatedUrl"
+            :value="generatedUrl"
+            :size="256"
+          />
+          <div class="w-64 flex justify-center mt-2">
+            <UseClipboard v-slot="{ copy, copied }">
+              <ATooltip @click="() => copy(secret)">
+                <template #title>
+                  {{ copied ? $gettext('Secret has been copied')
+                    : $gettext('Click to copy') }}
+                </template>
+                {{ $gettext('Or enter the secret: %{secret}', { secret }) }}
+              </ATooltip>
+            </UseClipboard>
+          </div>
         </div>
-      </div>
 
-      <div>
-        <p>{{ $gettext('Input the code from the app:') }}</p>
-        <OTPInput
-          ref="refOtp"
-          v-model="passcode"
-          @on-complete="enroll"
-        />
+        <div>
+          <p>{{ $gettext('Input the code from the app:') }}</p>
+          <OTPInput
+            ref="refOtp"
+            v-model="passcode"
+            @on-complete="enroll"
+          />
+        </div>
       </div>
     </template>
-
-    <div
-      v-if="resetting"
-      class="mt-2"
-    >
-      <p>{{ $gettext('Input the recovery code:') }}</p>
-      <AInputGroup compact>
-        <AInput v-model:value="inputRecoveryCode" />
-        <AButton
-          type="primary"
-          @click="reset2FA"
-        >
-          {{ $gettext('Recovery') }}
-        </AButton>
-      </AInputGroup>
-    </div>
   </div>
 </template>
 

+ 52 - 1
internal/crypto/aes.go

@@ -1,12 +1,17 @@
 package crypto
 
 import (
+	"context"
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/rand"
 	"encoding/base64"
-	"github.com/0xJacky/Nginx-UI/settings"
+	"encoding/json"
 	"io"
+	"reflect"
+
+	"github.com/0xJacky/Nginx-UI/settings"
+	"gorm.io/gorm/schema"
 )
 
 // AesEncrypt encrypts text and given key with AES.
@@ -55,3 +60,49 @@ func AesDecrypt(text []byte) ([]byte, error) {
 
 	return data, nil
 }
+
+type JSONAesSerializer struct{}
+
+func (JSONAesSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) (err error) {
+	fieldValue := reflect.New(field.FieldType)
+
+	if dbValue != nil {
+		var bytes []byte
+		switch v := dbValue.(type) {
+		case []byte:
+			bytes = v
+		case string:
+			bytes = []byte(v)
+		default:
+			bytes, err = json.Marshal(v)
+			if err != nil {
+				return err
+			}
+		}
+
+		if len(bytes) > 0 {
+			bytes, err = AesDecrypt(bytes)
+			if err != nil {
+				return err
+			}
+			err = json.Unmarshal(bytes, fieldValue.Interface())
+		}
+	}
+
+	field.ReflectValueOf(ctx, dst).Set(fieldValue.Elem())
+	return
+}
+
+// Value implements serializer interface
+func (JSONAesSerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue interface{}) (interface{}, error) {
+	result, err := json.Marshal(fieldValue)
+	if string(result) == "null" {
+		if field.TagSettings["NOT NULL"] != "" {
+			return "", nil
+		}
+		return nil, err
+	}
+
+	encrypt, err := AesEncrypt(result)
+	return string(encrypt), err
+}

+ 1 - 0
internal/user/errors.go

@@ -8,6 +8,7 @@ var (
 	ErrUserBanned              = e.New(40303, "user banned")
 	ErrOTPCode                 = e.New(40304, "invalid otp code")
 	ErrRecoveryCode            = e.New(40305, "invalid recovery code")
+	ErrTOTPNotEnabled          = e.New(40306, "legacy recovery code not allowed since totp is not enabled")
 	ErrWebAuthnNotConfigured   = e.New(50000, "WebAuthn settings are not configured")
 	ErrUserNotEnabledOTPAs2FA  = e.New(50001, "user not enabled otp as 2fa")
 	ErrOTPOrRecoveryCodeEmpty  = e.New(50002, "otp or recovery code empty")

+ 44 - 5
internal/user/otp.go

@@ -5,12 +5,15 @@ import (
 	"crypto/sha1"
 	"encoding/hex"
 	"fmt"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/internal/cache"
 	"github.com/0xJacky/Nginx-UI/internal/crypto"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
 	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/google/uuid"
 	"github.com/pquerna/otp/totp"
-	"time"
 )
 
 func VerifyOTP(user *model.User, otp, recoveryCode string) (err error) {
@@ -24,14 +27,50 @@ func VerifyOTP(user *model.User, otp, recoveryCode string) (err error) {
 			return ErrOTPCode
 		}
 	} else {
-		recoverCode, err := hex.DecodeString(recoveryCode)
+		// get user from db
+		u := query.User
+		user, err = u.Where(u.ID.Eq(user.ID)).First()
 		if err != nil {
 			return err
 		}
-		k := sha1.Sum(user.OTPSecret)
-		if !bytes.Equal(k[:], recoverCode) {
-			return ErrRecoveryCode
+
+		// legacy recovery code
+		if !user.RecoveryCodeGenerated() {
+			if user.OTPSecret == nil {
+				return ErrTOTPNotEnabled
+			}
+
+			recoverCode, err := hex.DecodeString(recoveryCode)
+			if err != nil {
+				return err
+			}
+			k := sha1.Sum(user.OTPSecret)
+			if !bytes.Equal(k[:], recoverCode) {
+				return ErrRecoveryCode
+			}
+		}
+
+		// check recovery code
+		usedCount := 0
+		verified := false
+		for _, code := range user.RecoveryCodes.Codes {
+			if code.Code == recoveryCode && code.UsedTime == nil {
+				t := time.Now().Unix()
+				code.UsedTime = &t
+				_, err = u.Where(u.ID.Eq(user.ID)).Updates(user)
+				if err != nil {
+					return err
+				}
+				verified = true
+			}
+			if code.UsedTime != nil {
+				usedCount++
+			}
+		}
+		if verified && usedCount == len(user.RecoveryCodes.Codes) {
+			notification.Warning("All Recovery Codes Have Been Used", "Please generate new recovery codes in the preferences immediately to prevent lockout.")
 		}
+		return ErrRecoveryCode
 	}
 	return
 }

+ 31 - 5
model/user.go

@@ -1,19 +1,37 @@
 package model
 
 import (
+	"github.com/0xJacky/Nginx-UI/internal/crypto"
 	"github.com/go-webauthn/webauthn/webauthn"
 	"github.com/spf13/cast"
 	"gorm.io/gorm"
+	"gorm.io/gorm/schema"
 )
 
+func init() {
+	schema.RegisterSerializer("json[aes]", crypto.JSONAesSerializer{})
+}
+
+type RecoveryCode struct {
+	Code     string `json:"code"`
+	UsedTime *int64 `json:"used_time,omitempty"  gorm:"type:datetime;default:null"`
+}
+
+type RecoveryCodes struct {
+	Codes          []*RecoveryCode `json:"codes"`
+	LastViewed     *int64          `json:"last_viewed,omitempty" gorm:"serializer:unixtime;type:datetime;default:null"`
+	LastDownloaded *int64          `json:"last_downloaded,omitempty" gorm:"serializer:unixtime;type:datetime;default:null"`
+}
+
 type User struct {
 	Model
 
-	Name         string `json:"name" cosy:"add:max=20;update:omitempty,max=20;list:fussy;db_unique"`
-	Password     string `json:"-" cosy:"json:password;add:required,max=20;update:omitempty,max=20"`
-	Status       bool   `json:"status" gorm:"default:1"`
-	OTPSecret    []byte `json:"-" gorm:"type:blob"`
-	EnabledTwoFA bool   `json:"enabled_2fa" gorm:"-"`
+	Name          string        `json:"name" cosy:"add:max=20;update:omitempty,max=20;list:fussy;db_unique"`
+	Password      string        `json:"-" cosy:"json:password;add:required,max=20;update:omitempty,max=20"`
+	Status        bool          `json:"status" gorm:"default:1"`
+	OTPSecret     []byte        `json:"-" gorm:"type:blob"`
+	RecoveryCodes RecoveryCodes `json:"-" gorm:"serializer:json[aes]"`
+	EnabledTwoFA  bool          `json:"enabled_2fa" gorm:"-"`
 }
 
 type AuthToken struct {
@@ -35,6 +53,14 @@ func (u *User) EnabledOTP() bool {
 	return len(u.OTPSecret) != 0
 }
 
+func (u *User) RecoveryCodeGenerated() bool {
+	return len(u.RecoveryCodes.Codes) > 0
+}
+
+func (u *User) RecoveryCodeViewed() bool {
+	return u.RecoveryCodes.LastViewed != nil
+}
+
 func (u *User) EnabledPasskey() bool {
 	var passkeys Passkey
 	db.Where("user_id", u.ID).Limit(1).Find(&passkeys)

+ 14 - 10
query/auths.gen.go

@@ -36,6 +36,7 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user {
 	_user.Password = field.NewString(tableName, "password")
 	_user.Status = field.NewBool(tableName, "status")
 	_user.OTPSecret = field.NewBytes(tableName, "otp_secret")
+	_user.RecoveryCodes = field.NewField(tableName, "recovery_codes")
 
 	_user.fillFieldMap()
 
@@ -45,15 +46,16 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user {
 type user struct {
 	userDo
 
-	ALL       field.Asterisk
-	ID        field.Uint64
-	CreatedAt field.Time
-	UpdatedAt field.Time
-	DeletedAt field.Field
-	Name      field.String
-	Password  field.String
-	Status    field.Bool
-	OTPSecret field.Bytes
+	ALL           field.Asterisk
+	ID            field.Uint64
+	CreatedAt     field.Time
+	UpdatedAt     field.Time
+	DeletedAt     field.Field
+	Name          field.String
+	Password      field.String
+	Status        field.Bool
+	OTPSecret     field.Bytes
+	RecoveryCodes field.Field
 
 	fieldMap map[string]field.Expr
 }
@@ -78,6 +80,7 @@ func (u *user) updateTableName(table string) *user {
 	u.Password = field.NewString(table, "password")
 	u.Status = field.NewBool(table, "status")
 	u.OTPSecret = field.NewBytes(table, "otp_secret")
+	u.RecoveryCodes = field.NewField(table, "recovery_codes")
 
 	u.fillFieldMap()
 
@@ -94,7 +97,7 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 }
 
 func (u *user) fillFieldMap() {
-	u.fieldMap = make(map[string]field.Expr, 8)
+	u.fieldMap = make(map[string]field.Expr, 9)
 	u.fieldMap["id"] = u.ID
 	u.fieldMap["created_at"] = u.CreatedAt
 	u.fieldMap["updated_at"] = u.UpdatedAt
@@ -103,6 +106,7 @@ func (u *user) fillFieldMap() {
 	u.fieldMap["password"] = u.Password
 	u.fieldMap["status"] = u.Status
 	u.fieldMap["otp_secret"] = u.OTPSecret
+	u.fieldMap["recovery_codes"] = u.RecoveryCodes
 }
 
 func (u user) clone(db *gorm.DB) user {

Some files were not shown because too many files changed in this diff