浏览代码

Merge pull request #555 from 0xJacky/feat/passkey

feat/passkey
Jacky 10 月之前
父节点
当前提交
b445a1bb69
共有 74 个文件被更改,包括 3802 次插入1591 次删除
  1. 2 2
      api/api.go
  2. 2 2
      api/system/install.go
  3. 156 0
      api/user/2fa.go
  4. 3 1
      api/user/auth.go
  5. 134 215
      api/user/otp.go
  6. 195 0
      api/user/passkey.go
  7. 17 3
      api/user/router.go
  8. 8 8
      api/user/user.go
  9. 6 0
      app.example.ini
  10. 2 0
      app/components.d.ts
  11. 3 1
      app/package.json
  12. 189 164
      app/pnpm-lock.yaml
  13. 37 0
      app/src/api/2fa.ts
  14. 11 0
      app/src/api/auth.ts
  15. 0 12
      app/src/api/otp.ts
  16. 36 0
      app/src/api/passkey.ts
  17. 136 0
      app/src/components/2FA/Authorization.vue
  18. 22 16
      app/src/components/2FA/use2FAModal.ts
  19. 1 1
      app/src/components/CodeEditor/CodeEditor.vue
  20. 0 78
      app/src/components/OTP/OTPAuthorization.vue
  21. 63 0
      app/src/components/ReactiveFromNow/ReactiveFromNow.vue
  22. 59 4
      app/src/components/SetLanguage/SetLanguage.vue
  23. 1 0
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  24. 1 1
      app/src/components/StdDesign/StdDataDisplay/StdPagination.vue
  25. 172 59
      app/src/language/en/app.po
  26. 174 58
      app/src/language/es/app.po
  27. 172 59
      app/src/language/fr_FR/app.po
  28. 172 59
      app/src/language/ko_KR/app.po
  29. 151 59
      app/src/language/messages.pot
  30. 174 58
      app/src/language/ru_RU/app.po
  31. 172 59
      app/src/language/vi_VN/app.po
  32. 二进制
      app/src/language/zh_CN/app.mo
  33. 175 61
      app/src/language/zh_CN/app.po
  34. 174 58
      app/src/language/zh_TW/app.po
  35. 2 2
      app/src/layouts/SideBar.vue
  36. 2 2
      app/src/lib/http/index.ts
  37. 12 1
      app/src/pinia/moudule/user.ts
  38. 1 2
      app/src/routes/index.ts
  39. 1 1
      app/src/views/config/ConfigEditor.vue
  40. 2 2
      app/src/views/config/components/Mkdir.vue
  41. 2 2
      app/src/views/config/components/Rename.vue
  42. 1 1
      app/src/views/domain/components/RightSettings.vue
  43. 1 1
      app/src/views/domain/ngx_conf/NgxConfigEditor.vue
  44. 1 1
      app/src/views/domain/ngx_conf/NgxServer.vue
  45. 1 1
      app/src/views/domain/ngx_conf/NgxUpstream.vue
  46. 65 6
      app/src/views/other/Login.vue
  47. 6 0
      app/src/views/preference/AuthSettings.vue
  48. 3 3
      app/src/views/preference/Preference.vue
  49. 118 0
      app/src/views/preference/components/AddPasskey.vue
  50. 159 0
      app/src/views/preference/components/Passkey.vue
  51. 8 7
      app/src/views/preference/components/TOTP.vue
  52. 4 4
      app/src/views/pty/Terminal.vue
  53. 1 1
      app/src/views/stream/components/RightSettings.vue
  54. 5 1
      go.mod
  55. 12 252
      go.sum
  56. 4 0
      internal/cache/cache.go
  57. 2 0
      internal/kernal/boot.go
  58. 2 2
      internal/kernal/skip_install.go
  59. 1 1
      internal/middleware/secure_session.go
  60. 46 0
      internal/passkey/webauthn.go
  61. 2 2
      internal/user/login.go
  62. 2 2
      internal/user/otp.go
  63. 7 7
      internal/user/user.go
  64. 43 11
      model/auth.go
  65. 2 1
      model/model.go
  66. 13 0
      model/passkey.go
  67. 157 157
      query/auths.gen.go
  68. 16 8
      query/gen.go
  69. 382 0
      query/passkeys.gen.go
  70. 71 71
      router/routers.go
  71. 1 1
      settings/auth.go
  72. 7 0
      settings/settings.go
  73. 8 0
      settings/settings_test.go
  74. 9 0
      settings/webauthn.go

+ 2 - 2
api/api.go

@@ -12,8 +12,8 @@ import (
 	"strings"
 	"strings"
 )
 )
 
 
-func CurrentUser(c *gin.Context) *model.Auth {
-	return c.MustGet("user").(*model.Auth)
+func CurrentUser(c *gin.Context) *model.User {
+	return c.MustGet("user").(*model.User)
 }
 }
 
 
 func ErrHandler(c *gin.Context, err error) {
 func ErrHandler(c *gin.Context, err error) {

+ 2 - 2
api/system/install.go

@@ -61,8 +61,8 @@ func InstallNginxUI(c *gin.Context) {
 
 
 	pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
 	pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
 
 
-	u := query.Auth
-	err = u.Create(&model.Auth{
+	u := query.User
+	err = u.Create(&model.User{
 		Name:     json.Username,
 		Name:     json.Username,
 		Password: string(pwd),
 		Password: string(pwd),
 	})
 	})

+ 156 - 0
api/user/2fa.go

@@ -0,0 +1,156 @@
+package user
+
+import (
+	"encoding/base64"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/cache"
+	"github.com/0xJacky/Nginx-UI/internal/passkey"
+	"github.com/0xJacky/Nginx-UI/internal/user"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/go-webauthn/webauthn/webauthn"
+	"github.com/google/uuid"
+	"net/http"
+	"strings"
+	"time"
+)
+
+type Status2FA struct {
+	Enabled       bool `json:"enabled"`
+	OTPStatus     bool `json:"otp_status"`
+	PasskeyStatus bool `json:"passkey_status"`
+}
+
+func get2FAStatus(c *gin.Context) (status Status2FA) {
+	// when accessing the node from the main cluster, there is no user in the context
+	u, ok := c.Get("user")
+	if ok {
+		userPtr := u.(*model.User)
+		status.OTPStatus = userPtr.EnabledOTP()
+		status.PasskeyStatus = userPtr.EnabledPasskey() && passkey.Enabled()
+		status.Enabled = status.OTPStatus || status.PasskeyStatus
+	}
+	return
+}
+
+func Get2FAStatus(c *gin.Context) {
+	c.JSON(http.StatusOK, get2FAStatus(c))
+}
+
+func SecureSessionStatus(c *gin.Context) {
+	status2FA := get2FAStatus(c)
+	if !status2FA.Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"status": false,
+		})
+		return
+	}
+
+	ssid := c.GetHeader("X-Secure-Session-ID")
+	if ssid == "" {
+		ssid = c.Query("X-Secure-Session-ID")
+	}
+	if ssid == "" {
+		c.JSON(http.StatusOK, gin.H{
+			"status": false,
+		})
+		return
+	}
+
+	u := api.CurrentUser(c)
+
+	c.JSON(http.StatusOK, gin.H{
+		"status": user.VerifySecureSessionID(ssid, u.ID),
+	})
+}
+
+func Start2FASecureSessionByOTP(c *gin.Context) {
+	var json struct {
+		OTP          string `json:"otp"`
+		RecoveryCode string `json:"recovery_code"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	u := api.CurrentUser(c)
+	if !u.EnabledOTP() {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"message": "User has not configured OTP as 2FA",
+		})
+		return
+	}
+
+	if json.OTP == "" && json.RecoveryCode == "" {
+		c.JSON(http.StatusBadRequest, LoginResponse{
+			Message: "The user has enabled OTP as 2FA",
+		})
+		return
+	}
+
+	if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
+		c.JSON(http.StatusBadRequest, LoginResponse{
+			Message: "Invalid OTP or recovery code",
+		})
+		return
+	}
+
+	sessionId := user.SetSecureSessionID(u.ID)
+
+	c.JSON(http.StatusOK, gin.H{
+		"session_id": sessionId,
+	})
+}
+
+func BeginStart2FASecureSessionByPasskey(c *gin.Context) {
+	if !passkey.Enabled() {
+		api.ErrHandler(c, fmt.Errorf("WebAuthn settings are not configured"))
+		return
+	}
+	webauthnInstance := passkey.GetInstance()
+	u := api.CurrentUser(c)
+	options, sessionData, err := webauthnInstance.BeginLogin(u)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	passkeySessionID := uuid.NewString()
+	cache.Set(passkeySessionID, sessionData, passkeyTimeout)
+	c.JSON(http.StatusOK, gin.H{
+		"session_id": passkeySessionID,
+		"options":    options,
+	})
+}
+
+func FinishStart2FASecureSessionByPasskey(c *gin.Context) {
+	if !passkey.Enabled() {
+		api.ErrHandler(c, fmt.Errorf("WebAuthn settings are not configured"))
+		return
+	}
+	passkeySessionID := c.GetHeader("X-Passkey-Session-ID")
+	sessionDataBytes, ok := cache.Get(passkeySessionID)
+	if !ok {
+		api.ErrHandler(c, fmt.Errorf("session not found"))
+		return
+	}
+	sessionData := sessionDataBytes.(*webauthn.SessionData)
+	webauthnInstance := passkey.GetInstance()
+	u := api.CurrentUser(c)
+	credential, err := webauthnInstance.FinishLogin(u, *sessionData, c.Request)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	rawID := strings.TrimRight(base64.StdEncoding.EncodeToString(credential.ID), "=")
+	p := query.Passkey
+	_, _ = p.Where(p.RawID.Eq(rawID)).Updates(&model.Passkey{
+		LastUsedAt: time.Now().Unix(),
+	})
+
+	sessionId := user.SetSecureSessionID(u.ID)
+
+	c.JSON(http.StatusOK, gin.H{
+		"session_id": sessionId,
+	})
+}

+ 3 - 1
api/user/auth.go

@@ -8,6 +8,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
+	"math/rand/v2"
 	"net/http"
 	"net/http"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -67,7 +68,8 @@ func Login(c *gin.Context) {
 
 
 	u, err := user.Login(json.Name, json.Password)
 	u, err := user.Login(json.Name, json.Password)
 	if err != nil {
 	if err != nil {
-		// time.Sleep(5 * time.Second)
+		random := time.Duration(rand.Int() % 10)
+		time.Sleep(random * time.Second)
 		switch {
 		switch {
 		case errors.Is(err, user.ErrPasswordIncorrect):
 		case errors.Is(err, user.ErrPasswordIncorrect):
 			c.JSON(http.StatusForbidden, LoginResponse{
 			c.JSON(http.StatusForbidden, LoginResponse{

+ 134 - 215
api/user/otp.go

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

+ 195 - 0
api/user/passkey.go

@@ -0,0 +1,195 @@
+package user
+
+import (
+	"encoding/base64"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/cache"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/internal/passkey"
+	"github.com/0xJacky/Nginx-UI/internal/user"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/go-webauthn/webauthn/webauthn"
+	"github.com/google/uuid"
+	"github.com/spf13/cast"
+	"gorm.io/gorm"
+	"net/http"
+	"strings"
+	"time"
+)
+
+const passkeyTimeout = 30 * time.Second
+
+func buildCachePasskeyRegKey(id int) string {
+	return fmt.Sprintf("passkey-reg-%d", id)
+}
+
+func GetPasskeyConfigStatus(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"status": passkey.Enabled(),
+	})
+}
+
+func BeginPasskeyRegistration(c *gin.Context) {
+	u := api.CurrentUser(c)
+
+	webauthnInstance := passkey.GetInstance()
+
+	options, sessionData, err := webauthnInstance.BeginRegistration(u)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	cache.Set(buildCachePasskeyRegKey(u.ID), sessionData, passkeyTimeout)
+
+	c.JSON(http.StatusOK, options)
+}
+
+func FinishPasskeyRegistration(c *gin.Context) {
+	cUser := api.CurrentUser(c)
+	webauthnInstance := passkey.GetInstance()
+	sessionDataBytes, ok := cache.Get(buildCachePasskeyRegKey(cUser.ID))
+	if !ok {
+		api.ErrHandler(c, fmt.Errorf("session not found"))
+		return
+	}
+
+	sessionData := sessionDataBytes.(*webauthn.SessionData)
+	credential, err := webauthnInstance.FinishRegistration(cUser, *sessionData, c.Request)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	cache.Del(buildCachePasskeyRegKey(cUser.ID))
+
+	rawId := strings.TrimRight(base64.StdEncoding.EncodeToString(credential.ID), "=")
+	passkeyName := c.Query("name")
+	p := query.Passkey
+	err = p.Create(&model.Passkey{
+		UserID:     cUser.ID,
+		Name:       passkeyName,
+		RawID:      rawId,
+		Credential: credential,
+		LastUsedAt: time.Now().Unix(),
+	})
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}
+
+func BeginPasskeyLogin(c *gin.Context) {
+	if !passkey.Enabled() {
+		api.ErrHandler(c, fmt.Errorf("WebAuthn settings are not configured"))
+		return
+	}
+	webauthnInstance := passkey.GetInstance()
+	options, sessionData, err := webauthnInstance.BeginDiscoverableLogin()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	sessionID := uuid.NewString()
+	cache.Set(sessionID, sessionData, passkeyTimeout)
+
+	c.JSON(http.StatusOK, gin.H{
+		"session_id": sessionID,
+		"options":    options,
+	})
+}
+
+func FinishPasskeyLogin(c *gin.Context) {
+	if !passkey.Enabled() {
+		api.ErrHandler(c, fmt.Errorf("WebAuthn settings are not configured"))
+		return
+	}
+	sessionId := c.GetHeader("X-Passkey-Session-ID")
+	sessionDataBytes, ok := cache.Get(sessionId)
+	if !ok {
+		api.ErrHandler(c, fmt.Errorf("session not found"))
+		return
+	}
+	webauthnInstance := passkey.GetInstance()
+	sessionData := sessionDataBytes.(*webauthn.SessionData)
+	var outUser *model.User
+	_, err := webauthnInstance.FinishDiscoverableLogin(
+		func(rawID, userHandle []byte) (user webauthn.User, err error) {
+			encodeRawID := strings.TrimRight(base64.StdEncoding.EncodeToString(rawID), "=")
+			u := query.User
+			logger.Debug("[WebAuthn] Discoverable Login", cast.ToInt(string(userHandle)))
+
+			p := query.Passkey
+			_, _ = p.Where(p.RawID.Eq(encodeRawID)).Updates(&model.Passkey{
+				LastUsedAt: time.Now().Unix(),
+			})
+
+			outUser, err = u.FirstByID(cast.ToInt(string(userHandle)))
+			return outUser, err
+		}, *sessionData, c.Request)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	b := query.BanIP
+	clientIP := c.ClientIP()
+	// login success, clear banned record
+	_, _ = b.Where(b.IP.Eq(clientIP)).Delete()
+
+	logger.Info("[User Login]", outUser.Name)
+	token, err := user.GenerateJWT(outUser)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, LoginResponse{
+			Message: err.Error(),
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, LoginResponse{
+		Code:    LoginSuccess,
+		Message: "ok",
+		Token:   token,
+		// SecureSessionID: secureSessionID,
+	})
+}
+
+func GetPasskeyList(c *gin.Context) {
+	u := api.CurrentUser(c)
+	p := query.Passkey
+	passkeys, err := p.Where(p.UserID.Eq(u.ID)).Find()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if len(passkeys) == 0 {
+		passkeys = make([]*model.Passkey, 0)
+	}
+
+	c.JSON(http.StatusOK, passkeys)
+}
+
+func UpdatePasskey(c *gin.Context) {
+	u := api.CurrentUser(c)
+	cosy.Core[model.Passkey](c).
+		SetValidRules(gin.H{
+			"name": "required",
+		}).GormScope(func(tx *gorm.DB) *gorm.DB {
+		return tx.Where("user_id", u.ID)
+	}).Modify()
+}
+
+func DeletePasskey(c *gin.Context) {
+	u := api.CurrentUser(c)
+	cosy.Core[model.Passkey](c).
+		GormScope(func(tx *gorm.DB) *gorm.DB {
+			return tx.Where("user_id", u.ID)
+		}).PermanentlyDelete()
+}

+ 17 - 3
api/user/router.go

@@ -8,8 +8,13 @@ func InitAuthRouter(r *gin.RouterGroup) {
 	r.POST("/login", Login)
 	r.POST("/login", Login)
 	r.DELETE("/logout", Logout)
 	r.DELETE("/logout", Logout)
 
 
+	r.GET("/begin_passkey_login", BeginPasskeyLogin)
+	r.POST("/finish_passkey_login", FinishPasskeyLogin)
+
 	r.GET("/casdoor_uri", GetCasdoorUri)
 	r.GET("/casdoor_uri", GetCasdoorUri)
 	r.POST("/casdoor_callback", CasdoorCallback)
 	r.POST("/casdoor_callback", CasdoorCallback)
+
+	r.GET("/passkeys/config", GetPasskeyConfigStatus)
 }
 }
 
 
 func InitManageUserRouter(r *gin.RouterGroup) {
 func InitManageUserRouter(r *gin.RouterGroup) {
@@ -22,11 +27,20 @@ func InitManageUserRouter(r *gin.RouterGroup) {
 }
 }
 
 
 func InitUserRouter(r *gin.RouterGroup) {
 func InitUserRouter(r *gin.RouterGroup) {
-	r.GET("/otp_status", OTPStatus)
+	r.GET("/2fa_status", Get2FAStatus)
+	r.GET("/2fa_secure_session/status", SecureSessionStatus)
+	r.POST("/2fa_secure_session/otp", Start2FASecureSessionByOTP)
+	r.GET("/2fa_secure_session/passkey", BeginStart2FASecureSessionByPasskey)
+	r.POST("/2fa_secure_session/passkey", FinishStart2FASecureSessionByPasskey)
+
 	r.GET("/otp_secret", GenerateTOTP)
 	r.GET("/otp_secret", GenerateTOTP)
 	r.POST("/otp_enroll", EnrollTOTP)
 	r.POST("/otp_enroll", EnrollTOTP)
 	r.POST("/otp_reset", ResetOTP)
 	r.POST("/otp_reset", ResetOTP)
 
 
-	r.GET("/otp_secure_session_status", SecureSessionStatus)
-	r.POST("/otp_secure_session", StartSecure2FASession)
+	r.GET("/begin_passkey_register", BeginPasskeyRegistration)
+	r.POST("/finish_passkey_register", FinishPasskeyRegistration)
+
+	r.GET("/passkeys", GetPasskeyList)
+	r.POST("/passkeys/:id", UpdatePasskey)
+	r.DELETE("/passkeys/:id", DeletePasskey)
 }
 }

+ 8 - 8
api/user/user.go

@@ -13,13 +13,13 @@ import (
 )
 )
 
 
 func GetUsers(c *gin.Context) {
 func GetUsers(c *gin.Context) {
-	cosy.Core[model.Auth](c).SetFussy("name").PagingList()
+	cosy.Core[model.User](c).SetFussy("name").PagingList()
 }
 }
 
 
 func GetUser(c *gin.Context) {
 func GetUser(c *gin.Context) {
 	id := cast.ToInt(c.Param("id"))
 	id := cast.ToInt(c.Param("id"))
 
 
-	u := query.Auth
+	u := query.User
 
 
 	user, err := u.FirstByID(id)
 	user, err := u.FirstByID(id)
 
 
@@ -43,7 +43,7 @@ func AddUser(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	u := query.Auth
+	u := query.User
 
 
 	pwd, err := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
 	pwd, err := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
 	if err != nil {
 	if err != nil {
@@ -52,7 +52,7 @@ func AddUser(c *gin.Context) {
 	}
 	}
 	json.Password = string(pwd)
 	json.Password = string(pwd)
 
 
-	user := model.Auth{
+	user := model.User{
 		Name:     json.Name,
 		Name:     json.Name,
 		Password: json.Password,
 		Password: json.Password,
 	}
 	}
@@ -84,14 +84,14 @@ func EditUser(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	u := query.Auth
+	u := query.User
 	user, err := u.FirstByID(userId)
 	user, err := u.FirstByID(userId)
 
 
 	if err != nil {
 	if err != nil {
 		api.ErrHandler(c, err)
 		api.ErrHandler(c, err)
 		return
 		return
 	}
 	}
-	edit := &model.Auth{
+	edit := &model.User{
 		Name: json.Name,
 		Name: json.Name,
 	}
 	}
 
 
@@ -124,9 +124,9 @@ func DeleteUser(c *gin.Context) {
 		})
 		})
 		return
 		return
 	}
 	}
-	cosy.Core[model.Auth](c).Destroy()
+	cosy.Core[model.User](c).Destroy()
 }
 }
 
 
 func RecoverUser(c *gin.Context) {
 func RecoverUser(c *gin.Context) {
-	cosy.Core[model.Auth](c).Recover()
+	cosy.Core[model.User](c).Recover()
 }
 }

+ 6 - 0
app.example.ini

@@ -17,6 +17,7 @@ CertRenewalInterval  = 7
 RecursiveNameservers = 
 RecursiveNameservers = 
 SkipInstallation     = false
 SkipInstallation     = false
 Name                 = 
 Name                 = 
+InsecureSkipVerify   = false
 
 
 [nginx]
 [nginx]
 AccessLogPath = /var/log/nginx/access.log
 AccessLogPath = /var/log/nginx/access.log
@@ -59,3 +60,8 @@ MaxAttempts         = 10
 
 
 [crypto]
 [crypto]
 Secret = secret2
 Secret = secret2
+
+[webauthn]
+RPDisplayName = 
+RPID          = 
+RPOrigins     = 

+ 2 - 0
app/components.d.ts

@@ -81,6 +81,8 @@ declare module 'vue' {
     OTPInputOTPInput: typeof import('./src/components/OTPInput/OTPInput.vue')['default']
     OTPInputOTPInput: typeof import('./src/components/OTPInput/OTPInput.vue')['default']
     OTPOTPAuthorization: typeof import('./src/components/OTP/OTPAuthorization.vue')['default']
     OTPOTPAuthorization: typeof import('./src/components/OTP/OTPAuthorization.vue')['default']
     PageHeaderPageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']
     PageHeaderPageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']
+    PasskeyPasskeyRegistration: typeof import('./src/components/Passkey/PasskeyRegistration.vue')['default']
+    ReactiveFromNowReactiveFromNow: typeof import('./src/components/ReactiveFromNow/ReactiveFromNow.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     RouterView: typeof import('vue-router')['RouterView']
     SensitiveStringSensitiveString: typeof import('./src/components/SensitiveString/SensitiveString.vue')['default']
     SensitiveStringSensitiveString: typeof import('./src/components/SensitiveString/SensitiveString.vue')['default']

+ 3 - 1
app/package.json

@@ -14,6 +14,7 @@
     "@0xjacky/vue-github-button": "^3.1.1",
     "@0xjacky/vue-github-button": "^3.1.1",
     "@ant-design/icons-vue": "^7.0.1",
     "@ant-design/icons-vue": "^7.0.1",
     "@formkit/auto-animate": "^0.8.2",
     "@formkit/auto-animate": "^0.8.2",
+    "@simplewebauthn/browser": "^10.0.0",
     "@vue/reactivity": "^3.5.5",
     "@vue/reactivity": "^3.5.5",
     "@vue/shared": "^3.5.5",
     "@vue/shared": "^3.5.5",
     "@vueuse/components": "^11.0.3",
     "@vueuse/components": "^11.0.3",
@@ -46,6 +47,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@antfu/eslint-config-vue": "^0.43.1",
     "@antfu/eslint-config-vue": "^0.43.1",
+    "@simplewebauthn/types": "^10.0.0",
     "@types/lodash": "^4.17.7",
     "@types/lodash": "^4.17.7",
     "@types/nprogress": "^0.2.3",
     "@types/nprogress": "^0.2.3",
     "@types/sortablejs": "^1.15.8",
     "@types/sortablejs": "^1.15.8",
@@ -67,7 +69,7 @@
     "less": "^4.2.0",
     "less": "^4.2.0",
     "postcss": "^8.4.47",
     "postcss": "^8.4.47",
     "tailwindcss": "^3.4.11",
     "tailwindcss": "^3.4.11",
-    "typescript": "5.5.4",
+    "typescript": "5.3.3",
     "unplugin-auto-import": "^0.18.3",
     "unplugin-auto-import": "^0.18.3",
     "unplugin-vue-components": "^0.27.4",
     "unplugin-vue-components": "^0.27.4",
     "unplugin-vue-define-options": "^1.5.1",
     "unplugin-vue-define-options": "^1.5.1",

+ 189 - 164
app/pnpm-lock.yaml

@@ -13,10 +13,13 @@ importers:
         version: 3.1.1
         version: 3.1.1
       '@ant-design/icons-vue':
       '@ant-design/icons-vue':
         specifier: ^7.0.1
         specifier: ^7.0.1
-        version: 7.0.1(vue@3.5.5(typescript@5.5.4))
+        version: 7.0.1(vue@3.5.5(typescript@5.3.3))
       '@formkit/auto-animate':
       '@formkit/auto-animate':
         specifier: ^0.8.2
         specifier: ^0.8.2
         version: 0.8.2
         version: 0.8.2
+      '@simplewebauthn/browser':
+        specifier: ^10.0.0
+        version: 10.0.0
       '@vue/reactivity':
       '@vue/reactivity':
         specifier: ^3.5.5
         specifier: ^3.5.5
         version: 3.5.5
         version: 3.5.5
@@ -25,13 +28,13 @@ importers:
         version: 3.5.5
         version: 3.5.5
       '@vueuse/components':
       '@vueuse/components':
         specifier: ^11.0.3
         specifier: ^11.0.3
-        version: 11.0.3(vue@3.5.5(typescript@5.5.4))
+        version: 11.0.3(vue@3.5.5(typescript@5.3.3))
       '@vueuse/core':
       '@vueuse/core':
         specifier: ^11.0.3
         specifier: ^11.0.3
-        version: 11.0.3(vue@3.5.5(typescript@5.5.4))
+        version: 11.0.3(vue@3.5.5(typescript@5.3.3))
       '@vueuse/integrations':
       '@vueuse/integrations':
         specifier: ^11.0.3
         specifier: ^11.0.3
-        version: 11.0.3(async-validator@4.2.5)(axios@1.7.7)(nprogress@0.2.0)(sortablejs@1.15.3)(universal-cookie@7.2.0)(vue@3.5.5(typescript@5.5.4))
+        version: 11.0.3(async-validator@4.2.5)(axios@1.7.7)(nprogress@0.2.0)(sortablejs@1.15.3)(universal-cookie@7.2.0)(vue@3.5.5(typescript@5.3.3))
       '@xterm/addon-attach':
       '@xterm/addon-attach':
         specifier: ^0.11.0
         specifier: ^0.11.0
         version: 0.11.0(@xterm/xterm@5.5.0)
         version: 0.11.0(@xterm/xterm@5.5.0)
@@ -43,7 +46,7 @@ importers:
         version: 5.5.0
         version: 5.5.0
       ant-design-vue:
       ant-design-vue:
         specifier: ^4.2.4
         specifier: ^4.2.4
-        version: 4.2.4(vue@3.5.5(typescript@5.5.4))
+        version: 4.2.4(vue@3.5.5(typescript@5.3.3))
       apexcharts:
       apexcharts:
         specifier: ^3.53.0
         specifier: ^3.53.0
         version: 3.53.0
         version: 3.53.0
@@ -67,10 +70,10 @@ importers:
         version: 0.2.0
         version: 0.2.0
       pinia:
       pinia:
         specifier: ^2.2.2
         specifier: ^2.2.2
-        version: 2.2.2(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4))
+        version: 2.2.2(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3))
       pinia-plugin-persistedstate:
       pinia-plugin-persistedstate:
         specifier: ^3.2.3
         specifier: ^3.2.3
-        version: 3.2.3(pinia@2.2.2(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4)))
+        version: 3.2.3(pinia@2.2.2(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3)))
       reconnecting-websocket:
       reconnecting-websocket:
         specifier: ^4.4.0
         specifier: ^4.4.0
         version: 4.4.0
         version: 4.4.0
@@ -85,29 +88,32 @@ importers:
         version: 0.3.6
         version: 0.3.6
       vue:
       vue:
         specifier: ^3.5.5
         specifier: ^3.5.5
-        version: 3.5.5(typescript@5.5.4)
+        version: 3.5.5(typescript@5.3.3)
       vue-router:
       vue-router:
         specifier: ^4.4.5
         specifier: ^4.4.5
-        version: 4.4.5(vue@3.5.5(typescript@5.5.4))
+        version: 4.4.5(vue@3.5.5(typescript@5.3.3))
       vue3-ace-editor:
       vue3-ace-editor:
         specifier: 2.2.4
         specifier: 2.2.4
-        version: 2.2.4(ace-builds@1.36.2)(vue@3.5.5(typescript@5.5.4))
+        version: 2.2.4(ace-builds@1.36.2)(vue@3.5.5(typescript@5.3.3))
       vue3-apexcharts:
       vue3-apexcharts:
         specifier: 1.5.3
         specifier: 1.5.3
-        version: 1.5.3(apexcharts@3.53.0)(vue@3.5.5(typescript@5.5.4))
+        version: 1.5.3(apexcharts@3.53.0)(vue@3.5.5(typescript@5.3.3))
       vue3-gettext:
       vue3-gettext:
         specifier: 3.0.0-beta.6
         specifier: 3.0.0-beta.6
-        version: 3.0.0-beta.6(@vue/compiler-sfc@3.5.5)(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4))
+        version: 3.0.0-beta.6(@vue/compiler-sfc@3.5.5)(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3))
       vue3-otp-input:
       vue3-otp-input:
         specifier: ^0.5.21
         specifier: ^0.5.21
-        version: 0.5.21(vue@3.5.5(typescript@5.5.4))
+        version: 0.5.21(vue@3.5.5(typescript@5.3.3))
       vuedraggable:
       vuedraggable:
         specifier: ^4.1.0
         specifier: ^4.1.0
-        version: 4.1.0(vue@3.5.5(typescript@5.5.4))
+        version: 4.1.0(vue@3.5.5(typescript@5.3.3))
     devDependencies:
     devDependencies:
       '@antfu/eslint-config-vue':
       '@antfu/eslint-config-vue':
         specifier: ^0.43.1
         specifier: ^0.43.1
-        version: 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)
+        version: 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)
+      '@simplewebauthn/types':
+        specifier: ^10.0.0
+        version: 10.0.0
       '@types/lodash':
       '@types/lodash':
         specifier: ^4.17.7
         specifier: ^4.17.7
         version: 4.17.7
         version: 4.17.7
@@ -119,16 +125,16 @@ importers:
         version: 1.15.8
         version: 1.15.8
       '@typescript-eslint/eslint-plugin':
       '@typescript-eslint/eslint-plugin':
         specifier: ^6.21.0
         specifier: ^6.21.0
-        version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
+        version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
       '@typescript-eslint/parser':
       '@typescript-eslint/parser':
         specifier: ^6.21.0
         specifier: ^6.21.0
-        version: 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+        version: 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       '@vitejs/plugin-vue':
       '@vitejs/plugin-vue':
         specifier: ^5.1.3
         specifier: ^5.1.3
-        version: 5.1.3(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.5.4))
+        version: 5.1.3(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.3.3))
       '@vitejs/plugin-vue-jsx':
       '@vitejs/plugin-vue-jsx':
         specifier: ^4.0.1
         specifier: ^4.0.1
-        version: 4.0.1(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.5.4))
+        version: 4.0.1(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.3.3))
       '@vue/compiler-sfc':
       '@vue/compiler-sfc':
         specifier: ^3.5.5
         specifier: ^3.5.5
         version: 3.5.5
         version: 3.5.5
@@ -146,13 +152,13 @@ importers:
         version: 8.57.0
         version: 8.57.0
       eslint-import-resolver-alias:
       eslint-import-resolver-alias:
         specifier: ^1.1.2
         specifier: ^1.1.2
-        version: 1.1.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0))
+        version: 1.1.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0))
       eslint-import-resolver-typescript:
       eslint-import-resolver-typescript:
         specifier: ^3.6.3
         specifier: ^3.6.3
-        version: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0)
+        version: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0)
       eslint-plugin-import:
       eslint-plugin-import:
         specifier: ^2.30.0
         specifier: ^2.30.0
-        version: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+        version: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
       eslint-plugin-regex:
       eslint-plugin-regex:
         specifier: ^1.10.0
         specifier: ^1.10.0
         version: 1.10.0(eslint@8.57.0)
         version: 1.10.0(eslint@8.57.0)
@@ -172,26 +178,26 @@ importers:
         specifier: ^3.4.11
         specifier: ^3.4.11
         version: 3.4.11
         version: 3.4.11
       typescript:
       typescript:
-        specifier: 5.5.4
-        version: 5.5.4
+        specifier: 5.3.3
+        version: 5.3.3
       unplugin-auto-import:
       unplugin-auto-import:
         specifier: ^0.18.3
         specifier: ^0.18.3
-        version: 0.18.3(@vueuse/core@11.0.3(vue@3.5.5(typescript@5.5.4)))(rollup@4.21.3)(webpack-sources@3.2.3)
+        version: 0.18.3(@vueuse/core@11.0.3(vue@3.5.5(typescript@5.3.3)))(rollup@4.21.3)(webpack-sources@3.2.3)
       unplugin-vue-components:
       unplugin-vue-components:
         specifier: ^0.27.4
         specifier: ^0.27.4
-        version: 0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.5(typescript@5.5.4))(webpack-sources@3.2.3)
+        version: 0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.5(typescript@5.3.3))(webpack-sources@3.2.3)
       unplugin-vue-define-options:
       unplugin-vue-define-options:
         specifier: ^1.5.1
         specifier: ^1.5.1
-        version: 1.5.1(rollup@4.21.3)(vue@3.5.5(typescript@5.5.4))(webpack-sources@3.2.3)
+        version: 1.5.1(rollup@4.21.3)(vue@3.5.5(typescript@5.3.3))(webpack-sources@3.2.3)
       vite:
       vite:
         specifier: ^5.4.5
         specifier: ^5.4.5
         version: 5.4.5(@types/node@22.5.5)(less@4.2.0)
         version: 5.4.5(@types/node@22.5.5)(less@4.2.0)
       vite-svg-loader:
       vite-svg-loader:
         specifier: ^5.1.0
         specifier: ^5.1.0
-        version: 5.1.0(vue@3.5.5(typescript@5.5.4))
+        version: 5.1.0(vue@3.5.5(typescript@5.3.3))
       vue-tsc:
       vue-tsc:
         specifier: ^2.1.6
         specifier: ^2.1.6
-        version: 2.1.6(typescript@5.5.4)
+        version: 2.1.6(typescript@5.3.3)
 
 
 packages:
 packages:
 
 
@@ -684,6 +690,12 @@ packages:
   '@simonwep/pickr@1.8.2':
   '@simonwep/pickr@1.8.2':
     resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
     resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
 
 
+  '@simplewebauthn/browser@10.0.0':
+    resolution: {integrity: sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==}
+
+  '@simplewebauthn/types@10.0.0':
+    resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
+
   '@stylistic/eslint-plugin-js@0.0.4':
   '@stylistic/eslint-plugin-js@0.0.4':
     resolution: {integrity: sha512-W1rq2xxlFNhgZZJO+L59wtvlDI0xARYxx0WD8EeWNBO7NDybUSYSozCIcY9XvxQbTAsEXBjwqokeYm0crt7RxQ==}
     resolution: {integrity: sha512-W1rq2xxlFNhgZZJO+L59wtvlDI0xARYxx0WD8EeWNBO7NDybUSYSozCIcY9XvxQbTAsEXBjwqokeYm0crt7RxQ==}
 
 
@@ -2908,6 +2920,11 @@ packages:
     resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
     resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
 
 
+  typescript@5.3.3:
+    resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
   typescript@5.5.4:
   typescript@5.5.4:
     resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
     resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
     engines: {node: '>=14.17'}
     engines: {node: '>=14.17'}
@@ -3178,20 +3195,20 @@ snapshots:
 
 
   '@ant-design/icons-svg@4.4.2': {}
   '@ant-design/icons-svg@4.4.2': {}
 
 
-  '@ant-design/icons-vue@7.0.1(vue@3.5.5(typescript@5.5.4))':
+  '@ant-design/icons-vue@7.0.1(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
       '@ant-design/colors': 6.0.0
       '@ant-design/colors': 6.0.0
       '@ant-design/icons-svg': 4.4.2
       '@ant-design/icons-svg': 4.4.2
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
-  '@antfu/eslint-config-basic@0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)':
+  '@antfu/eslint-config-basic@0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@stylistic/eslint-plugin-js': 0.0.4
       '@stylistic/eslint-plugin-js': 0.0.4
       eslint: 8.57.0
       eslint: 8.57.0
-      eslint-plugin-antfu: 0.43.1(eslint@8.57.0)(typescript@5.5.4)
+      eslint-plugin-antfu: 0.43.1(eslint@8.57.0)(typescript@5.3.3)
       eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
       eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
       eslint-plugin-html: 7.1.0
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: eslint-plugin-i@2.28.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-import: eslint-plugin-i@2.28.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
       eslint-plugin-jsdoc: 46.10.1(eslint@8.57.0)
       eslint-plugin-jsdoc: 46.10.1(eslint@8.57.0)
       eslint-plugin-jsonc: 2.16.0(eslint@8.57.0)
       eslint-plugin-jsonc: 2.16.0(eslint@8.57.0)
       eslint-plugin-markdown: 3.0.1(eslint@8.57.0)
       eslint-plugin-markdown: 3.0.1(eslint@8.57.0)
@@ -3199,7 +3216,7 @@ snapshots:
       eslint-plugin-no-only-tests: 3.3.0
       eslint-plugin-no-only-tests: 3.3.0
       eslint-plugin-promise: 6.6.0(eslint@8.57.0)
       eslint-plugin-promise: 6.6.0(eslint@8.57.0)
       eslint-plugin-unicorn: 48.0.1(eslint@8.57.0)
       eslint-plugin-unicorn: 48.0.1(eslint@8.57.0)
-      eslint-plugin-unused-imports: 3.2.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)
+      eslint-plugin-unused-imports: 3.2.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)
       eslint-plugin-yml: 1.14.0(eslint@8.57.0)
       eslint-plugin-yml: 1.14.0(eslint@8.57.0)
       jsonc-eslint-parser: 2.4.0
       jsonc-eslint-parser: 2.4.0
       yaml-eslint-parser: 1.2.3
       yaml-eslint-parser: 1.2.3
@@ -3211,25 +3228,25 @@ snapshots:
       - supports-color
       - supports-color
       - typescript
       - typescript
 
 
-  '@antfu/eslint-config-ts@0.43.1(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)':
+  '@antfu/eslint-config-ts@0.43.1(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
-      '@antfu/eslint-config-basic': 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)
-      '@stylistic/eslint-plugin-ts': 0.0.4(eslint@8.57.0)(typescript@5.5.4)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@antfu/eslint-config-basic': 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)
+      '@stylistic/eslint-plugin-ts': 0.0.4(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
-      eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
-      typescript: 5.5.4
+      eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - eslint-import-resolver-webpack
       - jest
       - jest
       - supports-color
       - supports-color
 
 
-  '@antfu/eslint-config-vue@0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)':
+  '@antfu/eslint-config-vue@0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
-      '@antfu/eslint-config-basic': 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)
-      '@antfu/eslint-config-ts': 0.43.1(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.4)
+      '@antfu/eslint-config-basic': 0.43.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)
+      '@antfu/eslint-config-ts': 0.43.1(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
       eslint-plugin-vue: 9.28.0(eslint@8.57.0)
       eslint-plugin-vue: 9.28.0(eslint@8.57.0)
       local-pkg: 0.4.3
       local-pkg: 0.4.3
@@ -3651,6 +3668,12 @@ snapshots:
       core-js: 3.38.1
       core-js: 3.38.1
       nanopop: 2.4.2
       nanopop: 2.4.2
 
 
+  '@simplewebauthn/browser@10.0.0':
+    dependencies:
+      '@simplewebauthn/types': 10.0.0
+
+  '@simplewebauthn/types@10.0.0': {}
+
   '@stylistic/eslint-plugin-js@0.0.4':
   '@stylistic/eslint-plugin-js@0.0.4':
     dependencies:
     dependencies:
       acorn: 8.12.1
       acorn: 8.12.1
@@ -3660,15 +3683,15 @@ snapshots:
       esutils: 2.0.3
       esutils: 2.0.3
       graphemer: 1.4.0
       graphemer: 1.4.0
 
 
-  '@stylistic/eslint-plugin-ts@0.0.4(eslint@8.57.0)(typescript@5.5.4)':
+  '@stylistic/eslint-plugin-ts@0.0.4(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@stylistic/eslint-plugin-js': 0.0.4
       '@stylistic/eslint-plugin-js': 0.0.4
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/scope-manager': 6.21.0
-      '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
       graphemer: 1.4.0
       graphemer: 1.4.0
-      typescript: 5.5.4
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -3717,13 +3740,13 @@ snapshots:
 
 
   '@types/web-bluetooth@0.0.20': {}
   '@types/web-bluetooth@0.0.20': {}
 
 
-  '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)':
+  '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@eslint-community/regexpp': 4.11.1
       '@eslint-community/regexpp': 4.11.1
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/scope-manager': 6.21.0
-      '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       '@typescript-eslint/visitor-keys': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.7
       debug: 4.3.7
       eslint: 8.57.0
       eslint: 8.57.0
@@ -3731,22 +3754,22 @@ snapshots:
       ignore: 5.3.2
       ignore: 5.3.2
       natural-compare: 1.4.0
       natural-compare: 1.4.0
       semver: 7.6.3
       semver: 7.6.3
-      ts-api-utils: 1.3.0(typescript@5.5.4)
+      ts-api-utils: 1.3.0(typescript@5.3.3)
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4)':
+  '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/types': 6.21.0
-      '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4)
+      '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
       '@typescript-eslint/visitor-keys': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.7
       debug: 4.3.7
       eslint: 8.57.0
       eslint: 8.57.0
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -3760,15 +3783,15 @@ snapshots:
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
 
 
-  '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.5.4)':
+  '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
-      '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       debug: 4.3.7
       debug: 4.3.7
       eslint: 8.57.0
       eslint: 8.57.0
-      ts-api-utils: 1.3.0(typescript@5.5.4)
+      ts-api-utils: 1.3.0(typescript@5.3.3)
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -3776,7 +3799,7 @@ snapshots:
 
 
   '@typescript-eslint/types@6.21.0': {}
   '@typescript-eslint/types@6.21.0': {}
 
 
-  '@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4)':
+  '@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/visitor-keys': 5.62.0
       '@typescript-eslint/visitor-keys': 5.62.0
@@ -3784,13 +3807,13 @@ snapshots:
       globby: 11.1.0
       globby: 11.1.0
       is-glob: 4.0.3
       is-glob: 4.0.3
       semver: 7.6.3
       semver: 7.6.3
-      tsutils: 3.21.0(typescript@5.5.4)
+      tsutils: 3.21.0(typescript@5.3.3)
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  '@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4)':
+  '@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
@@ -3799,20 +3822,20 @@ snapshots:
       is-glob: 4.0.3
       is-glob: 4.0.3
       minimatch: 9.0.3
       minimatch: 9.0.3
       semver: 7.6.3
       semver: 7.6.3
-      ts-api-utils: 1.3.0(typescript@5.5.4)
+      ts-api-utils: 1.3.0(typescript@5.3.3)
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  '@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.5.4)':
+  '@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
       '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
       '@types/json-schema': 7.0.15
       '@types/json-schema': 7.0.15
       '@types/semver': 7.5.8
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 5.62.0
       '@typescript-eslint/scope-manager': 5.62.0
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/types': 5.62.0
-      '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4)
+      '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
       eslint-scope: 5.1.1
       eslint-scope: 5.1.1
       semver: 7.6.3
       semver: 7.6.3
@@ -3820,14 +3843,14 @@ snapshots:
       - supports-color
       - supports-color
       - typescript
       - typescript
 
 
-  '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.5.4)':
+  '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
       '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
       '@types/json-schema': 7.0.15
       '@types/json-schema': 7.0.15
       '@types/semver': 7.5.8
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/types': 6.21.0
-      '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4)
+      '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
       semver: 7.6.3
       semver: 7.6.3
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -3846,20 +3869,20 @@ snapshots:
 
 
   '@ungap/structured-clone@1.2.0': {}
   '@ungap/structured-clone@1.2.0': {}
 
 
-  '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.5.4))':
+  '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
       '@babel/core': 7.25.2
       '@babel/core': 7.25.2
       '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2)
       '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2)
       '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.25.2)
       '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.25.2)
       vite: 5.4.5(@types/node@22.5.5)(less@4.2.0)
       vite: 5.4.5(@types/node@22.5.5)(less@4.2.0)
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  '@vitejs/plugin-vue@5.1.3(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.5.4))':
+  '@vitejs/plugin-vue@5.1.3(vite@5.4.5(@types/node@22.5.5)(less@4.2.0))(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
       vite: 5.4.5(@types/node@22.5.5)(less@4.2.0)
       vite: 5.4.5(@types/node@22.5.5)(less@4.2.0)
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
   '@volar/language-core@2.4.5':
   '@volar/language-core@2.4.5':
     dependencies:
     dependencies:
@@ -3873,7 +3896,7 @@ snapshots:
       path-browserify: 1.0.1
       path-browserify: 1.0.1
       vscode-uri: 3.0.8
       vscode-uri: 3.0.8
 
 
-  '@vue-macros/common@1.14.0(rollup@4.21.3)(vue@3.5.5(typescript@5.5.4))':
+  '@vue-macros/common@1.14.0(rollup@4.21.3)(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
       '@babel/types': 7.25.6
       '@babel/types': 7.25.6
       '@rollup/pluginutils': 5.1.0(rollup@4.21.3)
       '@rollup/pluginutils': 5.1.0(rollup@4.21.3)
@@ -3882,7 +3905,7 @@ snapshots:
       local-pkg: 0.5.0
       local-pkg: 0.5.0
       magic-string-ast: 0.6.2
       magic-string-ast: 0.6.2
     optionalDependencies:
     optionalDependencies:
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - rollup
       - rollup
 
 
@@ -3953,7 +3976,7 @@ snapshots:
 
 
   '@vue/devtools-api@6.6.4': {}
   '@vue/devtools-api@6.6.4': {}
 
 
-  '@vue/language-core@2.1.6(typescript@5.5.4)':
+  '@vue/language-core@2.1.6(typescript@5.3.3)':
     dependencies:
     dependencies:
       '@volar/language-core': 2.4.5
       '@volar/language-core': 2.4.5
       '@vue/compiler-dom': 3.5.5
       '@vue/compiler-dom': 3.5.5
@@ -3964,7 +3987,7 @@ snapshots:
       muggle-string: 0.4.1
       muggle-string: 0.4.1
       path-browserify: 1.0.1
       path-browserify: 1.0.1
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
   '@vue/reactivity@3.5.5':
   '@vue/reactivity@3.5.5':
     dependencies:
     dependencies:
@@ -3982,40 +4005,40 @@ snapshots:
       '@vue/shared': 3.5.5
       '@vue/shared': 3.5.5
       csstype: 3.1.3
       csstype: 3.1.3
 
 
-  '@vue/server-renderer@3.5.5(vue@3.5.5(typescript@5.5.4))':
+  '@vue/server-renderer@3.5.5(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
       '@vue/compiler-ssr': 3.5.5
       '@vue/compiler-ssr': 3.5.5
       '@vue/shared': 3.5.5
       '@vue/shared': 3.5.5
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
   '@vue/shared@3.5.5': {}
   '@vue/shared@3.5.5': {}
 
 
   '@vue/tsconfig@0.5.1': {}
   '@vue/tsconfig@0.5.1': {}
 
 
-  '@vueuse/components@11.0.3(vue@3.5.5(typescript@5.5.4))':
+  '@vueuse/components@11.0.3(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
-      '@vueuse/core': 11.0.3(vue@3.5.5(typescript@5.5.4))
-      '@vueuse/shared': 11.0.3(vue@3.5.5(typescript@5.5.4))
-      vue-demi: 0.14.10(vue@3.5.5(typescript@5.5.4))
+      '@vueuse/core': 11.0.3(vue@3.5.5(typescript@5.3.3))
+      '@vueuse/shared': 11.0.3(vue@3.5.5(typescript@5.3.3))
+      vue-demi: 0.14.10(vue@3.5.5(typescript@5.3.3))
     transitivePeerDependencies:
     transitivePeerDependencies:
       - '@vue/composition-api'
       - '@vue/composition-api'
       - vue
       - vue
 
 
-  '@vueuse/core@11.0.3(vue@3.5.5(typescript@5.5.4))':
+  '@vueuse/core@11.0.3(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
       '@types/web-bluetooth': 0.0.20
       '@types/web-bluetooth': 0.0.20
       '@vueuse/metadata': 11.0.3
       '@vueuse/metadata': 11.0.3
-      '@vueuse/shared': 11.0.3(vue@3.5.5(typescript@5.5.4))
-      vue-demi: 0.14.10(vue@3.5.5(typescript@5.5.4))
+      '@vueuse/shared': 11.0.3(vue@3.5.5(typescript@5.3.3))
+      vue-demi: 0.14.10(vue@3.5.5(typescript@5.3.3))
     transitivePeerDependencies:
     transitivePeerDependencies:
       - '@vue/composition-api'
       - '@vue/composition-api'
       - vue
       - vue
 
 
-  '@vueuse/integrations@11.0.3(async-validator@4.2.5)(axios@1.7.7)(nprogress@0.2.0)(sortablejs@1.15.3)(universal-cookie@7.2.0)(vue@3.5.5(typescript@5.5.4))':
+  '@vueuse/integrations@11.0.3(async-validator@4.2.5)(axios@1.7.7)(nprogress@0.2.0)(sortablejs@1.15.3)(universal-cookie@7.2.0)(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
-      '@vueuse/core': 11.0.3(vue@3.5.5(typescript@5.5.4))
-      '@vueuse/shared': 11.0.3(vue@3.5.5(typescript@5.5.4))
-      vue-demi: 0.14.10(vue@3.5.5(typescript@5.5.4))
+      '@vueuse/core': 11.0.3(vue@3.5.5(typescript@5.3.3))
+      '@vueuse/shared': 11.0.3(vue@3.5.5(typescript@5.3.3))
+      vue-demi: 0.14.10(vue@3.5.5(typescript@5.3.3))
     optionalDependencies:
     optionalDependencies:
       async-validator: 4.2.5
       async-validator: 4.2.5
       axios: 1.7.7
       axios: 1.7.7
@@ -4028,9 +4051,9 @@ snapshots:
 
 
   '@vueuse/metadata@11.0.3': {}
   '@vueuse/metadata@11.0.3': {}
 
 
-  '@vueuse/shared@11.0.3(vue@3.5.5(typescript@5.5.4))':
+  '@vueuse/shared@11.0.3(vue@3.5.5(typescript@5.3.3))':
     dependencies:
     dependencies:
-      vue-demi: 0.14.10(vue@3.5.5(typescript@5.5.4))
+      vue-demi: 0.14.10(vue@3.5.5(typescript@5.3.3))
     transitivePeerDependencies:
     transitivePeerDependencies:
       - '@vue/composition-api'
       - '@vue/composition-api'
       - vue
       - vue
@@ -4076,10 +4099,10 @@ snapshots:
 
 
   ansi-styles@6.2.1: {}
   ansi-styles@6.2.1: {}
 
 
-  ant-design-vue@4.2.4(vue@3.5.5(typescript@5.5.4)):
+  ant-design-vue@4.2.4(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       '@ant-design/colors': 6.0.0
       '@ant-design/colors': 6.0.0
-      '@ant-design/icons-vue': 7.0.1(vue@3.5.5(typescript@5.5.4))
+      '@ant-design/icons-vue': 7.0.1(vue@3.5.5(typescript@5.3.3))
       '@babel/runtime': 7.25.6
       '@babel/runtime': 7.25.6
       '@ctrl/tinycolor': 3.6.1
       '@ctrl/tinycolor': 3.6.1
       '@emotion/hash': 0.9.2
       '@emotion/hash': 0.9.2
@@ -4098,8 +4121,8 @@ snapshots:
       shallow-equal: 1.2.1
       shallow-equal: 1.2.1
       stylis: 4.3.4
       stylis: 4.3.4
       throttle-debounce: 5.0.2
       throttle-debounce: 5.0.2
-      vue: 3.5.5(typescript@5.5.4)
-      vue-types: 3.0.2(vue@3.5.5(typescript@5.5.4))
+      vue: 3.5.5(typescript@5.3.3)
+      vue-types: 3.0.2(vue@3.5.5(typescript@5.3.3))
       warning: 4.0.3
       warning: 4.0.3
 
 
   any-promise@1.3.0: {}
   any-promise@1.3.0: {}
@@ -4347,14 +4370,14 @@ snapshots:
 
 
   core-js@3.38.1: {}
   core-js@3.38.1: {}
 
 
-  cosmiconfig@9.0.0(typescript@5.5.4):
+  cosmiconfig@9.0.0(typescript@5.3.3):
     dependencies:
     dependencies:
       env-paths: 2.2.1
       env-paths: 2.2.1
       import-fresh: 3.3.0
       import-fresh: 3.3.0
       js-yaml: 4.1.0
       js-yaml: 4.1.0
       parse-json: 5.2.0
       parse-json: 5.2.0
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
   crc-32@1.2.2: {}
   crc-32@1.2.2: {}
 
 
@@ -4624,9 +4647,9 @@ snapshots:
       eslint: 8.57.0
       eslint: 8.57.0
       semver: 7.6.3
       semver: 7.6.3
 
 
-  eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)):
+  eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)):
     dependencies:
     dependencies:
-      eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+      eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
 
 
   eslint-import-resolver-node@0.3.9:
   eslint-import-resolver-node@0.3.9:
     dependencies:
     dependencies:
@@ -4636,39 +4659,39 @@ snapshots:
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0):
+  eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0):
     dependencies:
     dependencies:
       '@nolyfill/is-core-module': 1.0.39
       '@nolyfill/is-core-module': 1.0.39
       debug: 4.3.7
       debug: 4.3.7
       enhanced-resolve: 5.17.1
       enhanced-resolve: 5.17.1
       eslint: 8.57.0
       eslint: 8.57.0
-      eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
+      eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
       fast-glob: 3.3.2
       fast-glob: 3.3.2
       get-tsconfig: 4.8.1
       get-tsconfig: 4.8.1
       is-bun-module: 1.2.1
       is-bun-module: 1.2.1
       is-glob: 4.0.3
       is-glob: 4.0.3
     optionalDependencies:
     optionalDependencies:
-      eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+      eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - '@typescript-eslint/parser'
       - '@typescript-eslint/parser'
       - eslint-import-resolver-node
       - eslint-import-resolver-node
       - eslint-import-resolver-webpack
       - eslint-import-resolver-webpack
       - supports-color
       - supports-color
 
 
-  eslint-module-utils@2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0):
+  eslint-module-utils@2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0):
     dependencies:
     dependencies:
       debug: 3.2.7
       debug: 3.2.7
     optionalDependencies:
     optionalDependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0)
+      eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  eslint-plugin-antfu@0.43.1(eslint@8.57.0)(typescript@5.5.4):
+  eslint-plugin-antfu@0.43.1(eslint@8.57.0)(typescript@5.3.3):
     dependencies:
     dependencies:
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - eslint
       - eslint
       - supports-color
       - supports-color
@@ -4691,13 +4714,13 @@ snapshots:
     dependencies:
     dependencies:
       htmlparser2: 8.0.2
       htmlparser2: 8.0.2
 
 
-  eslint-plugin-i@2.28.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0):
+  eslint-plugin-i@2.28.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0):
     dependencies:
     dependencies:
       debug: 3.2.7
       debug: 3.2.7
       doctrine: 2.1.0
       doctrine: 2.1.0
       eslint: 8.57.0
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
+      eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
       get-tsconfig: 4.8.1
       get-tsconfig: 4.8.1
       is-glob: 4.0.3
       is-glob: 4.0.3
       minimatch: 3.1.2
       minimatch: 3.1.2
@@ -4709,7 +4732,7 @@ snapshots:
       - eslint-import-resolver-webpack
       - eslint-import-resolver-webpack
       - supports-color
       - supports-color
 
 
-  eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0):
+  eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0):
     dependencies:
     dependencies:
       '@rtsao/scc': 1.1.0
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
       array-includes: 3.1.8
@@ -4720,7 +4743,7 @@ snapshots:
       doctrine: 2.1.0
       doctrine: 2.1.0
       eslint: 8.57.0
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
+      eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0)
       hasown: 2.0.2
       hasown: 2.0.2
       is-core-module: 2.15.1
       is-core-module: 2.15.1
       is-glob: 4.0.3
       is-glob: 4.0.3
@@ -4731,18 +4754,18 @@ snapshots:
       semver: 6.3.1
       semver: 6.3.1
       tsconfig-paths: 3.15.0
       tsconfig-paths: 3.15.0
     optionalDependencies:
     optionalDependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - eslint-import-resolver-webpack
       - supports-color
       - supports-color
 
 
-  eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4):
+  eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3):
     dependencies:
     dependencies:
-      '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3)
       eslint: 8.57.0
       eslint: 8.57.0
     optionalDependencies:
     optionalDependencies:
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
       - typescript
       - typescript
@@ -4828,12 +4851,12 @@ snapshots:
       semver: 7.6.3
       semver: 7.6.3
       strip-indent: 3.0.0
       strip-indent: 3.0.0
 
 
-  eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0):
+  eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0):
     dependencies:
     dependencies:
       eslint: 8.57.0
       eslint: 8.57.0
       eslint-rule-composer: 0.3.0
       eslint-rule-composer: 0.3.0
     optionalDependencies:
     optionalDependencies:
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
 
 
   eslint-plugin-vue@9.28.0(eslint@8.57.0):
   eslint-plugin-vue@9.28.0(eslint@8.57.0):
     dependencies:
     dependencies:
@@ -5054,7 +5077,7 @@ snapshots:
       glob: 7.2.3
       glob: 7.2.3
       parse5: 6.0.1
       parse5: 6.0.1
       pofile: 1.0.11
       pofile: 1.0.11
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
   github-buttons@2.29.0: {}
   github-buttons@2.29.0: {}
 
 
@@ -5659,17 +5682,17 @@ snapshots:
 
 
   pify@4.0.1: {}
   pify@4.0.1: {}
 
 
-  pinia-plugin-persistedstate@3.2.3(pinia@2.2.2(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4))):
+  pinia-plugin-persistedstate@3.2.3(pinia@2.2.2(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3))):
     dependencies:
     dependencies:
-      pinia: 2.2.2(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4))
+      pinia: 2.2.2(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3))
 
 
-  pinia@2.2.2(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4)):
+  pinia@2.2.2(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       '@vue/devtools-api': 6.6.4
       '@vue/devtools-api': 6.6.4
-      vue: 3.5.5(typescript@5.5.4)
-      vue-demi: 0.14.10(vue@3.5.5(typescript@5.5.4))
+      vue: 3.5.5(typescript@5.3.3)
+      vue-demi: 0.14.10(vue@3.5.5(typescript@5.3.3))
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
   pirates@4.0.6: {}
   pirates@4.0.6: {}
 
 
@@ -6103,9 +6126,9 @@ snapshots:
     dependencies:
     dependencies:
       is-number: 7.0.0
       is-number: 7.0.0
 
 
-  ts-api-utils@1.3.0(typescript@5.5.4):
+  ts-api-utils@1.3.0(typescript@5.3.3):
     dependencies:
     dependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
   ts-interface-checker@0.1.13: {}
   ts-interface-checker@0.1.13: {}
 
 
@@ -6120,10 +6143,10 @@ snapshots:
 
 
   tslib@2.7.0: {}
   tslib@2.7.0: {}
 
 
-  tsutils@3.21.0(typescript@5.5.4):
+  tsutils@3.21.0(typescript@5.3.3):
     dependencies:
     dependencies:
       tslib: 1.14.1
       tslib: 1.14.1
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
   type-check@0.4.0:
   type-check@0.4.0:
     dependencies:
     dependencies:
@@ -6167,6 +6190,8 @@ snapshots:
       is-typed-array: 1.1.13
       is-typed-array: 1.1.13
       possible-typed-array-names: 1.0.0
       possible-typed-array-names: 1.0.0
 
 
+  typescript@5.3.3: {}
+
   typescript@5.5.4: {}
   typescript@5.5.4: {}
 
 
   typical@4.0.0: {}
   typical@4.0.0: {}
@@ -6210,7 +6235,7 @@ snapshots:
       '@types/cookie': 0.6.0
       '@types/cookie': 0.6.0
       cookie: 0.6.0
       cookie: 0.6.0
 
 
-  unplugin-auto-import@0.18.3(@vueuse/core@11.0.3(vue@3.5.5(typescript@5.5.4)))(rollup@4.21.3)(webpack-sources@3.2.3):
+  unplugin-auto-import@0.18.3(@vueuse/core@11.0.3(vue@3.5.5(typescript@5.3.3)))(rollup@4.21.3)(webpack-sources@3.2.3):
     dependencies:
     dependencies:
       '@antfu/utils': 0.7.10
       '@antfu/utils': 0.7.10
       '@rollup/pluginutils': 5.1.0(rollup@4.21.3)
       '@rollup/pluginutils': 5.1.0(rollup@4.21.3)
@@ -6221,12 +6246,12 @@ snapshots:
       unimport: 3.12.0(rollup@4.21.3)(webpack-sources@3.2.3)
       unimport: 3.12.0(rollup@4.21.3)(webpack-sources@3.2.3)
       unplugin: 1.14.1(webpack-sources@3.2.3)
       unplugin: 1.14.1(webpack-sources@3.2.3)
     optionalDependencies:
     optionalDependencies:
-      '@vueuse/core': 11.0.3(vue@3.5.5(typescript@5.5.4))
+      '@vueuse/core': 11.0.3(vue@3.5.5(typescript@5.3.3))
     transitivePeerDependencies:
     transitivePeerDependencies:
       - rollup
       - rollup
       - webpack-sources
       - webpack-sources
 
 
-  unplugin-vue-components@0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.5(typescript@5.5.4))(webpack-sources@3.2.3):
+  unplugin-vue-components@0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.5(typescript@5.3.3))(webpack-sources@3.2.3):
     dependencies:
     dependencies:
       '@antfu/utils': 0.7.10
       '@antfu/utils': 0.7.10
       '@rollup/pluginutils': 5.1.0(rollup@4.21.3)
       '@rollup/pluginutils': 5.1.0(rollup@4.21.3)
@@ -6238,7 +6263,7 @@ snapshots:
       minimatch: 9.0.5
       minimatch: 9.0.5
       mlly: 1.7.1
       mlly: 1.7.1
       unplugin: 1.14.1(webpack-sources@3.2.3)
       unplugin: 1.14.1(webpack-sources@3.2.3)
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
     optionalDependencies:
     optionalDependencies:
       '@babel/parser': 7.25.6
       '@babel/parser': 7.25.6
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -6246,9 +6271,9 @@ snapshots:
       - supports-color
       - supports-color
       - webpack-sources
       - webpack-sources
 
 
-  unplugin-vue-define-options@1.5.1(rollup@4.21.3)(vue@3.5.5(typescript@5.5.4))(webpack-sources@3.2.3):
+  unplugin-vue-define-options@1.5.1(rollup@4.21.3)(vue@3.5.5(typescript@5.3.3))(webpack-sources@3.2.3):
     dependencies:
     dependencies:
-      '@vue-macros/common': 1.14.0(rollup@4.21.3)(vue@3.5.5(typescript@5.5.4))
+      '@vue-macros/common': 1.14.0(rollup@4.21.3)(vue@3.5.5(typescript@5.3.3))
       ast-walker-scope: 0.6.2
       ast-walker-scope: 0.6.2
       unplugin: 1.14.1(webpack-sources@3.2.3)
       unplugin: 1.14.1(webpack-sources@3.2.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -6287,10 +6312,10 @@ snapshots:
       node-object-hash: 3.0.0
       node-object-hash: 3.0.0
       typescript: 5.5.4
       typescript: 5.5.4
 
 
-  vite-svg-loader@5.1.0(vue@3.5.5(typescript@5.5.4)):
+  vite-svg-loader@5.1.0(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       svgo: 3.3.2
       svgo: 3.3.2
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
   vite@5.4.5(@types/node@22.5.5)(less@4.2.0):
   vite@5.4.5(@types/node@22.5.5)(less@4.2.0):
     dependencies:
     dependencies:
@@ -6304,9 +6329,9 @@ snapshots:
 
 
   vscode-uri@3.0.8: {}
   vscode-uri@3.0.8: {}
 
 
-  vue-demi@0.14.10(vue@3.5.5(typescript@5.5.4)):
+  vue-demi@0.14.10(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
   vue-eslint-parser@9.4.3(eslint@8.57.0):
   vue-eslint-parser@9.4.3(eslint@8.57.0):
     dependencies:
     dependencies:
@@ -6321,67 +6346,67 @@ snapshots:
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  vue-router@4.4.5(vue@3.5.5(typescript@5.5.4)):
+  vue-router@4.4.5(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       '@vue/devtools-api': 6.6.4
       '@vue/devtools-api': 6.6.4
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
-  vue-tsc@2.1.6(typescript@5.5.4):
+  vue-tsc@2.1.6(typescript@5.3.3):
     dependencies:
     dependencies:
       '@volar/typescript': 2.4.5
       '@volar/typescript': 2.4.5
-      '@vue/language-core': 2.1.6(typescript@5.5.4)
+      '@vue/language-core': 2.1.6(typescript@5.3.3)
       semver: 7.6.3
       semver: 7.6.3
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
-  vue-types@3.0.2(vue@3.5.5(typescript@5.5.4)):
+  vue-types@3.0.2(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       is-plain-object: 3.0.1
       is-plain-object: 3.0.1
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
-  vue3-ace-editor@2.2.4(ace-builds@1.36.2)(vue@3.5.5(typescript@5.5.4)):
+  vue3-ace-editor@2.2.4(ace-builds@1.36.2)(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       ace-builds: 1.36.2
       ace-builds: 1.36.2
       resize-observer-polyfill: 1.5.1
       resize-observer-polyfill: 1.5.1
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
-  vue3-apexcharts@1.5.3(apexcharts@3.53.0)(vue@3.5.5(typescript@5.5.4)):
+  vue3-apexcharts@1.5.3(apexcharts@3.53.0)(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       apexcharts: 3.53.0
       apexcharts: 3.53.0
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
-  vue3-gettext@3.0.0-beta.6(@vue/compiler-sfc@3.5.5)(typescript@5.5.4)(vue@3.5.5(typescript@5.5.4)):
+  vue3-gettext@3.0.0-beta.6(@vue/compiler-sfc@3.5.5)(typescript@5.3.3)(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       '@vue/compiler-sfc': 3.5.5
       '@vue/compiler-sfc': 3.5.5
       chalk: 4.1.2
       chalk: 4.1.2
       command-line-args: 5.2.1
       command-line-args: 5.2.1
-      cosmiconfig: 9.0.0(typescript@5.5.4)
+      cosmiconfig: 9.0.0(typescript@5.3.3)
       gettext-extractor: 3.8.0
       gettext-extractor: 3.8.0
       glob: 10.4.5
       glob: 10.4.5
       parse5: 6.0.1
       parse5: 6.0.1
       parse5-htmlparser2-tree-adapter: 6.0.1
       parse5-htmlparser2-tree-adapter: 6.0.1
       pofile: 1.1.4
       pofile: 1.1.4
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - typescript
       - typescript
 
 
-  vue3-otp-input@0.5.21(vue@3.5.5(typescript@5.5.4)):
+  vue3-otp-input@0.5.21(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
-  vue@3.5.5(typescript@5.5.4):
+  vue@3.5.5(typescript@5.3.3):
     dependencies:
     dependencies:
       '@vue/compiler-dom': 3.5.5
       '@vue/compiler-dom': 3.5.5
       '@vue/compiler-sfc': 3.5.5
       '@vue/compiler-sfc': 3.5.5
       '@vue/runtime-dom': 3.5.5
       '@vue/runtime-dom': 3.5.5
-      '@vue/server-renderer': 3.5.5(vue@3.5.5(typescript@5.5.4))
+      '@vue/server-renderer': 3.5.5(vue@3.5.5(typescript@5.3.3))
       '@vue/shared': 3.5.5
       '@vue/shared': 3.5.5
     optionalDependencies:
     optionalDependencies:
-      typescript: 5.5.4
+      typescript: 5.3.3
 
 
-  vuedraggable@4.1.0(vue@3.5.5(typescript@5.5.4)):
+  vuedraggable@4.1.0(vue@3.5.5(typescript@5.3.3)):
     dependencies:
     dependencies:
       sortablejs: 1.14.0
       sortablejs: 1.14.0
-      vue: 3.5.5(typescript@5.5.4)
+      vue: 3.5.5(typescript@5.3.3)
 
 
   warning@4.0.3:
   warning@4.0.3:
     dependencies:
     dependencies:

+ 37 - 0
app/src/api/2fa.ts

@@ -0,0 +1,37 @@
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types'
+import http from '@/lib/http'
+
+export interface TwoFAStatusResponse {
+  enabled: boolean
+  otp_status: boolean
+  passkey_status: boolean
+}
+
+const twoFA = {
+  status(): Promise<TwoFAStatusResponse> {
+    return http.get('/2fa_status')
+  },
+  start_secure_session_by_otp(passcode: string, recovery_code: string): Promise<{ session_id: string }> {
+    return http.post('/2fa_secure_session/otp', {
+      otp: passcode,
+      recovery_code,
+    })
+  },
+  secure_session_status(): Promise<{ status: boolean }> {
+    return http.get('/2fa_secure_session/status')
+  },
+  begin_start_secure_session_by_passkey() {
+    return http.get('/2fa_secure_session/passkey')
+  },
+  finish_start_secure_session_by_passkey(data: { session_id: string; options: AuthenticationResponseJSON }): Promise<{
+    session_id: string
+  }> {
+    return http.post('/2fa_secure_session/passkey', data.options, {
+      headers: {
+        'X-Passkey-Session-Id': data.session_id,
+      },
+    })
+  },
+}
+
+export default twoFA

+ 11 - 0
app/src/api/auth.ts

@@ -1,3 +1,4 @@
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types'
 import http from '@/lib/http'
 import http from '@/lib/http'
 import { useUserStore } from '@/pinia'
 import { useUserStore } from '@/pinia'
 
 
@@ -37,6 +38,16 @@ const auth = {
   async get_casdoor_uri(): Promise<{ uri: string }> {
   async get_casdoor_uri(): Promise<{ uri: string }> {
     return http.get('/casdoor_uri')
     return http.get('/casdoor_uri')
   },
   },
+  begin_passkey_login() {
+    return http.get('/begin_passkey_login')
+  },
+  finish_passkey_login(data: { session_id: string; options: AuthenticationResponseJSON }) {
+    return http.post('/finish_passkey_login', data.options, {
+      headers: {
+        'X-Passkey-Session-Id': data.session_id,
+      },
+    })
+  },
 }
 }
 
 
 export default auth
 export default auth

+ 0 - 12
app/src/api/otp.ts

@@ -6,9 +6,6 @@ export interface OTPGenerateSecretResponse {
 }
 }
 
 
 const otp = {
 const otp = {
-  status(): Promise<{ status: boolean }> {
-    return http.get('/otp_status')
-  },
   generate_secret(): Promise<OTPGenerateSecretResponse> {
   generate_secret(): Promise<OTPGenerateSecretResponse> {
     return http.get('/otp_secret')
     return http.get('/otp_secret')
   },
   },
@@ -18,15 +15,6 @@ const otp = {
   reset(recovery_code: string) {
   reset(recovery_code: string) {
     return http.post('/otp_reset', { recovery_code })
     return http.post('/otp_reset', { recovery_code })
   },
   },
-  start_secure_session(passcode: string, recovery_code: string): Promise<{ session_id: string }> {
-    return http.post('/otp_secure_session', {
-      otp: passcode,
-      recovery_code,
-    })
-  },
-  secure_session_status() {
-    return http.get('/otp_secure_session_status')
-  },
 }
 }
 
 
 export default otp
 export default otp

+ 36 - 0
app/src/api/passkey.ts

@@ -0,0 +1,36 @@
+import type { RegistrationResponseJSON } from '@simplewebauthn/types'
+import http from '@/lib/http'
+import type { ModelBase } from '@/api/curd'
+
+export interface Passkey extends ModelBase {
+  name: string
+  user_id: string
+  raw_id: string
+}
+
+const passkey = {
+  begin_registration() {
+    return http.get('/begin_passkey_register')
+  },
+  finish_registration(attestationResponse: RegistrationResponseJSON, passkeyName: string) {
+    return http.post('/finish_passkey_register', attestationResponse, {
+      params: {
+        name: passkeyName,
+      },
+    })
+  },
+  get_list() {
+    return http.get('/passkeys')
+  },
+  update(passkeyId: number, data: Passkey) {
+    return http.post(`/passkeys/${passkeyId}`, data)
+  },
+  remove(passkeyId: number) {
+    return http.delete(`/passkeys/${passkeyId}`)
+  },
+  get_config_status(): Promise<{ status: boolean }> {
+    return http.get('/passkeys/config')
+  },
+}
+
+export default passkey

+ 136 - 0
app/src/components/2FA/Authorization.vue

@@ -0,0 +1,136 @@
+<script setup lang="ts">
+import { KeyOutlined } from '@ant-design/icons-vue'
+import { startAuthentication } from '@simplewebauthn/browser'
+import { message } from 'ant-design-vue'
+import OTPInput from '@/components/OTPInput/OTPInput.vue'
+import type { TwoFAStatusResponse } from '@/api/2fa'
+import twoFA from '@/api/2fa'
+import { useUserStore } from '@/pinia'
+
+defineProps<{
+  twoFAStatus: TwoFAStatusResponse
+}>()
+
+const emit = defineEmits(['submitOTP', 'submitSecureSessionID'])
+
+const user = useUserStore()
+const refOTP = ref()
+const useRecoveryCode = ref(false)
+const passcode = ref('')
+const recoveryCode = ref('')
+const passkeyLoading = ref(false)
+
+function clickUseRecoveryCode() {
+  passcode.value = ''
+  useRecoveryCode.value = true
+}
+
+function clickUseOTP() {
+  passcode.value = ''
+  useRecoveryCode.value = false
+}
+
+function onSubmit() {
+  emit('submitOTP', passcode.value, recoveryCode.value)
+}
+
+function clearInput() {
+  refOTP.value?.clearInput()
+}
+
+defineExpose({
+  clearInput,
+})
+
+async function passkeyAuthenticate() {
+  passkeyLoading.value = true
+  try {
+    const begin = await twoFA.begin_start_secure_session_by_passkey()
+    const asseResp = await startAuthentication(begin.options.publicKey)
+
+    const r = await twoFA.finish_start_secure_session_by_passkey({
+      session_id: begin.session_id,
+      options: asseResp,
+    })
+
+    emit('submitSecureSessionID', r.session_id)
+  }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  catch (e: any) {
+    message.error($gettext(e.message ?? 'Server error'))
+  }
+  passkeyLoading.value = false
+}
+
+onMounted(() => {
+  if (user.passkeyLoginAvailable)
+    passkeyAuthenticate()
+})
+</script>
+
+<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>
+
+    <div
+      v-if="twoFAStatus.passkey_status"
+      class="flex flex-col justify-center"
+    >
+      <ADivider v-if="twoFAStatus.otp_status">
+        <div class="text-sm font-normal opacity-75">
+          {{ $gettext('Or') }}
+        </div>
+      </ADivider>
+
+      <AButton
+        :loading="passkeyLoading"
+        @click="passkeyAuthenticate"
+      >
+        <KeyOutlined />
+        {{ $gettext('Authenticate with a passkey') }}
+      </AButton>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less">
+:deep(.ant-input-group.ant-input-group-compact) {
+  display: flex;
+}
+</style>

+ 22 - 16
app/src/components/OTP/useOTPModal.ts → app/src/components/2FA/use2FAModal.ts

@@ -1,12 +1,12 @@
 import { createVNode, render } from 'vue'
 import { createVNode, render } from 'vue'
 import { Modal, message } from 'ant-design-vue'
 import { Modal, message } from 'ant-design-vue'
 import { useCookies } from '@vueuse/integrations/useCookies'
 import { useCookies } from '@vueuse/integrations/useCookies'
-import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
-import otp from '@/api/otp'
+import Authorization from '@/components/2FA/Authorization.vue'
+import twoFA from '@/api/2fa'
 import { useUserStore } from '@/pinia'
 import { useUserStore } from '@/pinia'
 
 
-const useOTPModal = () => {
-  const refOTPAuthorization = ref<typeof OTPAuthorization>()
+const use2FAModal = () => {
+  const refOTPAuthorization = ref<typeof Authorization>()
   const randomId = Math.random().toString(36).substring(2, 8)
   const randomId = Math.random().toString(36).substring(2, 8)
   const { secureSessionId } = storeToRefs(useUserStore())
   const { secureSessionId } = storeToRefs(useUserStore())
 
 
@@ -22,11 +22,11 @@ const useOTPModal = () => {
   }
   }
 
 
   const open = async (): Promise<string> => {
   const open = async (): Promise<string> => {
-    const { status } = await otp.status()
-    const { status: secureSessionStatus } = await otp.secure_session_status()
+    const twoFAStatus = await twoFA.status()
+    const { status: secureSessionStatus } = await twoFA.secure_session_status()
 
 
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
-      if (!status) {
+      if (!twoFAStatus.enabled) {
         resolve('')
         resolve('')
 
 
         return
         return
@@ -50,12 +50,16 @@ const useOTPModal = () => {
         container = null
         container = null
       }
       }
 
 
-      const verify = (passcode: string, recovery: string) => {
-        otp.start_secure_session(passcode, recovery).then(async r => {
-          cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
-          close()
-          secureSessionId.value = r.session_id
-          resolve(r.session_id)
+      const setSessionId = (sessionId: string) => {
+        cookies.set('secure_session_id', sessionId, { maxAge: 60 * 3 })
+        close()
+        secureSessionId.value = sessionId
+        resolve(sessionId)
+      }
+
+      const verifyOTP = (passcode: string, recovery: string) => {
+        twoFA.start_secure_session_by_otp(passcode, recovery).then(async r => {
+          setSessionId(r.session_id)
         }).catch(async () => {
         }).catch(async () => {
           refOTPAuthorization.value?.clearInput()
           refOTPAuthorization.value?.clearInput()
           await message.error($gettext('Invalid passcode or recovery code'))
           await message.error($gettext('Invalid passcode or recovery code'))
@@ -76,11 +80,13 @@ const useOTPModal = () => {
         },
         },
       }, {
       }, {
         default: () => h(
         default: () => h(
-          OTPAuthorization,
+          Authorization,
           {
           {
             ref: refOTPAuthorization,
             ref: refOTPAuthorization,
+            twoFAStatus,
             class: 'mt-3',
             class: 'mt-3',
-            onOnSubmit: verify,
+            onSubmitOTP: verifyOTP,
+            onSubmitSecureSessionID: setSessionId,
           },
           },
         ),
         ),
       })
       })
@@ -92,4 +98,4 @@ const useOTPModal = () => {
   return { open }
   return { open }
 }
 }
 
 
-export default useOTPModal
+export default use2FAModal

+ 1 - 1
app/src/components/CodeEditor/CodeEditor.vue

@@ -40,7 +40,7 @@ ace.config.setModuleUrl('ace/ext/searchbox', extSearchboxUrl)
   />
   />
 </template>
 </template>
 
 
-<style scoped>
+<style lang="less" scoped>
 :deep(.ace_placeholder) {
 :deep(.ace_placeholder) {
   z-index: 1;
   z-index: 1;
   position: relative;
   position: relative;

+ 0 - 78
app/src/components/OTP/OTPAuthorization.vue

@@ -1,78 +0,0 @@
-<script setup lang="ts">
-import OTPInput from '@/components/OTPInput/OTPInput.vue'
-
-const emit = defineEmits(['onSubmit'])
-
-const refOTP = ref()
-const useRecoveryCode = ref(false)
-const passcode = ref('')
-const recoveryCode = ref('')
-
-function clickUseRecoveryCode() {
-  passcode.value = ''
-  useRecoveryCode.value = true
-}
-
-function clickUseOTP() {
-  passcode.value = ''
-  useRecoveryCode.value = false
-}
-
-function onSubmit() {
-  emit('onSubmit', passcode.value, recoveryCode.value)
-}
-
-function clearInput() {
-  refOTP.value?.clearInput()
-}
-
-defineExpose({
-  clearInput,
-})
-</script>
-
-<template>
-  <div>
-    <div v-if="!useRecoveryCode">
-      <p>{{ $gettext('Please enter the 2FA 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>
-</template>
-
-<style scoped lang="less">
-:deep(.ant-input-group.ant-input-group-compact) {
-  display: flex;
-}
-</style>

+ 63 - 0
app/src/components/ReactiveFromNow/ReactiveFromNow.vue

@@ -0,0 +1,63 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
+const props = defineProps<{
+  time?: string | number
+}>()
+
+dayjs.extend(relativeTime)
+
+const text = ref('')
+
+const time = computed(() => {
+  if (!props.time)
+    return ''
+
+  if (typeof props.time === 'number')
+    return props.time
+
+  return Number.parseInt(props.time)
+})
+
+let timer: NodeJS.Timeout
+let step: number = 1
+
+async function computedText() {
+  if (!time.value)
+    return
+
+  // if time is not today, return the datetime
+  const thatDay = dayjs.unix(time.value).format('YYYY-MM-DD')
+  if (dayjs().format('YYYY-MM-DD') !== dayjs.unix(time.value).format('YYYY-MM-DD')) {
+    clearInterval(timer)
+    text.value = thatDay
+
+    return
+  }
+
+  text.value = dayjs.unix(time.value).fromNow()
+
+  clearInterval(timer)
+
+  timer = setInterval(computedText, step * 60 * 1000)
+
+  step += 5
+
+  if (step >= 60)
+    step = 60
+}
+
+onMounted(computedText)
+watch(() => props.time, computedText)
+</script>
+
+<template>
+  <div class="reactive-time inline">
+    {{ text }}
+  </div>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 59 - 4
app/src/components/SetLanguage/SetLanguage.vue

@@ -1,10 +1,19 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import { watch } from 'vue'
-
+import dayjs from 'dayjs'
 import { useSettingsStore } from '@/pinia'
 import { useSettingsStore } from '@/pinia'
 import gettext from '@/gettext'
 import gettext from '@/gettext'
 import loadTranslations from '@/api/translations'
 import loadTranslations from '@/api/translations'
 
 
+import 'dayjs/locale/fr'
+import 'dayjs/locale/ja'
+import 'dayjs/locale/ko'
+import 'dayjs/locale/de'
+import 'dayjs/locale/zh-cn'
+import 'dayjs/locale/zh-tw'
+import 'dayjs/locale/pt'
+import 'dayjs/locale/es'
+import 'dayjs/locale/it'
+
 const settings = useSettingsStore()
 const settings = useSettingsStore()
 
 
 const route = useRoute()
 const route = useRoute()
@@ -20,15 +29,61 @@ const current = computed({
 
 
 const languageAvailable = gettext.available
 const languageAvailable = gettext.available
 
 
+const updateTitle = () => {
+  const name = route.meta.name as never as () => string
+
+  document.title = `${name()} | Nginx UI`
+}
+
 watch(current, v => {
 watch(current, v => {
   loadTranslations(route)
   loadTranslations(route)
   settings.set_language(v)
   settings.set_language(v)
   gettext.current = v
   gettext.current = v
 
 
-  const name = route.meta.name as never as () => string
+  updateTitle()
+})
 
 
-  document.title = `${name()} | Nginx UI`
+onMounted(() => {
+  updateTitle()
 })
 })
+
+function init() {
+  switch (current.value) {
+    case 'fr':
+      dayjs.locale('fr')
+      break
+    case 'ja':
+      dayjs.locale('ja')
+      break
+    case 'ko':
+      dayjs.locale('ko')
+      break
+    case 'de':
+      dayjs.locale('de')
+      break
+    case 'en':
+      dayjs.locale('en')
+      break
+    case 'zh_TW':
+      dayjs.locale('zh-tw')
+      break
+    case 'pt':
+      dayjs.locale('pt')
+      break
+    case 'es':
+      dayjs.locale('es')
+      break
+    case 'it':
+      dayjs.locale('it')
+      break
+    default:
+      dayjs.locale('zh-cn')
+  }
+}
+
+init()
+
+watch(current, init)
 </script>
 </script>
 
 
 <template>
 <template>

+ 1 - 0
app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -161,6 +161,7 @@ function view(id: number | string) {
   get(id).then(() => {
   get(id).then(() => {
     visible.value = true
     visible.value = true
     modifyMode.value = false
     modifyMode.value = false
+    editMode.value = 'modify'
   }).catch(e => {
   }).catch(e => {
     message.error($gettext(e?.message ?? 'Server error'), 5)
     message.error($gettext(e?.message ?? 'Server error'), 5)
   })
   })

+ 1 - 1
app/src/components/StdDesign/StdDataDisplay/StdPagination.vue

@@ -30,7 +30,7 @@ const pageSize = computed({
     class="pagination-container"
     class="pagination-container"
   >
   >
     <APagination
     <APagination
-      v-model:pageSize="pageSize"
+      v-model:page-size="pageSize"
       :disabled="loading"
       :disabled="loading"
       :current="pagination.current_page"
       :current="pagination.current_page"
       show-size-changer
       show-size-changer

+ 172 - 59
app/src/language/en/app.po

@@ -13,7 +13,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr ""
 msgstr ""
 
 
@@ -37,7 +37,7 @@ msgstr "Username"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "Action"
 msgstr "Action"
@@ -52,6 +52,11 @@ msgstr "Action"
 msgid "Add"
 msgid "Add"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 #, fuzzy
 #, fuzzy
@@ -90,6 +95,10 @@ msgstr "Add Location"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "Advance Mode"
 msgstr "Advance Mode"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr ""
 msgstr ""
@@ -110,11 +119,16 @@ msgstr ""
 msgid "Arch"
 msgid "Arch"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 #, fuzzy
 #, fuzzy
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Are you sure you want to remove this directive?"
 msgstr "Are you sure you want to remove this directive?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "Are you sure you want to remove this directive?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 #, fuzzy
 #, fuzzy
@@ -168,7 +182,7 @@ msgstr ""
 msgid "Assistant"
 msgid "Assistant"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -176,6 +190,14 @@ msgstr ""
 msgid "Auth"
 msgid "Auth"
 msgstr ""
 msgstr ""
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -209,15 +231,15 @@ msgstr "Back"
 msgid "Back to list"
 msgid "Back to list"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr ""
 msgstr ""
 
 
@@ -259,7 +281,7 @@ msgstr ""
 msgid "CADir"
 msgid "CADir"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -273,6 +295,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -435,6 +458,7 @@ msgid "Create Folder"
 msgstr "Create Another"
 msgstr "Create Another"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "Created at"
 msgstr "Created at"
@@ -456,12 +480,12 @@ msgstr ""
 msgid "Credentials"
 msgid "Credentials"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+msgid "Current account is enabled TOTP."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
@@ -682,6 +706,12 @@ msgstr ""
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -757,12 +787,7 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:124
-#, fuzzy
-msgid "Enable 2FA"
-msgstr "Enabled"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 #, fuzzy
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "Enabled successfully"
 msgstr "Enabled successfully"
@@ -785,6 +810,11 @@ msgstr "Enabled successfully"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "Enable TLS"
 msgstr "Enable TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "Enable TLS"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -902,6 +932,12 @@ msgstr ""
 msgid "Finished"
 msgid "Finished"
 msgstr "Finished"
 msgstr "Finished"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr ""
 msgstr ""
@@ -974,18 +1010,22 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -1001,7 +1041,7 @@ msgstr ""
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "Certificate Status"
 msgstr "Certificate Status"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr ""
 msgstr ""
 
 
@@ -1017,12 +1057,12 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr ""
 msgstr ""
 
 
@@ -1044,7 +1084,7 @@ msgstr ""
 msgid "Invalid"
 msgid "Invalid"
 msgstr "Invalid E-mail!"
 msgstr "Invalid E-mail!"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -1058,11 +1098,11 @@ msgstr "Invalid E-mail!"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr ""
 msgstr ""
 
 
@@ -1098,6 +1138,11 @@ msgstr ""
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "Created at"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "Leave blank for no change"
 msgstr "Leave blank for no change"
@@ -1163,11 +1208,11 @@ msgstr "Locations"
 msgid "Log"
 msgid "Log"
 msgstr "Login"
 msgstr "Login"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "Login"
 msgstr "Login"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "Login successful"
 msgstr "Login successful"
 
 
@@ -1221,7 +1266,7 @@ msgstr "Manage Users"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "Certificate is valid"
 msgstr "Certificate is valid"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -1281,6 +1326,7 @@ msgstr "Single Directive"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1367,7 +1413,7 @@ msgstr "Saved successfully"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1469,6 +1515,10 @@ msgstr ""
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr ""
 msgstr ""
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr ""
 msgstr ""
@@ -1498,7 +1548,18 @@ msgstr ""
 msgid "Params"
 msgid "Params"
 msgstr "Params"
 msgstr "Params"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "Password"
 msgstr "Password"
 
 
@@ -1524,8 +1585,14 @@ msgstr ""
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+msgid "Please enter the OTP code:"
 msgstr ""
 msgstr ""
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1566,11 +1633,11 @@ msgstr ""
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "Please input your E-mail!"
 msgstr "Please input your E-mail!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "Please input your password!"
 msgstr "Please input your password!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "Please input your username!"
 msgstr "Please input your username!"
 
 
@@ -1634,16 +1701,16 @@ msgstr ""
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "Saved successfully"
 msgstr "Saved successfully"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr ""
 msgstr ""
 
 
@@ -1664,6 +1731,11 @@ msgstr ""
 msgid "Register failed"
 msgid "Register failed"
 msgstr "Enable failed"
 msgstr "Enable failed"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "Enabled successfully"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 #, fuzzy
 #, fuzzy
 msgid "Register successfully"
 msgid "Register successfully"
@@ -1699,11 +1771,12 @@ msgstr ""
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 #, fuzzy
 #, fuzzy
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "Saved successfully"
 msgstr "Saved successfully"
@@ -1783,7 +1856,7 @@ msgstr ""
 msgid "Reset"
 msgid "Reset"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr ""
 msgstr ""
 
 
@@ -1808,6 +1881,7 @@ msgstr ""
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "Save"
 msgstr "Save"
@@ -1836,7 +1910,7 @@ msgstr "Saved successfully"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "Saved successfully"
 msgstr "Saved successfully"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 
 
@@ -1844,7 +1918,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1869,9 +1943,12 @@ msgstr "Send"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1916,6 +1993,10 @@ msgstr ""
 msgid "Show"
 msgid "Show"
 msgstr ""
 msgstr ""
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "Single Directive"
 msgstr "Single Directive"
@@ -1949,7 +2030,7 @@ msgstr "Certificate Status"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "Certificate Status"
 msgstr "Certificate Status"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 #, fuzzy
 #, fuzzy
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "Login"
 msgstr "Login"
@@ -2120,7 +2201,7 @@ msgstr "Certificate Status"
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
@@ -2184,7 +2265,8 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr ""
 msgstr ""
@@ -2193,12 +2275,19 @@ msgstr ""
 msgid "Title"
 msgid "Title"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2211,11 +2300,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 msgstr ""
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2225,7 +2318,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr ""
 msgstr ""
 
 
@@ -2235,6 +2328,11 @@ msgstr ""
 msgid "Type"
 msgid "Type"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "Saved successfully"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2281,11 +2379,11 @@ msgstr "Uptime:"
 msgid "URL"
 msgid "URL"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -2294,11 +2392,11 @@ msgstr ""
 msgid "User"
 msgid "User"
 msgstr "Username"
 msgstr "Username"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "Username"
 msgstr "Username"
 
 
@@ -2338,6 +2436,7 @@ msgstr "Basic Mode"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "Warning"
 msgstr "Warning"
 
 
@@ -2368,7 +2467,7 @@ msgstr ""
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "Yes"
 msgstr "Yes"
@@ -2381,6 +2480,20 @@ msgstr ""
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#, fuzzy
+#~ msgid "Enable 2FA"
+#~ msgstr "Enabled"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Rename "
 #~ msgid "Rename "
 #~ msgstr "Username"
 #~ msgstr "Username"

+ 174 - 58
app/src/language/es/app.po

@@ -20,7 +20,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr "2FA"
 msgstr "2FA"
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr "Configuración de 2FA"
 msgstr "Configuración de 2FA"
 
 
@@ -43,7 +43,7 @@ msgstr "Usuario ACME"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "Acción"
 msgstr "Acción"
@@ -58,6 +58,11 @@ msgstr "Acción"
 msgid "Add"
 msgid "Add"
 msgstr "Agregar"
 msgstr "Agregar"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
 msgid "Add Configuration"
@@ -92,6 +97,10 @@ msgstr "Adicional"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "Modo avanzado"
 msgstr "Modo avanzado"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr "URL Base de la API"
 msgstr "URL Base de la API"
@@ -112,10 +121,15 @@ msgstr "Token de la API"
 msgid "Arch"
 msgid "Arch"
 msgstr "Arquitectura"
 msgstr "Arquitectura"
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "¿Está seguro de eliminar esta IP bloqueada inmediatamente?"
 msgstr "¿Está seguro de eliminar esta IP bloqueada inmediatamente?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "¿Está seguro de eliminar esta IP bloqueada inmediatamente?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 msgid "Are you sure you want to clear all notifications?"
 msgid "Are you sure you want to clear all notifications?"
@@ -161,7 +175,7 @@ msgstr "Preguntar por ayuda a ChatGPT"
 msgid "Assistant"
 msgid "Assistant"
 msgstr "Asistente"
 msgstr "Asistente"
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr "Intentos"
 msgstr "Intentos"
 
 
@@ -169,6 +183,14 @@ msgstr "Intentos"
 msgid "Auth"
 msgid "Auth"
 msgstr "Autenticación"
 msgstr "Autenticación"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -201,15 +223,15 @@ msgstr "Volver al Inicio"
 msgid "Back to list"
 msgid "Back to list"
 msgstr "Volver a la lista"
 msgstr "Volver a la lista"
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr "Umbral de Prohibición en Minutos"
 msgstr "Umbral de Prohibición en Minutos"
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr "IPs prohibidas"
 msgstr "IPs prohibidas"
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr "Bloqueado hasta"
 msgstr "Bloqueado hasta"
 
 
@@ -249,7 +271,7 @@ msgstr "Dir CA"
 msgid "CADir"
 msgid "CADir"
 msgstr "Directorio CA"
 msgstr "Directorio CA"
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -263,6 +285,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -412,6 +435,7 @@ msgid "Create Folder"
 msgstr "Crear carpeta"
 msgstr "Crear carpeta"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "Creado el"
 msgstr "Creado el"
@@ -432,13 +456,15 @@ msgstr "Credencial"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "Credenciales"
 msgstr "Credenciales"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+#, fuzzy
+msgid "Current account is enabled TOTP."
 msgstr ""
 msgstr ""
 "La cuenta actual tiene habilitada la autenticación de dos factores (2FA)."
 "La cuenta actual tiene habilitada la autenticación de dos factores (2FA)."
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+#, fuzzy
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 "La cuenta actual no tiene habilitada la autenticación de dos factores (2FA)."
 "La cuenta actual no tiene habilitada la autenticación de dos factores (2FA)."
 
 
@@ -652,6 +678,12 @@ msgstr "Descargando la última versión"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr "Modo de ejecución de prueba habilitado"
 msgstr "Modo de ejecución de prueba habilitado"
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -720,11 +752,7 @@ msgstr "Falló el habilitado de %{conf_name} en %{node_name}"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Habilitado exitoso de %{conf_name} en %{node_name}"
 msgstr "Habilitado exitoso de %{conf_name} en %{node_name}"
 
 
-#: src/views/preference/components/TOTP.vue:124
-msgid "Enable 2FA"
-msgstr "Habilitar 2FA"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "Habilitar 2FA exitoso"
 msgstr "Habilitar 2FA exitoso"
 
 
@@ -745,6 +773,11 @@ msgstr "Habilitado con Éxito"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "Habilitar TLS"
 msgstr "Habilitar TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "Habilitar TLS"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -859,6 +892,12 @@ msgstr "Filtro"
 msgid "Finished"
 msgid "Finished"
 msgstr "Terminado"
 msgstr "Terminado"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "Para usuario chino: https://mirror.ghproxy.com/"
 msgstr "Para usuario chino: https://mirror.ghproxy.com/"
@@ -927,7 +966,7 @@ msgstr "HTTP01"
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr "Si se deja en blanco, se utilizará el directorio CA predeterminado."
 msgstr "Si se deja en blanco, se utilizará el directorio CA predeterminado."
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
@@ -936,7 +975,7 @@ msgstr ""
 "el máximo de intentos en los minutos del umbral de prohibición, la IP será "
 "el máximo de intentos en los minutos del umbral de prohibición, la IP será "
 "bloqueada por un período de tiempo."
 "bloqueada por un período de tiempo."
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
@@ -944,6 +983,10 @@ msgstr ""
 "Si pierde su teléfono móvil, puede usar el código de recuperación para "
 "Si pierde su teléfono móvil, puede usar el código de recuperación para "
 "restablecer su 2FA."
 "restablecer su 2FA."
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -960,7 +1003,7 @@ msgstr "Importar"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "Importar Certificado"
 msgstr "Importar Certificado"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "Nombre de usuario o contraseña incorrectos"
 msgstr "Nombre de usuario o contraseña incorrectos"
 
 
@@ -976,12 +1019,12 @@ msgstr "Error de actualización de kernel inicial"
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "Inicializando la actualización del kernel"
 msgstr "Inicializando la actualización del kernel"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr "Ingrese el código de la aplicación:"
 msgstr "Ingrese el código de la aplicación:"
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr "Ingrese el código de recuperación:"
 msgstr "Ingrese el código de recuperación:"
 
 
@@ -1001,7 +1044,7 @@ msgstr "Intervalo"
 msgid "Invalid"
 msgid "Invalid"
 msgstr "Inválido"
 msgstr "Inválido"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr "Código 2FA o de recuperación inválido"
 msgstr "Código 2FA o de recuperación inválido"
 
 
@@ -1014,11 +1057,11 @@ msgstr "Nombre de archivo inválido"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr "Nombre de carpeta inválido"
 msgstr "Nombre de carpeta inválido"
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr "Código de acceso o código de recuperación inválido"
 msgstr "Código de acceso o código de recuperación inválido"
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr "IP"
 msgstr "IP"
 
 
@@ -1051,6 +1094,11 @@ msgstr "Tipo llave"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "Comprobado por última vez el"
 msgstr "Comprobado por última vez el"
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "Comprobado por última vez el"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "Para no modificar dejar en blanco"
 msgstr "Para no modificar dejar en blanco"
@@ -1110,11 +1158,11 @@ msgstr "Ubicaciones"
 msgid "Log"
 msgid "Log"
 msgstr "Registro"
 msgstr "Registro"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "Acceso"
 msgstr "Acceso"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "Acceso exitoso"
 msgstr "Acceso exitoso"
 
 
@@ -1172,7 +1220,7 @@ msgstr "Administrar usuarios"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "Certificado Administrado"
 msgstr "Certificado Administrado"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr "Intentos máximos"
 msgstr "Intentos máximos"
 
 
@@ -1227,6 +1275,7 @@ msgstr "Directiva multilínea"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1308,7 +1357,7 @@ msgstr "Nginx reiniciado con éxito"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1409,6 +1458,10 @@ msgstr "En línea"
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr "OpenAI"
 msgstr "OpenAI"
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr "Nombre original"
 msgstr "Nombre original"
@@ -1437,7 +1490,18 @@ msgstr "Sobrescribir archivo existente"
 msgid "Params"
 msgid "Params"
 msgstr "Parámetros"
 msgstr "Parámetros"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "Contraseña"
 msgstr "Contraseña"
 
 
@@ -1463,8 +1527,15 @@ msgstr "Error al ejecutar la actualización del kernel"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "Realizando la actualizaciónd el kernel"
 msgstr "Realizando la actualizaciónd el kernel"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+#, fuzzy
+msgid "Please enter the OTP code:"
 msgstr "Por favor, ingrese el código 2FA:"
 msgstr "Por favor, ingrese el código 2FA:"
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1510,11 +1581,11 @@ msgstr ""
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "¡Por favor ingrese su correo electrónico!"
 msgstr "¡Por favor ingrese su correo electrónico!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "¡Por favor ingrese su contraseña!"
 msgstr "¡Por favor ingrese su contraseña!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "¡Por favor ingrese su nombre de usuario!"
 msgstr "¡Por favor ingrese su nombre de usuario!"
 
 
@@ -1578,16 +1649,16 @@ msgstr "Recuperar"
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "Recuperado con éxito"
 msgstr "Recuperado con éxito"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr "Recuperación"
 msgstr "Recuperación"
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr "Código de Recuperación"
 msgstr "Código de Recuperación"
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr "Código de Recuperación:"
 msgstr "Código de Recuperación:"
 
 
@@ -1607,6 +1678,11 @@ msgstr "Registrar"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "Fallo en el registro"
 msgstr "Fallo en el registro"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "Registrado con éxito"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 msgid "Register successfully"
 msgid "Register successfully"
 msgstr "Registrado con éxito"
 msgstr "Registrado con éxito"
@@ -1640,11 +1716,12 @@ msgstr "Recargando"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "Recargando Nginx"
 msgstr "Recargando Nginx"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr "Eliminar"
 msgstr "Eliminar"
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "Eliminado con éxito"
 msgstr "Eliminado con éxito"
 
 
@@ -1714,7 +1791,7 @@ msgstr "Pedido con parámetros incorrectos"
 msgid "Reset"
 msgid "Reset"
 msgstr "Limpiar"
 msgstr "Limpiar"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "Restablecer 2FA"
 msgstr "Restablecer 2FA"
 
 
@@ -1738,6 +1815,7 @@ msgstr "Corriendo"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "Guardar"
 msgstr "Guardar"
@@ -1765,7 +1843,7 @@ msgstr "Guardado con éxito"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "Guardado con éxito"
 msgstr "Guardado con éxito"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 "Escanee el código QR con su teléfono móvil para agregar la cuenta a la "
 "Escanee el código QR con su teléfono móvil para agregar la cuenta a la "
@@ -1775,7 +1853,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr "SDK"
 msgstr "SDK"
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1800,9 +1878,12 @@ msgstr "Enviado"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1848,6 +1929,10 @@ msgstr "Usando el proveedor de desafíos HTTP01"
 msgid "Show"
 msgid "Show"
 msgstr "Mostrar"
 msgstr "Mostrar"
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "Directiva de una sola línea"
 msgstr "Directiva de una sola línea"
@@ -1876,7 +1961,7 @@ msgstr "Ruta de la llave del certificado SSL"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "Ruta del certificado SSL"
 msgstr "Ruta del certificado SSL"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "Acceso SSO"
 msgstr "Acceso SSO"
 
 
@@ -2045,7 +2130,7 @@ msgstr "La ruta existe, pero el archivo no es un certificado"
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr "La ruta existe, pero el archivo no es una clave privada"
 msgstr "La ruta existe, pero el archivo no es una clave privada"
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
@@ -2117,7 +2202,8 @@ msgstr ""
 "Esto actualizará o reinstalará la interfaz de usuario de Nginx en "
 "Esto actualizará o reinstalará la interfaz de usuario de Nginx en "
 "%{nodeNames} a %{version}."
 "%{nodeNames} a %{version}."
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr "Consejos"
 msgstr "Consejos"
@@ -2126,7 +2212,7 @@ msgstr "Consejos"
 msgid "Title"
 msgid "Title"
 msgstr "Título"
 msgstr "Título"
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
@@ -2134,6 +2220,13 @@ msgstr ""
 "Para habilitarlo, necesitas instalar la aplicación Google Authenticator o "
 "Para habilitarlo, necesitas instalar la aplicación Google Authenticator o "
 "Microsoft Authenticator en tu teléfono móvil."
 "Microsoft Authenticator en tu teléfono móvil."
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2150,13 +2243,17 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr "El token no es válido"
 msgstr "El token no es válido"
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 msgstr ""
 "Demasiados intentos fallidos de inicio de sesión, por favor intente "
 "Demasiados intentos fallidos de inicio de sesión, por favor intente "
 "nuevamente más tarde"
 "nuevamente más tarde"
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2168,7 +2265,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr "Basura"
 msgstr "Basura"
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr "Se requiere autenticación de dos factores"
 msgstr "Se requiere autenticación de dos factores"
 
 
@@ -2178,6 +2275,11 @@ msgstr "Se requiere autenticación de dos factores"
 msgid "Type"
 msgid "Type"
 msgstr "Tipo"
 msgstr "Tipo"
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "Actualización exitosa"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2221,11 +2323,11 @@ msgstr "Tiempo encendido:"
 msgid "URL"
 msgid "URL"
 msgstr "URL"
 msgstr "URL"
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr "Usar OTP"
 msgstr "Usar OTP"
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr "Usar código de recuperación"
 msgstr "Usar código de recuperación"
 
 
@@ -2233,11 +2335,11 @@ msgstr "Usar código de recuperación"
 msgid "User"
 msgid "User"
 msgstr "Usuario"
 msgstr "Usuario"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr "El usuario está bloqueado"
 msgstr "El usuario está bloqueado"
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "Nombre de usuario"
 msgstr "Nombre de usuario"
 
 
@@ -2275,6 +2377,7 @@ msgstr "Modo de vista"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "Advertencia"
 msgstr "Advertencia"
 
 
@@ -2309,7 +2412,7 @@ msgstr "Escribir certificado a disco"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "Si"
 msgstr "Si"
@@ -2322,6 +2425,19 @@ msgstr "Estás usando la última versión"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Puede consultar la actualización de Nginx UI en esta página."
 msgstr "Puede consultar la actualización de Nginx UI en esta página."
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#~ msgid "Enable 2FA"
+#~ msgstr "Habilitar 2FA"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Rename "
 #~ msgid "Rename "
 #~ msgstr "Renombrar"
 #~ msgstr "Renombrar"

+ 172 - 59
app/src/language/fr_FR/app.po

@@ -15,7 +15,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr ""
 msgstr ""
 
 
@@ -39,7 +39,7 @@ msgstr "Nom d'utilisateur"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "Action"
 msgstr "Action"
@@ -54,6 +54,11 @@ msgstr "Action"
 msgid "Add"
 msgid "Add"
 msgstr "Ajouter"
 msgstr "Ajouter"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 #, fuzzy
 #, fuzzy
@@ -92,6 +97,10 @@ msgstr "Supplémentaire"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "Mode avancé"
 msgstr "Mode avancé"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr "URL de base de l'API"
 msgstr "URL de base de l'API"
@@ -114,11 +123,16 @@ msgstr "Jeton d'API"
 msgid "Arch"
 msgid "Arch"
 msgstr "Arch"
 msgstr "Arch"
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 #, fuzzy
 #, fuzzy
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Etes-vous sûr que vous voulez supprimer ?"
 msgstr "Etes-vous sûr que vous voulez supprimer ?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "Etes-vous sûr que vous voulez supprimer ?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 #, fuzzy
 #, fuzzy
@@ -170,7 +184,7 @@ msgstr "Modèle ChatGPT"
 msgid "Assistant"
 msgid "Assistant"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -179,6 +193,14 @@ msgstr ""
 msgid "Auth"
 msgid "Auth"
 msgstr "Autheur"
 msgstr "Autheur"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -211,15 +233,15 @@ msgstr "Retour au menu principal"
 msgid "Back to list"
 msgid "Back to list"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr ""
 msgstr ""
 
 
@@ -261,7 +283,7 @@ msgstr ""
 msgid "CADir"
 msgid "CADir"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -275,6 +297,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -435,6 +458,7 @@ msgid "Create Folder"
 msgstr "Créer un autre"
 msgstr "Créer un autre"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "Créé le"
 msgstr "Créé le"
@@ -456,12 +480,12 @@ msgstr "Identifiant"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "Identifiants"
 msgstr "Identifiants"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+msgid "Current account is enabled TOTP."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
@@ -682,6 +706,12 @@ msgstr "Téléchargement de la dernière version"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -757,12 +787,7 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:124
-#, fuzzy
-msgid "Enable 2FA"
-msgstr "Activé"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 #, fuzzy
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "Activé avec succès"
 msgstr "Activé avec succès"
@@ -785,6 +810,11 @@ msgstr "Activé avec succès"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "Activer TLS"
 msgstr "Activer TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "Activer TLS"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -905,6 +935,12 @@ msgstr "Filtrer"
 msgid "Finished"
 msgid "Finished"
 msgstr "Finie"
 msgstr "Finie"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 #, fuzzy
 #, fuzzy
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
@@ -975,18 +1011,22 @@ msgstr "HTTP01"
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -1003,7 +1043,7 @@ msgstr "Exporter"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "État du certificat"
 msgstr "État du certificat"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 #, fuzzy
 #, fuzzy
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "Le pseudo ou mot de passe est incorect"
 msgstr "Le pseudo ou mot de passe est incorect"
@@ -1020,12 +1060,12 @@ msgstr "Erreur du programme de mise à niveau initial du core"
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "Initialisation du programme de mise à niveau du core"
 msgstr "Initialisation du programme de mise à niveau du core"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr ""
 msgstr ""
 
 
@@ -1045,7 +1085,7 @@ msgstr ""
 msgid "Invalid"
 msgid "Invalid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -1058,11 +1098,11 @@ msgstr ""
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr ""
 msgstr ""
 
 
@@ -1098,6 +1138,11 @@ msgstr "Type"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "Dernière vérification le"
 msgstr "Dernière vérification le"
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "Dernière vérification le"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "Laisser vide pour aucun changement"
 msgstr "Laisser vide pour aucun changement"
@@ -1165,11 +1210,11 @@ msgstr "Localisations"
 msgid "Log"
 msgid "Log"
 msgstr "Connexion"
 msgstr "Connexion"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "Connexion"
 msgstr "Connexion"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "Connexion réussie"
 msgstr "Connexion réussie"
 
 
@@ -1223,7 +1268,7 @@ msgstr "Gérer les utilisateurs"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "Changer de certificat"
 msgstr "Changer de certificat"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -1281,6 +1326,7 @@ msgstr "Directive multiligne"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1365,7 +1411,7 @@ msgstr "Nginx a redémarré avec succès"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1467,6 +1513,10 @@ msgstr ""
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr "OpenAI"
 msgstr "OpenAI"
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr ""
 msgstr ""
@@ -1495,7 +1545,18 @@ msgstr ""
 msgid "Params"
 msgid "Params"
 msgstr "Paramètres"
 msgstr "Paramètres"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "Mot de passe"
 msgstr "Mot de passe"
 
 
@@ -1521,8 +1582,14 @@ msgstr "Erreur lors de la mise a niveau du core"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "Exécution de la mise à niveau du core"
 msgstr "Exécution de la mise à niveau du core"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+msgid "Please enter the OTP code:"
 msgstr ""
 msgstr ""
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1570,11 +1637,11 @@ msgstr ""
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "Veuillez saisir votre e-mail !"
 msgstr "Veuillez saisir votre e-mail !"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "Veuillez saisir votre mot de passe !"
 msgstr "Veuillez saisir votre mot de passe !"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "Veuillez saisir votre nom d'utilisateur !"
 msgstr "Veuillez saisir votre nom d'utilisateur !"
 
 
@@ -1640,16 +1707,16 @@ msgstr ""
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "Enregistré avec succès"
 msgstr "Enregistré avec succès"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr ""
 msgstr ""
 
 
@@ -1671,6 +1738,11 @@ msgstr "Enregistrement de l'utilisateur"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "Enregistrement de l'utilisateur"
 msgstr "Enregistrement de l'utilisateur"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "Activé avec succès"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 #, fuzzy
 #, fuzzy
 msgid "Register successfully"
 msgid "Register successfully"
@@ -1706,11 +1778,12 @@ msgstr "Rechargement"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "Rechargement de nginx"
 msgstr "Rechargement de nginx"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 #, fuzzy
 #, fuzzy
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "Enregistré avec succès"
 msgstr "Enregistré avec succès"
@@ -1790,7 +1863,7 @@ msgstr ""
 msgid "Reset"
 msgid "Reset"
 msgstr "Réinitialiser"
 msgstr "Réinitialiser"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 #, fuzzy
 #, fuzzy
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "Réinitialiser"
 msgstr "Réinitialiser"
@@ -1815,6 +1888,7 @@ msgstr "En cours d'éxécution"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "Enregistrer"
 msgstr "Enregistrer"
@@ -1842,7 +1916,7 @@ msgstr "Sauvegarde réussie"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "Enregistré avec succès"
 msgstr "Enregistré avec succès"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 
 
@@ -1850,7 +1924,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1875,9 +1949,12 @@ msgstr "Envoyer"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1924,6 +2001,10 @@ msgstr "Utilisation du fournisseur de challenge HTTP01"
 msgid "Show"
 msgid "Show"
 msgstr ""
 msgstr ""
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "Directive unique"
 msgstr "Directive unique"
@@ -1954,7 +2035,7 @@ msgstr "Chemin de la clé du certificat SSL"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "Chemin du certificat SSL"
 msgstr "Chemin du certificat SSL"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 #, fuzzy
 #, fuzzy
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "Connexion"
 msgstr "Connexion"
@@ -2126,7 +2207,7 @@ msgstr "Chemin de la clé du certificat SSL"
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
@@ -2194,7 +2275,8 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr ""
 msgstr ""
@@ -2203,12 +2285,19 @@ msgstr ""
 msgid "Title"
 msgid "Title"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2225,11 +2314,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 msgstr ""
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2239,7 +2332,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr ""
 msgstr ""
 
 
@@ -2249,6 +2342,11 @@ msgstr ""
 msgid "Type"
 msgid "Type"
 msgstr "Type"
 msgstr "Type"
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "Mis à jour avec succés"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2293,11 +2391,11 @@ msgstr "Disponibilité :"
 msgid "URL"
 msgid "URL"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -2306,11 +2404,11 @@ msgstr ""
 msgid "User"
 msgid "User"
 msgstr "Nom d'utilisateur"
 msgstr "Nom d'utilisateur"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "Nom d'utilisateur"
 msgstr "Nom d'utilisateur"
 
 
@@ -2351,6 +2449,7 @@ msgstr "Mode simple"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "Avertissement"
 msgstr "Avertissement"
 
 
@@ -2383,7 +2482,7 @@ msgstr "Écriture du certificat sur le disque"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "Oui"
 msgstr "Oui"
@@ -2396,6 +2495,20 @@ msgstr "Vous utilisez la dernière version"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Vous pouvez vérifier la mise à niveau de Nginx UI sur cette page."
 msgstr "Vous pouvez vérifier la mise à niveau de Nginx UI sur cette page."
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#, fuzzy
+#~ msgid "Enable 2FA"
+#~ msgstr "Activé"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Rename "
 #~ msgid "Rename "
 #~ msgstr "Nom d'utilisateur"
 #~ msgstr "Nom d'utilisateur"

+ 172 - 59
app/src/language/ko_KR/app.po

@@ -18,7 +18,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr "2FA"
 msgstr "2FA"
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr "2FA 설정"
 msgstr "2FA 설정"
 
 
@@ -41,7 +41,7 @@ msgstr "ACME 사용자"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "작업"
 msgstr "작업"
@@ -56,6 +56,11 @@ msgstr "작업"
 msgid "Add"
 msgid "Add"
 msgstr "추가"
 msgstr "추가"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
 msgid "Add Configuration"
@@ -90,6 +95,10 @@ msgstr "추가적인"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "고급 모드"
 msgstr "고급 모드"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr "API 기본 URL"
 msgstr "API 기본 URL"
@@ -110,10 +119,15 @@ msgstr "API 토큰"
 msgid "Arch"
 msgid "Arch"
 msgstr "아키텍처"
 msgstr "아키텍처"
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "차단된 IP를 즉시 삭제하시겠습니까?"
 msgstr "차단된 IP를 즉시 삭제하시겠습니까?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "차단된 IP를 즉시 삭제하시겠습니까?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 msgid "Are you sure you want to clear all notifications?"
 msgid "Are you sure you want to clear all notifications?"
@@ -159,7 +173,7 @@ msgstr "ChatGPT에게 도움 요청"
 msgid "Assistant"
 msgid "Assistant"
 msgstr "조수"
 msgstr "조수"
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr "시도 횟수"
 msgstr "시도 횟수"
 
 
@@ -167,6 +181,14 @@ msgstr "시도 횟수"
 msgid "Auth"
 msgid "Auth"
 msgstr "인증"
 msgstr "인증"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -199,15 +221,15 @@ msgstr "홈으로"
 msgid "Back to list"
 msgid "Back to list"
 msgstr "목록으로 돌아가기"
 msgstr "목록으로 돌아가기"
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr "차단 시간(분)"
 msgstr "차단 시간(분)"
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr "차단된 IP"
 msgstr "차단된 IP"
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr "차단될 시간"
 msgstr "차단될 시간"
 
 
@@ -247,7 +269,7 @@ msgstr "CA 디렉토리"
 msgid "CADir"
 msgid "CADir"
 msgstr "CA 디렉토리"
 msgstr "CA 디렉토리"
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -261,6 +283,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -414,6 +437,7 @@ msgid "Create Folder"
 msgstr "다른 것 생성하기"
 msgstr "다른 것 생성하기"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "생성 시간"
 msgstr "생성 시간"
@@ -435,12 +459,12 @@ msgstr "인증 정보"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "인증 정보들"
 msgstr "인증 정보들"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+msgid "Current account is enabled TOTP."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
@@ -650,6 +674,12 @@ msgstr "최신 릴리스 다운로드 중"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr "드라이런 모드 활성화됨"
 msgstr "드라이런 모드 활성화됨"
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -720,12 +750,7 @@ msgstr "%{node_name}에서 %{conf_name} 활성화 실패"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "%{node_name}에서 %{conf_name} 성공적으로 활성화됨"
 msgstr "%{node_name}에서 %{conf_name} 성공적으로 활성화됨"
 
 
-#: src/views/preference/components/TOTP.vue:124
-#, fuzzy
-msgid "Enable 2FA"
-msgstr "활성화"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 #, fuzzy
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "성공적으로 활성화"
 msgstr "성공적으로 활성화"
@@ -747,6 +772,11 @@ msgstr "성공적으로 활성화"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "TLS 활성화"
 msgstr "TLS 활성화"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "TLS 활성화"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -864,6 +894,12 @@ msgstr "필터"
 msgid "Finished"
 msgid "Finished"
 msgstr "완료됨"
 msgstr "완료됨"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "중국 사용자를 위해: https://mirror.ghproxy.com/"
 msgstr "중국 사용자를 위해: https://mirror.ghproxy.com/"
@@ -936,18 +972,22 @@ msgstr "HTTP01"
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -963,7 +1003,7 @@ msgstr "가져오기"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "인증서 상태"
 msgstr "인증서 상태"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 #, fuzzy
 #, fuzzy
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "사용자 이름 또는 비밀번호가 올바르지 않습니다"
 msgstr "사용자 이름 또는 비밀번호가 올바르지 않습니다"
@@ -980,12 +1020,12 @@ msgstr "초기 코어 업그레이더 오류"
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "코어 업그레이더 초기화"
 msgstr "코어 업그레이더 초기화"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr ""
 msgstr ""
 
 
@@ -1007,7 +1047,7 @@ msgstr "간격"
 msgid "Invalid"
 msgid "Invalid"
 msgstr "유효함"
 msgstr "유효함"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -1021,11 +1061,11 @@ msgstr "Invalid E-mail!"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr ""
 msgstr ""
 
 
@@ -1061,6 +1101,11 @@ msgstr "키 유형"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "마지막 확인 시간"
 msgstr "마지막 확인 시간"
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "마지막 확인 시간"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "변경사항이 없으면 비워두세요"
 msgstr "변경사항이 없으면 비워두세요"
@@ -1126,11 +1171,11 @@ msgstr "위치들"
 msgid "Log"
 msgid "Log"
 msgstr "로그인"
 msgstr "로그인"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "로그인"
 msgstr "로그인"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "로그인 성공"
 msgstr "로그인 성공"
 
 
@@ -1189,7 +1234,7 @@ msgstr "사용자 관리"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "인증서 유효"
 msgstr "인증서 유효"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -1249,6 +1294,7 @@ msgstr "단일 지시문"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1335,7 +1381,7 @@ msgstr "Nginx가 성공적으로 재시작됨"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1437,6 +1483,10 @@ msgstr "온라인"
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr "오픈AI"
 msgstr "오픈AI"
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr ""
 msgstr ""
@@ -1466,7 +1516,18 @@ msgstr "기존 파일 덮어쓰기"
 msgid "Params"
 msgid "Params"
 msgstr "파라미터"
 msgstr "파라미터"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "비밀번호"
 msgstr "비밀번호"
 
 
@@ -1492,8 +1553,14 @@ msgstr "핵심 업그레이드 오류 수행"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "핵심 업그레이드 수행 중"
 msgstr "핵심 업그레이드 수행 중"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+msgid "Please enter the OTP code:"
 msgstr ""
 msgstr ""
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1536,11 +1603,11 @@ msgstr "이름을 입력해주세요, 이것은 새 구성의 파일 이름으
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "이메일을 입력해주세요!"
 msgstr "이메일을 입력해주세요!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "비밀번호를 입력해주세요!"
 msgstr "비밀번호를 입력해주세요!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "사용자 이름을 입력해주세요!"
 msgstr "사용자 이름을 입력해주세요!"
 
 
@@ -1604,16 +1671,16 @@ msgstr ""
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "성공적으로 제거됨"
 msgstr "성공적으로 제거됨"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr ""
 msgstr ""
 
 
@@ -1635,6 +1702,11 @@ msgstr "사용자 등록 중"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "사용자 등록 중"
 msgstr "사용자 등록 중"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "성공적으로 갱신됨"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 #, fuzzy
 #, fuzzy
 msgid "Register successfully"
 msgid "Register successfully"
@@ -1671,11 +1743,12 @@ msgstr "리로딩 중"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "Nginx 리로딩 중"
 msgstr "Nginx 리로딩 중"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 #, fuzzy
 #, fuzzy
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "성공적으로 제거됨"
 msgstr "성공적으로 제거됨"
@@ -1755,7 +1828,7 @@ msgstr "잘못된 매개변수로 요청됨"
 msgid "Reset"
 msgid "Reset"
 msgstr "재설정"
 msgstr "재설정"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 #, fuzzy
 #, fuzzy
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "재설정"
 msgstr "재설정"
@@ -1781,6 +1854,7 @@ msgstr "실행 중"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "저장"
 msgstr "저장"
@@ -1809,7 +1883,7 @@ msgstr "성공적으로 저장됨"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "성공적으로 저장됨"
 msgstr "성공적으로 저장됨"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 
 
@@ -1817,7 +1891,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1842,9 +1916,12 @@ msgstr "보내기"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1889,6 +1966,10 @@ msgstr "HTTP01 공급자 설정"
 msgid "Show"
 msgid "Show"
 msgstr ""
 msgstr ""
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "단일 지시문"
 msgstr "단일 지시문"
@@ -1921,7 +2002,7 @@ msgstr "SSL 인증서 키 경로"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "SSL 인증서 경로"
 msgstr "SSL 인증서 경로"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 #, fuzzy
 #, fuzzy
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "SSO 로그인"
 msgstr "SSO 로그인"
@@ -2092,7 +2173,7 @@ msgstr "Certificate Status"
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr "경로는 존재하지만 파일은 개인 키가 아닙니다"
 msgstr "경로는 존재하지만 파일은 개인 키가 아닙니다"
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
@@ -2158,7 +2239,8 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr "팁"
 msgstr "팁"
@@ -2167,12 +2249,19 @@ msgstr "팁"
 msgid "Title"
 msgid "Title"
 msgstr "제목"
 msgstr "제목"
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2188,11 +2277,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr "토큰이 유효하지 않습니다"
 msgstr "토큰이 유효하지 않습니다"
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 msgstr ""
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2202,7 +2295,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr ""
 msgstr ""
 
 
@@ -2212,6 +2305,11 @@ msgstr ""
 msgid "Type"
 msgid "Type"
 msgstr "유형"
 msgstr "유형"
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "성공적으로 저장되었습니다"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2258,11 +2356,11 @@ msgstr "가동 시간:"
 msgid "URL"
 msgid "URL"
 msgstr "URL"
 msgstr "URL"
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -2271,11 +2369,11 @@ msgstr ""
 msgid "User"
 msgid "User"
 msgstr "사용자 이름"
 msgstr "사용자 이름"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "사용자 이름"
 msgstr "사용자 이름"
 
 
@@ -2317,6 +2415,7 @@ msgstr "기본 모드"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "경고"
 msgstr "경고"
 
 
@@ -2351,7 +2450,7 @@ msgstr "인증서를 디스크에 쓰기"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "예"
 msgstr "예"
@@ -2364,6 +2463,20 @@ msgstr "최신 버전을 사용하고 있습니다"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "이 페이지에서 Nginx UI 업그레이드를 확인할 수 있습니다."
 msgstr "이 페이지에서 Nginx UI 업그레이드를 확인할 수 있습니다."
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#, fuzzy
+#~ msgid "Enable 2FA"
+#~ msgstr "활성화"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Enter"
 #~ msgid "Enter"
 #~ msgstr "간격"
 #~ msgstr "간격"

+ 151 - 59
app/src/language/messages.pot

@@ -6,7 +6,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr ""
 msgstr ""
 
 
@@ -32,7 +32,7 @@ msgstr ""
 #: src/views/domain/DomainList.vue:47
 #: src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26
+#: src/views/preference/AuthSettings.vue:27
 #: src/views/stream/StreamList.vue:47
 #: src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
@@ -48,6 +48,11 @@ msgstr ""
 msgid "Add"
 msgid "Add"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112
 #: src/routes/index.ts:112
 #: src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
@@ -85,6 +90,10 @@ msgstr ""
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr ""
 msgstr ""
@@ -105,10 +114,14 @@ msgstr ""
 msgid "Arch"
 msgid "Arch"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/Passkey.vue:119
+msgid "Are you sure to delete this passkey immediately?"
+msgstr ""
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 msgid "Are you sure you want to clear all notifications?"
 msgid "Are you sure you want to clear all notifications?"
@@ -155,7 +168,7 @@ msgstr ""
 msgid "Assistant"
 msgid "Assistant"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -163,6 +176,14 @@ msgstr ""
 msgid "Auth"
 msgid "Auth"
 msgstr ""
 msgstr ""
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -198,15 +219,15 @@ msgstr ""
 msgid "Back to list"
 msgid "Back to list"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr ""
 msgstr ""
 
 
@@ -247,7 +268,7 @@ msgstr ""
 msgid "CADir"
 msgid "CADir"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -261,6 +282,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -412,6 +434,7 @@ msgid "Create Folder"
 msgstr ""
 msgstr ""
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr ""
 msgstr ""
@@ -432,12 +455,12 @@ msgstr ""
 msgid "Credentials"
 msgid "Credentials"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+msgid "Current account is enabled TOTP."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
@@ -652,6 +675,10 @@ msgstr ""
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid "Due to the security policies of some browsers, you cannot use passkeys on non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -722,11 +749,7 @@ msgstr ""
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:124
-msgid "Enable 2FA"
-msgstr ""
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr ""
 msgstr ""
 
 
@@ -747,6 +770,10 @@ msgstr ""
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/TOTP.vue:125
+msgid "Enable TOTP"
+msgstr ""
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175
 #: src/views/domain/DomainEdit.vue:175
 #: src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainList.vue:29
@@ -868,6 +895,10 @@ msgstr ""
 msgid "Finished"
 msgid "Finished"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid "Follow the instructions in the dialog to complete the passkey registration process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr ""
 msgstr ""
@@ -937,14 +968,18 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 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."
 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 ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid "If you lose your mobile phone, you can use the recovery code to reset your 2FA."
 msgid "If you lose your mobile phone, you can use the recovery code to reset your 2FA."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid "If your domain has CNAME records and you cannot obtain certificates, you need to enable this option."
 msgid "If your domain has CNAME records and you cannot obtain certificates, you need to enable this option."
 msgstr ""
 msgstr ""
@@ -958,7 +993,7 @@ msgstr ""
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr ""
 msgstr ""
 
 
@@ -975,12 +1010,12 @@ msgstr ""
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr ""
 msgstr ""
 
 
@@ -1001,7 +1036,7 @@ msgstr ""
 msgid "Invalid"
 msgid "Invalid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -1014,11 +1049,11 @@ msgstr ""
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr ""
 msgstr ""
 
 
@@ -1051,6 +1086,10 @@ msgstr ""
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/Passkey.vue:102
+msgid "Last used at"
+msgstr ""
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr ""
 msgstr ""
@@ -1111,12 +1150,12 @@ msgid "Log"
 msgstr ""
 msgstr ""
 
 
 #: src/routes/index.ts:305
 #: src/routes/index.ts:305
-#: src/views/other/Login.vue:207
+#: src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:130
-#: src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133
+#: src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr ""
 msgstr ""
 
 
@@ -1162,7 +1201,7 @@ msgstr ""
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -1219,6 +1258,7 @@ msgstr ""
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13
 #: src/views/stream/StreamList.vue:13
@@ -1303,7 +1343,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1401,6 +1441,11 @@ msgstr ""
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr ""
 msgstr ""
 
 
+#: src/components/2FA/Authorization.vue:117
+#: src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr ""
 msgstr ""
@@ -1429,7 +1474,15 @@ msgstr ""
 msgid "Params"
 msgid "Params"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:174
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid "Passkeys are webauthn credentials that validate your identity using touch, facial recognition, a device password, or a PIN. They can be used as a password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208
 #: src/views/user/userColumns.tsx:18
 #: src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr ""
 msgstr ""
@@ -1456,8 +1509,12 @@ msgstr ""
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid "Please enter a name for the passkey you wish to create and click the OK button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+msgid "Please enter the OTP code:"
 msgstr ""
 msgstr ""
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1491,12 +1548,12 @@ msgid "Please input your E-mail!"
 msgstr ""
 msgstr ""
 
 
 #: src/views/other/Install.vue:44
 #: src/views/other/Install.vue:44
-#: src/views/other/Login.vue:44
+#: src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr ""
 msgstr ""
 
 
 #: src/views/other/Install.vue:38
 #: src/views/other/Install.vue:38
-#: src/views/other/Login.vue:38
+#: src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr ""
 msgstr ""
 
 
@@ -1559,16 +1616,16 @@ msgstr ""
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr ""
 msgstr ""
 
 
@@ -1588,6 +1645,10 @@ msgstr ""
 msgid "Register failed"
 msgid "Register failed"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+msgid "Register passkey successfully"
+msgstr ""
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 msgid "Register successfully"
 msgid "Register successfully"
 msgstr ""
 msgstr ""
@@ -1621,11 +1682,12 @@ msgstr ""
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr ""
 msgstr ""
 
 
@@ -1689,7 +1751,7 @@ msgstr ""
 msgid "Reset"
 msgid "Reset"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr ""
 msgstr ""
 
 
@@ -1714,6 +1776,7 @@ msgstr ""
 #: src/views/config/ConfigEditor.vue:222
 #: src/views/config/ConfigEditor.vue:222
 #: src/views/domain/DomainEdit.vue:260
 #: src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151
 #: src/views/preference/Preference.vue:151
 #: src/views/stream/StreamEdit.vue:252
 #: src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
@@ -1744,7 +1807,7 @@ msgstr ""
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 
 
@@ -1752,7 +1815,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1779,9 +1842,12 @@ msgstr ""
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15
 #: src/views/environment/Environment.vue:15
 #: src/views/other/Install.vue:68
 #: src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83
 #: src/views/preference/Preference.vue:83
 #: src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81
 #: src/views/stream/StreamList.vue:81
@@ -1825,6 +1891,10 @@ msgstr ""
 msgid "Show"
 msgid "Show"
 msgstr ""
 msgstr ""
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr ""
 msgstr ""
@@ -1853,7 +1923,7 @@ msgstr ""
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr ""
 msgstr ""
 
 
@@ -2001,7 +2071,7 @@ msgstr ""
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid "The recovery code is only displayed once, please save it in a safe place."
 msgid "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
 
 
@@ -2053,7 +2123,8 @@ msgstr ""
 msgid "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgid "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr ""
 msgstr ""
@@ -2062,10 +2133,14 @@ msgstr ""
 msgid "Title"
 msgid "Title"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid "To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone."
 msgid "To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid "To ensure security, Webauthn configuration cannot be added through the UI. Please manually configure the following in the app.ini configuration file and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 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?"
 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 ""
 msgstr ""
@@ -2074,11 +2149,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 msgstr ""
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid "TOTP is a two-factor authentication method that uses a time-based one-time password algorithm."
 msgid "TOTP is a two-factor authentication method that uses a time-based one-time password algorithm."
 msgstr ""
 msgstr ""
 
 
@@ -2086,7 +2165,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr ""
 msgstr ""
 
 
@@ -2096,6 +2175,10 @@ msgstr ""
 msgid "Type"
 msgid "Type"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/Passkey.vue:41
+msgid "Update successfully"
+msgstr ""
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31
 #: src/views/config/configColumns.ts:31
@@ -2145,11 +2228,11 @@ msgstr ""
 msgid "URL"
 msgid "URL"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -2157,11 +2240,11 @@ msgstr ""
 msgid "User"
 msgid "User"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:164
+#: src/views/other/Login.vue:198
 #: src/views/user/userColumns.tsx:9
 #: src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr ""
 msgstr ""
@@ -2201,6 +2284,7 @@ msgstr ""
 #: src/views/config/InspectConfig.vue:33
 #: src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr ""
 msgstr ""
 
 
@@ -2227,7 +2311,7 @@ msgstr ""
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr ""
 msgstr ""
@@ -2239,3 +2323,11 @@ msgstr ""
 #: src/views/system/Upgrade.vue:166
 #: src/views/system/Upgrade.vue:166
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr ""
 msgstr ""
+
+#: src/views/preference/components/AddPasskey.vue:93
+msgid "You have not configured the settings of Webauthn, so you cannot add a passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""

+ 174 - 58
app/src/language/ru_RU/app.po

@@ -18,7 +18,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr "2ФА"
 msgstr "2ФА"
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr "Настройки 2ФА"
 msgstr "Настройки 2ФА"
 
 
@@ -41,7 +41,7 @@ msgstr "Пользователь ACME"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "Действие"
 msgstr "Действие"
@@ -56,6 +56,11 @@ msgstr "Действие"
 msgid "Add"
 msgid "Add"
 msgstr "Добавить"
 msgstr "Добавить"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
 msgid "Add Configuration"
@@ -90,6 +95,10 @@ msgstr "Дополнительно"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "Расширенный режим"
 msgstr "Расширенный режим"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr "Базовый URL API"
 msgstr "Базовый URL API"
@@ -110,10 +119,15 @@ msgstr "API токен"
 msgid "Arch"
 msgid "Arch"
 msgstr "Архитектура"
 msgstr "Архитектура"
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Вы уверены, что хотите немедленно удалить этот заблокированный IP?"
 msgstr "Вы уверены, что хотите немедленно удалить этот заблокированный IP?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "Вы уверены, что хотите немедленно удалить этот заблокированный IP?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 msgid "Are you sure you want to clear all notifications?"
 msgid "Are you sure you want to clear all notifications?"
@@ -161,7 +175,7 @@ msgstr "Обратитесь за помощью к ChatGPT"
 msgid "Assistant"
 msgid "Assistant"
 msgstr "Ассистент"
 msgstr "Ассистент"
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr "Попытки"
 msgstr "Попытки"
 
 
@@ -169,6 +183,14 @@ msgstr "Попытки"
 msgid "Auth"
 msgid "Auth"
 msgstr "Авторизация"
 msgstr "Авторизация"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -201,15 +223,15 @@ msgstr "Вернуться на главную"
 msgid "Back to list"
 msgid "Back to list"
 msgstr "Возврат к списку"
 msgstr "Возврат к списку"
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr "Порог блокировки в минутах"
 msgstr "Порог блокировки в минутах"
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr "Заблокированные IP-адреса"
 msgstr "Заблокированные IP-адреса"
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr "Заблокирован до"
 msgstr "Заблокирован до"
 
 
@@ -250,7 +272,7 @@ msgstr "Директория корневого сертификата"
 msgid "CADir"
 msgid "CADir"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -264,6 +286,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -413,6 +436,7 @@ msgid "Create Folder"
 msgstr "Создать папку"
 msgstr "Создать папку"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "Создан в"
 msgstr "Создан в"
@@ -433,12 +457,14 @@ msgstr "Учетные данные"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "Учетные данные"
 msgstr "Учетные данные"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+#, fuzzy
+msgid "Current account is enabled TOTP."
 msgstr "Текущая учетная запись имеет включенную 2ФА."
 msgstr "Текущая учетная запись имеет включенную 2ФА."
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+#, fuzzy
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 "Текущая учетная запись не имеет включенной двухфакторной аутентификации."
 "Текущая учетная запись не имеет включенной двухфакторной аутентификации."
 
 
@@ -650,6 +676,12 @@ msgstr "Загрузка последней версии"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr "Включен пробный режим"
 msgstr "Включен пробный режим"
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -720,11 +752,7 @@ msgstr "Включение %{conf_name} in %{node_name} нипалучилася
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Включение %{conf_name} in %{node_name} успешно"
 msgstr "Включение %{conf_name} in %{node_name} успешно"
 
 
-#: src/views/preference/components/TOTP.vue:124
-msgid "Enable 2FA"
-msgstr "Включить 2ФА"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "Двухфакторная аутентификация успешно включена"
 msgstr "Двухфакторная аутентификация успешно включена"
 
 
@@ -745,6 +773,11 @@ msgstr "Включено успешно"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "Включить TLS"
 msgstr "Включить TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "Включить TLS"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -858,6 +891,12 @@ msgstr "Фильтр"
 msgid "Finished"
 msgid "Finished"
 msgstr "Готово"
 msgstr "Готово"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "Для китайских пользователей: https://mirror.ghproxy.com/"
 msgstr "Для китайских пользователей: https://mirror.ghproxy.com/"
@@ -926,7 +965,7 @@ msgstr "HTTP01"
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr "Если оставить пустым, будет использоваться каталог CA по умолчанию."
 msgstr "Если оставить пустым, будет использоваться каталог CA по умолчанию."
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
@@ -935,7 +974,7 @@ msgstr ""
 "количества попыток в течение пороговых минут блокировки, IP будет "
 "количества попыток в течение пороговых минут блокировки, IP будет "
 "заблокирован на определенный период времени."
 "заблокирован на определенный период времени."
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
@@ -943,6 +982,10 @@ msgstr ""
 "Если вы потеряете свой мобильный телефон, вы можете использовать код "
 "Если вы потеряете свой мобильный телефон, вы можете использовать код "
 "восстановления для сброса 2FA."
 "восстановления для сброса 2FA."
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -959,7 +1002,7 @@ msgstr "Импорт"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "Импортировать сертификат"
 msgstr "Импортировать сертификат"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "Неверное имя пользователя или пароль"
 msgstr "Неверное имя пользователя или пароль"
 
 
@@ -975,12 +1018,12 @@ msgstr "Ошибка первоначального обновления ядр
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "Инициализация программы обновления ядра"
 msgstr "Инициализация программы обновления ядра"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr "Введите код из приложения:"
 msgstr "Введите код из приложения:"
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr "Введите код восстановления:"
 msgstr "Введите код восстановления:"
 
 
@@ -1000,7 +1043,7 @@ msgstr "Интервал"
 msgid "Invalid"
 msgid "Invalid"
 msgstr "Недействительно"
 msgstr "Недействительно"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr "Неверный 2FA или код восстановления"
 msgstr "Неверный 2FA или код восстановления"
 
 
@@ -1013,11 +1056,11 @@ msgstr "Неверное имя файла"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr "Недопустимое имя папки"
 msgstr "Недопустимое имя папки"
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr "Неверный пароль или код восстановления"
 msgstr "Неверный пароль или код восстановления"
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr "IP"
 msgstr "IP"
 
 
@@ -1050,6 +1093,11 @@ msgstr "Тип ключа"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "Последняя проверка в"
 msgstr "Последняя проверка в"
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "Последняя проверка в"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "Оставьте пустым без изменений"
 msgstr "Оставьте пустым без изменений"
@@ -1109,11 +1157,11 @@ msgstr "Locations"
 msgid "Log"
 msgid "Log"
 msgstr "Журнал"
 msgstr "Журнал"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "Логин"
 msgstr "Логин"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "Авторизация успешна"
 msgstr "Авторизация успешна"
 
 
@@ -1170,7 +1218,7 @@ msgstr "Пользователи"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "Управление сертификатом"
 msgstr "Управление сертификатом"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr "Максимальное количество попыток"
 msgstr "Максимальное количество попыток"
 
 
@@ -1225,6 +1273,7 @@ msgstr "Многострочная директива"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1306,7 +1355,7 @@ msgstr "Nginx успешно перезапущен"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1407,6 +1456,10 @@ msgstr "Онлайн"
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr "OpenAI"
 msgstr "OpenAI"
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr "Оригинальное имя"
 msgstr "Оригинальное имя"
@@ -1435,7 +1488,18 @@ msgstr "Перезаписать существующий файл"
 msgid "Params"
 msgid "Params"
 msgstr "Параметры"
 msgstr "Параметры"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "Пароль"
 msgstr "Пароль"
 
 
@@ -1461,8 +1525,15 @@ msgstr "Ошибка обновления ядра"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "Выполнение обновления ядра"
 msgstr "Выполнение обновления ядра"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+#, fuzzy
+msgid "Please enter the OTP code:"
 msgstr "Пожалуйста, введите код 2FA:"
 msgstr "Пожалуйста, введите код 2FA:"
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1508,11 +1579,11 @@ msgstr ""
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "Введите ваш E-mail!"
 msgstr "Введите ваш E-mail!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "Введите ваш пароль!"
 msgstr "Введите ваш пароль!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "Введите ваше имя пользователя!"
 msgstr "Введите ваше имя пользователя!"
 
 
@@ -1576,16 +1647,16 @@ msgstr "Восстановить"
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "Восстановлено успешно"
 msgstr "Восстановлено успешно"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr "Восстановление"
 msgstr "Восстановление"
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr "Код восстановления"
 msgstr "Код восстановления"
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr "Код восстановления:"
 msgstr "Код восстановления:"
 
 
@@ -1605,6 +1676,11 @@ msgstr "Регистрация"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "Регистрация не удалась"
 msgstr "Регистрация не удалась"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "Зарегистрировано успешно"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 msgid "Register successfully"
 msgid "Register successfully"
 msgstr "Зарегистрировано успешно"
 msgstr "Зарегистрировано успешно"
@@ -1638,11 +1714,12 @@ msgstr "Перезагружается"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "Перезагружается nginx"
 msgstr "Перезагружается nginx"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr "Удалить"
 msgstr "Удалить"
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "Удалено успешно"
 msgstr "Удалено успешно"
 
 
@@ -1713,7 +1790,7 @@ msgstr "Запрос с неправильными параметрами"
 msgid "Reset"
 msgid "Reset"
 msgstr "Сброс"
 msgstr "Сброс"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "Сброс 2FA"
 msgstr "Сброс 2FA"
 
 
@@ -1737,6 +1814,7 @@ msgstr "Выполняется"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "Сохранить"
 msgstr "Сохранить"
@@ -1764,7 +1842,7 @@ msgstr "Сохранено успешно"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "Успешно сохранено"
 msgstr "Успешно сохранено"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 "Отсканируйте QR-код с помощью мобильного телефона, чтобы добавить учетную "
 "Отсканируйте QR-код с помощью мобильного телефона, чтобы добавить учетную "
@@ -1774,7 +1852,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr "SDK"
 msgstr "SDK"
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1799,9 +1877,12 @@ msgstr "Отправлено"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1847,6 +1928,10 @@ msgstr ""
 msgid "Show"
 msgid "Show"
 msgstr "Показать"
 msgstr "Показать"
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "Одиночная Директива"
 msgstr "Одиночная Директива"
@@ -1875,7 +1960,7 @@ msgstr "Путь к ключу SSL-сертификата"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "Путь к SSL сертификату"
 msgstr "Путь к SSL сертификату"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "SSO Вход"
 msgstr "SSO Вход"
 
 
@@ -2042,7 +2127,7 @@ msgstr "Путь существует, но файл не является се
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr "Путь существует, но файл не является приватным ключом"
 msgstr "Путь существует, но файл не является приватным ключом"
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
@@ -2114,7 +2199,8 @@ msgstr ""
 "Это обновит или переустановит интерфейс Nginx на %{nodeNames} до версии "
 "Это обновит или переустановит интерфейс Nginx на %{nodeNames} до версии "
 "%{version}."
 "%{version}."
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr "Советы"
 msgstr "Советы"
@@ -2123,7 +2209,7 @@ msgstr "Советы"
 msgid "Title"
 msgid "Title"
 msgstr "Заголовок"
 msgstr "Заголовок"
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
@@ -2131,6 +2217,13 @@ msgstr ""
 "Чтобы включить это, вам нужно установить приложение Google или Microsoft "
 "Чтобы включить это, вам нужно установить приложение Google или Microsoft "
 "Authenticator на свой мобильный телефон."
 "Authenticator на свой мобильный телефон."
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2147,11 +2240,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr "Токен недействителен"
 msgstr "Токен недействителен"
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr "Слишком много неудачных попыток входа, попробуйте позже"
 msgstr "Слишком много неудачных попыток входа, попробуйте позже"
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2163,7 +2260,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr "Корзина"
 msgstr "Корзина"
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr "Требуется двухфакторная аутентификация"
 msgstr "Требуется двухфакторная аутентификация"
 
 
@@ -2173,6 +2270,11 @@ msgstr "Требуется двухфакторная аутентификаци
 msgid "Type"
 msgid "Type"
 msgstr "Тип"
 msgstr "Тип"
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "Успешно обновлено"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2216,11 +2318,11 @@ msgstr "Аптайм:"
 msgid "URL"
 msgid "URL"
 msgstr "URL"
 msgstr "URL"
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr "Использовать OTP"
 msgstr "Использовать OTP"
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr "Использовать код восстановления"
 msgstr "Использовать код восстановления"
 
 
@@ -2228,11 +2330,11 @@ msgstr "Использовать код восстановления"
 msgid "User"
 msgid "User"
 msgstr "Пользователь"
 msgstr "Пользователь"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr "Пользователь заблокирован"
 msgstr "Пользователь заблокирован"
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "Имя пользователя"
 msgstr "Имя пользователя"
 
 
@@ -2271,6 +2373,7 @@ msgstr "Простой режим"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "Внимание"
 msgstr "Внимание"
 
 
@@ -2305,7 +2408,7 @@ msgstr "Запись сертификата на диск"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "Да"
 msgstr "Да"
@@ -2318,6 +2421,19 @@ msgstr "Вы используете последнюю версию"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Вы можете проверить обновление Nginx UI на этой странице."
 msgstr "Вы можете проверить обновление Nginx UI на этой странице."
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#~ msgid "Enable 2FA"
+#~ msgstr "Включить 2ФА"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Rename "
 #~ msgid "Rename "
 #~ msgstr "Имя пользователя"
 #~ msgstr "Имя пользователя"

+ 172 - 59
app/src/language/vi_VN/app.po

@@ -13,7 +13,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr ""
 msgstr ""
 
 
@@ -37,7 +37,7 @@ msgstr "Người dùng"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "Hành động"
 msgstr "Hành động"
@@ -52,6 +52,11 @@ msgstr "Hành động"
 msgid "Add"
 msgid "Add"
 msgstr "Thêm"
 msgstr "Thêm"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 #, fuzzy
 #, fuzzy
@@ -90,6 +95,10 @@ msgstr "Tùy chọn bổ sung"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "Nâng cao"
 msgstr "Nâng cao"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr ""
 msgstr ""
@@ -110,11 +119,16 @@ msgstr ""
 msgid "Arch"
 msgid "Arch"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 #, fuzzy
 #, fuzzy
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "Bạn chắc chắn muốn xóa nó "
 msgstr "Bạn chắc chắn muốn xóa nó "
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "Bạn chắc chắn muốn xóa nó "
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 #, fuzzy
 #, fuzzy
@@ -168,7 +182,7 @@ msgstr "Hỏi ChatGPT"
 msgid "Assistant"
 msgid "Assistant"
 msgstr "Trợ lý"
 msgstr "Trợ lý"
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -177,6 +191,14 @@ msgstr ""
 msgid "Auth"
 msgid "Auth"
 msgstr "Tác giả"
 msgstr "Tác giả"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -210,15 +232,15 @@ msgstr "Quay lại"
 msgid "Back to list"
 msgid "Back to list"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr ""
 msgstr ""
 
 
@@ -261,7 +283,7 @@ msgstr ""
 msgid "CADir"
 msgid "CADir"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -275,6 +297,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -437,6 +460,7 @@ msgid "Create Folder"
 msgstr "Tạo thêm"
 msgstr "Tạo thêm"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "Ngày tạo"
 msgstr "Ngày tạo"
@@ -458,12 +482,12 @@ msgstr "Chứng chỉ"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "Chứng chỉ"
 msgstr "Chứng chỉ"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+msgid "Current account is enabled TOTP."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+msgid "Current account is not enabled TOTP."
 msgstr ""
 msgstr ""
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
@@ -687,6 +711,12 @@ msgstr "Đang tải phiên bản mới nhất"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr "Đã bật chế độ Dry run"
 msgstr "Đã bật chế độ Dry run"
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -762,12 +792,7 @@ msgstr "Không thể bật %{conf_name} trên %{node_name}"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "Đã bật %{conf_name} trên %{node_name}"
 msgstr "Đã bật %{conf_name} trên %{node_name}"
 
 
-#: src/views/preference/components/TOTP.vue:124
-#, fuzzy
-msgid "Enable 2FA"
-msgstr "Đã bật"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 #, fuzzy
 #, fuzzy
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "Đã bật"
 msgstr "Đã bật"
@@ -790,6 +815,11 @@ msgstr "Đã bật"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "Bật TLS"
 msgstr "Bật TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "Bật TLS"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -908,6 +938,12 @@ msgstr "Lọc"
 msgid "Finished"
 msgid "Finished"
 msgstr "Đã hoàn thành"
 msgstr "Đã hoàn thành"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 #, fuzzy
 #, fuzzy
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
@@ -981,18 +1017,22 @@ msgstr ""
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -1009,7 +1049,7 @@ msgstr "Xuất"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "Chứng chỉ"
 msgstr "Chứng chỉ"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 #, fuzzy
 #, fuzzy
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "Tên người dùng hoặc mật khẩu không chính xác"
 msgstr "Tên người dùng hoặc mật khẩu không chính xác"
@@ -1026,12 +1066,12 @@ msgstr "Không thể khởi tạo trình nâng cấp"
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "Đang khởi tạo trình nâng cấp"
 msgstr "Đang khởi tạo trình nâng cấp"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr ""
 msgstr ""
 
 
@@ -1053,7 +1093,7 @@ msgstr ""
 msgid "Invalid"
 msgid "Invalid"
 msgstr "Hợp lệ"
 msgstr "Hợp lệ"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -1067,11 +1107,11 @@ msgstr "E-mail không chính xác!"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr ""
 msgstr ""
 
 
@@ -1108,6 +1148,11 @@ msgstr "Loại"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "Kiểm tra lần cuối lúc"
 msgstr "Kiểm tra lần cuối lúc"
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "Kiểm tra lần cuối lúc"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "Bỏ trống nếu không thay đổi"
 msgstr "Bỏ trống nếu không thay đổi"
@@ -1173,11 +1218,11 @@ msgstr "Locations"
 msgid "Log"
 msgid "Log"
 msgstr "Log"
 msgstr "Log"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "Đăng nhập"
 msgstr "Đăng nhập"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "Đăng nhập thành công"
 msgstr "Đăng nhập thành công"
 
 
@@ -1230,7 +1275,7 @@ msgstr "Người dùng"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr ""
 msgstr ""
 
 
@@ -1290,6 +1335,7 @@ msgstr "Single Directive"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1376,7 +1422,7 @@ msgstr "Restart Nginx thành công"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1478,6 +1524,10 @@ msgstr "Trực tuyến"
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr ""
 msgstr ""
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr ""
 msgstr ""
@@ -1507,7 +1557,18 @@ msgstr "Ghi đè tập tin đã tồn tại"
 msgid "Params"
 msgid "Params"
 msgstr "Tham số"
 msgstr "Tham số"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "Mật khẩu"
 msgstr "Mật khẩu"
 
 
@@ -1533,8 +1594,14 @@ msgstr "Nâng cấp core không thành công"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "Nâng cấp core"
 msgstr "Nâng cấp core"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+msgid "Please enter the OTP code:"
 msgstr ""
 msgstr ""
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1579,11 +1646,11 @@ msgstr ""
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "Vui lòng nhập E-mail của bạn!"
 msgstr "Vui lòng nhập E-mail của bạn!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "Vui lòng nhập mật khẩu!"
 msgstr "Vui lòng nhập mật khẩu!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "Vui lòng nhập username!"
 msgstr "Vui lòng nhập username!"
 
 
@@ -1647,16 +1714,16 @@ msgstr ""
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "Xoá thành công"
 msgstr "Xoá thành công"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr ""
 msgstr ""
 
 
@@ -1678,6 +1745,11 @@ msgstr "Đăng ký người dùng"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "Đăng ký người dùng"
 msgstr "Đăng ký người dùng"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "Gia hạn chứng chỉ SSL"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 #, fuzzy
 #, fuzzy
 msgid "Register successfully"
 msgid "Register successfully"
@@ -1714,11 +1786,12 @@ msgstr "Đang tải lại"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "Tải lại nginx"
 msgstr "Tải lại nginx"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 #, fuzzy
 #, fuzzy
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "Xoá thành công"
 msgstr "Xoá thành công"
@@ -1798,7 +1871,7 @@ msgstr "Yêu cầu có chứa tham số sai"
 msgid "Reset"
 msgid "Reset"
 msgstr "Đặt lại"
 msgstr "Đặt lại"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 #, fuzzy
 #, fuzzy
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "Đặt lại"
 msgstr "Đặt lại"
@@ -1824,6 +1897,7 @@ msgstr "Running"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "Lưu"
 msgstr "Lưu"
@@ -1852,7 +1926,7 @@ msgstr "Lưu thành công"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "Lưu thành công"
 msgstr "Lưu thành công"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr ""
 msgstr ""
 
 
@@ -1860,7 +1934,7 @@ msgstr ""
 msgid "SDK"
 msgid "SDK"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1885,9 +1959,12 @@ msgstr "Gửi"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1933,6 +2010,10 @@ msgstr "Sử dụng HTTP01 để xác thực SSL"
 msgid "Show"
 msgid "Show"
 msgstr ""
 msgstr ""
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "Single Directive"
 msgstr "Single Directive"
@@ -1962,7 +2043,7 @@ msgstr ""
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr ""
 msgstr ""
 
 
@@ -2130,7 +2211,7 @@ msgstr ""
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr ""
 msgstr ""
@@ -2193,7 +2274,8 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
 msgstr ""
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr ""
 msgstr ""
@@ -2202,12 +2284,19 @@ msgstr ""
 msgid "Title"
 msgid "Title"
 msgstr "Tiêu đề"
 msgstr "Tiêu đề"
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2224,11 +2313,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr ""
 msgstr ""
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2238,7 +2331,7 @@ msgstr ""
 msgid "Trash"
 msgid "Trash"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr ""
 msgstr ""
 
 
@@ -2248,6 +2341,11 @@ msgstr ""
 msgid "Type"
 msgid "Type"
 msgstr "Loại"
 msgstr "Loại"
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "Cập nhật thành công"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2294,11 +2392,11 @@ msgstr "Thời gian hoạt động:"
 msgid "URL"
 msgid "URL"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr ""
 msgstr ""
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr ""
 msgstr ""
 
 
@@ -2307,11 +2405,11 @@ msgstr ""
 msgid "User"
 msgid "User"
 msgstr "Người dùng"
 msgstr "Người dùng"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr ""
 msgstr ""
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "Username"
 msgstr "Username"
 
 
@@ -2353,6 +2451,7 @@ msgstr "Cơ bản"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "Lưu ý"
 msgstr "Lưu ý"
 
 
@@ -2387,7 +2486,7 @@ msgstr "Ghi chứng chỉ vào disk"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "Có"
 msgstr "Có"
@@ -2400,6 +2499,20 @@ msgstr "Bạn đang sử dụng phiên bản mới nhất"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Bạn có thể kiểm tra nâng cấp Nginx UI tại trang này"
 msgstr "Bạn có thể kiểm tra nâng cấp Nginx UI tại trang này"
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#, fuzzy
+#~ msgid "Enable 2FA"
+#~ msgstr "Đã bật"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Rename "
 #~ msgid "Rename "
 #~ msgstr "Username"
 #~ msgstr "Username"

二进制
app/src/language/zh_CN/app.mo


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

@@ -17,7 +17,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr "2FA"
 msgstr "2FA"
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr "2FA 设置"
 msgstr "2FA 设置"
 
 
@@ -40,7 +40,7 @@ msgstr "ACME 用户"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "操作"
 msgstr "操作"
@@ -55,6 +55,11 @@ msgstr "操作"
 msgid "Add"
 msgid "Add"
 msgstr "添加"
 msgstr "添加"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr "添加 Passkey"
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
 msgid "Add Configuration"
@@ -89,6 +94,10 @@ msgstr "额外选项"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "高级模式"
 msgstr "高级模式"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr "然后,刷新此页面并再次点击添加 Passkey。"
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr "API 地址"
 msgstr "API 地址"
@@ -109,10 +118,14 @@ msgstr "API Token"
 msgid "Arch"
 msgid "Arch"
 msgstr "架构"
 msgstr "架构"
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "您确定要立即删除这个被禁用的 IP 吗?"
 msgstr "您确定要立即删除这个被禁用的 IP 吗?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "您确定要立即删除这个 Passkey 吗?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 msgid "Are you sure you want to clear all notifications?"
 msgid "Are you sure you want to clear all notifications?"
@@ -158,7 +171,7 @@ msgstr "与ChatGPT聊天"
 msgid "Assistant"
 msgid "Assistant"
 msgstr "助手"
 msgstr "助手"
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr "尝试次数"
 msgstr "尝试次数"
 
 
@@ -166,6 +179,14 @@ msgstr "尝试次数"
 msgid "Auth"
 msgid "Auth"
 msgstr "认证"
 msgstr "认证"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr "通过 Passkey 认证"
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr "认证设置"
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -198,15 +219,15 @@ msgstr "返回首页"
 msgid "Back to list"
 msgid "Back to list"
 msgstr "返回列表"
 msgstr "返回列表"
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr "禁止阈值(分钟)"
 msgstr "禁止阈值(分钟)"
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr "禁止 IP 列表"
 msgstr "禁止 IP 列表"
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr "禁用至"
 msgstr "禁用至"
 
 
@@ -246,7 +267,7 @@ msgstr "CA Dir"
 msgid "CADir"
 msgid "CADir"
 msgstr "CADir"
 msgstr "CADir"
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr "无法扫描?使用文本密钥绑定"
 msgstr "无法扫描?使用文本密钥绑定"
 
 
@@ -260,6 +281,7 @@ msgstr "无法扫描?使用文本密钥绑定"
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -407,6 +429,7 @@ msgid "Create Folder"
 msgstr "创建文件夹"
 msgstr "创建文件夹"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "创建时间"
 msgstr "创建时间"
@@ -427,13 +450,13 @@ msgstr "DNS 凭证"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "凭证"
 msgstr "凭证"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
-msgstr "当前账户已启用二步验证。"
+#: src/views/preference/components/TOTP.vue:99
+msgid "Current account is enabled TOTP."
+msgstr "当前账户已启用 TOTP 验证。"
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
-msgstr "当前用户未启用二步验证。"
+#: src/views/preference/components/TOTP.vue:96
+msgid "Current account is not enabled TOTP."
+msgstr "当前用户未启用 TOTP 验证。"
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
 msgid "Current Version"
 msgid "Current Version"
@@ -640,6 +663,14 @@ msgstr "下载最新版本"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr "试运行模式已启动"
 msgstr "试运行模式已启动"
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+"由于某些浏览器的安全策略,除非在 localhost 上使用,否则不能在非 HTTPS 网站上"
+"使用 Passkey。"
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -708,11 +739,7 @@ msgstr "在%{node_name}中启用%{conf_name}失败"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功启用%{node_name}中的%{conf_name}"
 msgstr "成功启用%{node_name}中的%{conf_name}"
 
 
-#: src/views/preference/components/TOTP.vue:124
-msgid "Enable 2FA"
-msgstr "启用二步验证"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "二步验证启用成功"
 msgstr "二步验证启用成功"
 
 
@@ -733,6 +760,10 @@ msgstr "启用成功"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "启用 TLS"
 msgstr "启用 TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+msgid "Enable TOTP"
+msgstr "启用 TOTP"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -846,6 +877,12 @@ msgstr "过滤"
 msgid "Finished"
 msgid "Finished"
 msgstr "完成"
 msgstr "完成"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr "按照对话框中的指示完成 Passkey 的注册过程。"
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "中国用户:https://mirror.ghproxy.com/"
 msgstr "中国用户:https://mirror.ghproxy.com/"
@@ -914,7 +951,7 @@ msgstr "HTTP01"
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr "如果留空,则使用默认 CA Dir。"
 msgstr "如果留空,则使用默认 CA Dir。"
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
@@ -922,12 +959,16 @@ msgstr ""
 "如果某个 IP 的登录失败次数达到禁用阈值分钟内的最大尝试次数,该 IP 将被禁止登"
 "如果某个 IP 的登录失败次数达到禁用阈值分钟内的最大尝试次数,该 IP 将被禁止登"
 "录一段时间。"
 "录一段时间。"
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
 msgstr "如果丢失了手机,可以使用恢复代码重置二步验证。"
 msgstr "如果丢失了手机,可以使用恢复代码重置二步验证。"
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr "如果您的浏览器支持 WebAuthn Passkey,则会出现一个对话框。"
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -942,7 +983,7 @@ msgstr "导入"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "导入证书"
 msgstr "导入证书"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "用户名或密码错误"
 msgstr "用户名或密码错误"
 
 
@@ -958,12 +999,12 @@ msgstr "初始化核心升级程序错误"
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "初始化核心升级器"
 msgstr "初始化核心升级器"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr "输入应用程序中的代码:"
 msgstr "输入应用程序中的代码:"
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr "输入恢复代码:"
 msgstr "输入恢复代码:"
 
 
@@ -983,7 +1024,7 @@ msgstr "间隔"
 msgid "Invalid"
 msgid "Invalid"
 msgstr "无效的"
 msgstr "无效的"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr "无效的二步验证码或恢复密码"
 msgstr "无效的二步验证码或恢复密码"
 
 
@@ -996,11 +1037,11 @@ msgstr "文件名无效"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr "无效文件夹名"
 msgstr "无效文件夹名"
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr "二次验证码或恢复代码无效"
 msgstr "二次验证码或恢复代码无效"
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr "IP"
 msgstr "IP"
 
 
@@ -1033,6 +1074,10 @@ msgstr "密钥类型"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "最后检查时间"
 msgstr "最后检查时间"
 
 
+#: src/views/preference/components/Passkey.vue:102
+msgid "Last used at"
+msgstr "上次使用"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "留空表示不修改"
 msgstr "留空表示不修改"
@@ -1092,11 +1137,11 @@ msgstr "Locations"
 msgid "Log"
 msgid "Log"
 msgstr "日志"
 msgstr "日志"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "登录"
 msgstr "登录"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "登录成功"
 msgstr "登录成功"
 
 
@@ -1151,7 +1196,7 @@ msgstr "用户管理"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "托管证书"
 msgstr "托管证书"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr "最大尝试次数"
 msgstr "最大尝试次数"
 
 
@@ -1206,6 +1251,7 @@ msgstr "多行指令"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1287,7 +1333,7 @@ msgstr "Nginx 重启成功"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1386,6 +1432,10 @@ msgstr "在线"
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr "OpenAI"
 msgstr "OpenAI"
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr "或"
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr "原名"
 msgstr "原名"
@@ -1414,7 +1464,20 @@ msgstr "覆盖现有文件"
 msgid "Params"
 msgid "Params"
 msgstr "参数"
 msgstr "参数"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr "Passkey"
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+"Passkey 是一种网络认证凭据,可通过指纹、面部识别、设备密码或 PIN 码验证身份。"
+"它们可用作密码替代品或二步验证方法。"
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "密码"
 msgstr "密码"
 
 
@@ -1440,9 +1503,15 @@ msgstr "执行核心升级错误"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "正在进行核心升级"
 msgstr "正在进行核心升级"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
-msgstr "请输入二步验证码:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr "请为您要创建的 Passkey 输入一个名称,然后单击下面的 \"确定 \"按钮。"
+
+#: src/components/2FA/Authorization.vue:75
+msgid "Please enter the OTP code:"
+msgstr "请输入 OTP:"
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
 msgid ""
 msgid ""
@@ -1482,11 +1551,11 @@ msgstr "请输入名称,这将被用作新配置的文件名!"
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "请输入您的邮箱!"
 msgstr "请输入您的邮箱!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "请输入您的密码!"
 msgstr "请输入您的密码!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "请输入您的用户名!"
 msgstr "请输入您的用户名!"
 
 
@@ -1548,16 +1617,16 @@ msgstr "恢复"
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "恢复成功"
 msgstr "恢复成功"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr "恢复"
 msgstr "恢复"
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr "恢复代码"
 msgstr "恢复代码"
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr "恢复代码:"
 msgstr "恢复代码:"
 
 
@@ -1577,6 +1646,10 @@ msgstr "注册"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "注册失败"
 msgstr "注册失败"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+msgid "Register passkey successfully"
+msgstr "Passkey 注册成功"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 msgid "Register successfully"
 msgid "Register successfully"
 msgstr "注册成功"
 msgstr "注册成功"
@@ -1610,11 +1683,12 @@ msgstr "重载中"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "正在重载 Nginx"
 msgstr "正在重载 Nginx"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr "删除"
 msgstr "删除"
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "移除成功"
 msgstr "移除成功"
 
 
@@ -1684,7 +1758,7 @@ msgstr "请求参数错误"
 msgid "Reset"
 msgid "Reset"
 msgstr "重置"
 msgstr "重置"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "重置二步验证"
 msgstr "重置二步验证"
 
 
@@ -1708,6 +1782,7 @@ msgstr "运行中"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "保存"
 msgstr "保存"
@@ -1735,7 +1810,7 @@ msgstr "保存成功"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "保存成功"
 msgstr "保存成功"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 
 
@@ -1743,7 +1818,7 @@ msgstr "用手机扫描二维码,将账户添加到应用程序中。"
 msgid "SDK"
 msgid "SDK"
 msgstr "SDK"
 msgstr "SDK"
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr "密钥已复制"
 msgstr "密钥已复制"
 
 
@@ -1768,9 +1843,12 @@ msgstr "上传"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1814,6 +1892,10 @@ msgstr "使用 HTTP01 challenge provider"
 msgid "Show"
 msgid "Show"
 msgstr "显示"
 msgstr "显示"
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr "使用 Passkey 登录"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "单行指令"
 msgstr "单行指令"
@@ -1842,7 +1924,7 @@ msgstr "SSL证书密钥路径"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "SSL证书路径"
 msgstr "SSL证书路径"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "SSO 登录"
 msgstr "SSO 登录"
 
 
@@ -2000,7 +2082,7 @@ msgstr "路径存在,但文件不是证书"
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr "路径存在,但文件不是私钥"
 msgstr "路径存在,但文件不是私钥"
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr "恢复密码只会显示一次,请妥善保存。"
 msgstr "恢复密码只会显示一次,请妥善保存。"
@@ -2063,7 +2145,8 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr "将 %{nodeNames} 上的 Nginx UI 升级或重新安装到 %{version} 版本。"
 msgstr "将 %{nodeNames} 上的 Nginx UI 升级或重新安装到 %{version} 版本。"
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr "提示"
 msgstr "提示"
@@ -2072,13 +2155,22 @@ msgstr "提示"
 msgid "Title"
 msgid "Title"
 msgstr "标题"
 msgstr "标题"
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
 msgstr ""
 msgstr ""
 "要启用该功能,您需要在手机上安装 Google 或 Microsoft Authenticator 应用程序。"
 "要启用该功能,您需要在手机上安装 Google 或 Microsoft Authenticator 应用程序。"
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+"为确保安全,Webauthn 配置不能通过用户界面添加。请在 app.ini 配置文件中手动配"
+"置以下内容,并重启 Nginx UI 服务。"
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2093,11 +2185,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr "Token 无效"
 msgstr "Token 无效"
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr "登录失败次数过多,请稍后再试"
 msgstr "登录失败次数过多,请稍后再试"
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr "TOTP"
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2107,7 +2203,7 @@ msgstr "TOTP 是一种使用基于时间的一次性密码算法的双因素身
 msgid "Trash"
 msgid "Trash"
 msgstr "回收站"
 msgstr "回收站"
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr "需要两步验证"
 msgstr "需要两步验证"
 
 
@@ -2117,6 +2213,10 @@ msgstr "需要两步验证"
 msgid "Type"
 msgid "Type"
 msgstr "类型"
 msgstr "类型"
 
 
+#: src/views/preference/components/Passkey.vue:41
+msgid "Update successfully"
+msgstr "更新成功"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2160,11 +2260,11 @@ msgstr "运行时间:"
 msgid "URL"
 msgid "URL"
 msgstr "URL"
 msgstr "URL"
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr "使用二步验证码"
 msgstr "使用二步验证码"
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr "使用恢复代码"
 msgstr "使用恢复代码"
 
 
@@ -2172,11 +2272,11 @@ msgstr "使用恢复代码"
 msgid "User"
 msgid "User"
 msgstr "用户"
 msgstr "用户"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr "用户被禁止"
 msgstr "用户被禁止"
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "用户名"
 msgstr "用户名"
 
 
@@ -2214,6 +2314,7 @@ msgstr "预览模式"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "警告"
 msgstr "警告"
 
 
@@ -2245,7 +2346,7 @@ msgstr "正在将证书写入磁盘"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "是的"
 msgstr "是的"
@@ -2258,6 +2359,19 @@ msgstr "您使用的是最新版本"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "你可以在这个页面检查Nginx UI的升级。"
 msgstr "你可以在这个页面检查Nginx UI的升级。"
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr "您尚未配置 Webauthn 的设置,因此无法添加 Passkey。"
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr "你的 Passkeys"
+
+#~ msgid "Enable 2FA"
+#~ msgstr "启用二步验证"
+
 #~ msgid "Enter"
 #~ msgid "Enter"
 #~ msgstr "进入"
 #~ msgstr "进入"
 
 

+ 174 - 58
app/src/language/zh_TW/app.po

@@ -21,7 +21,7 @@ msgstr ""
 msgid "2FA"
 msgid "2FA"
 msgstr "多重要素驗證"
 msgstr "多重要素驗證"
 
 
-#: src/views/preference/components/TOTP.vue:90
+#: src/views/preference/AuthSettings.vue:58
 msgid "2FA Settings"
 msgid "2FA Settings"
 msgstr "多重要素驗證設定"
 msgstr "多重要素驗證設定"
 
 
@@ -44,7 +44,7 @@ msgstr "ACME 用戶"
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/notificationColumns.tsx:54
 #: src/views/notification/notificationColumns.tsx:54
-#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
+#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
 msgid "Action"
 msgstr "操作"
 msgstr "操作"
@@ -59,6 +59,11 @@ msgstr "操作"
 msgid "Add"
 msgid "Add"
 msgstr "新增"
 msgstr "新增"
 
 
+#: src/views/preference/components/AddPasskey.vue:51
+#: src/views/preference/components/AddPasskey.vue:55
+msgid "Add a passkey"
+msgstr ""
+
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:143
 #: src/views/config/ConfigEditor.vue:204
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
 msgid "Add Configuration"
@@ -93,6 +98,10 @@ msgstr "其他設定"
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr "進階模式"
 msgstr "進階模式"
 
 
+#: src/views/preference/components/AddPasskey.vue:105
+msgid "Afterwards, refresh this page and click add passkey again."
+msgstr ""
+
 #: src/views/preference/OpenAISettings.vue:44
 #: src/views/preference/OpenAISettings.vue:44
 msgid "API Base Url"
 msgid "API Base Url"
 msgstr "API 基礎網址"
 msgstr "API 基礎網址"
@@ -113,10 +122,15 @@ msgstr "API Token"
 msgid "Arch"
 msgid "Arch"
 msgstr "架構"
 msgstr "架構"
 
 
-#: src/views/preference/AuthSettings.vue:96
+#: src/views/preference/AuthSettings.vue:102
 msgid "Are you sure to delete this banned IP immediately?"
 msgid "Are you sure to delete this banned IP immediately?"
 msgstr "您確定要刪除這個被禁用的 IP 嗎?"
 msgstr "您確定要刪除這個被禁用的 IP 嗎?"
 
 
+#: src/views/preference/components/Passkey.vue:119
+#, fuzzy
+msgid "Are you sure to delete this passkey immediately?"
+msgstr "您確定要刪除這個被禁用的 IP 嗎?"
+
 #: src/components/Notification/Notification.vue:86
 #: src/components/Notification/Notification.vue:86
 #: src/views/notification/Notification.vue:40
 #: src/views/notification/Notification.vue:40
 msgid "Are you sure you want to clear all notifications?"
 msgid "Are you sure you want to clear all notifications?"
@@ -162,7 +176,7 @@ msgstr "向 ChatGPT 尋求幫助"
 msgid "Assistant"
 msgid "Assistant"
 msgstr "助理"
 msgstr "助理"
 
 
-#: src/views/preference/AuthSettings.vue:17
+#: src/views/preference/AuthSettings.vue:18
 msgid "Attempts"
 msgid "Attempts"
 msgstr "嘗試次數"
 msgstr "嘗試次數"
 
 
@@ -170,6 +184,14 @@ msgstr "嘗試次數"
 msgid "Auth"
 msgid "Auth"
 msgstr "身份驗證"
 msgstr "身份驗證"
 
 
+#: src/components/2FA/Authorization.vue:126
+msgid "Authenticate with a passkey"
+msgstr ""
+
+#: src/views/preference/AuthSettings.vue:63
+msgid "Authentication Settings"
+msgstr ""
+
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:106
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 #: src/views/domain/ngx_conf/config_template/ConfigTemplate.vue:120
 msgid "Author"
 msgid "Author"
@@ -202,15 +224,15 @@ msgstr "返回首頁"
 msgid "Back to list"
 msgid "Back to list"
 msgstr "返回列表"
 msgstr "返回列表"
 
 
-#: src/views/preference/AuthSettings.vue:70
+#: src/views/preference/AuthSettings.vue:76
 msgid "Ban Threshold Minutes"
 msgid "Ban Threshold Minutes"
 msgstr "封禁閾值分鐘數"
 msgstr "封禁閾值分鐘數"
 
 
-#: src/views/preference/AuthSettings.vue:84
+#: src/views/preference/AuthSettings.vue:90
 msgid "Banned IPs"
 msgid "Banned IPs"
 msgstr "被禁止的 IP"
 msgstr "被禁止的 IP"
 
 
-#: src/views/preference/AuthSettings.vue:20
+#: src/views/preference/AuthSettings.vue:21
 msgid "Banned Until"
 msgid "Banned Until"
 msgstr "禁止至"
 msgstr "禁止至"
 
 
@@ -250,7 +272,7 @@ msgstr "CA Dir"
 msgid "CADir"
 msgid "CADir"
 msgstr "CADir"
 msgstr "CADir"
 
 
-#: src/views/preference/components/TOTP.vue:150
+#: src/views/preference/components/TOTP.vue:151
 msgid "Can't scan? Use text key binding"
 msgid "Can't scan? Use text key binding"
 msgstr ""
 msgstr ""
 
 
@@ -264,6 +286,7 @@ msgstr ""
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:50
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxServer.vue:80
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
 #: src/views/domain/ngx_conf/NgxUpstream.vue:33
+#: src/views/preference/components/Passkey.vue:147
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/Deploy.vue:21
 #: src/views/stream/components/RightSettings.vue:51
 #: src/views/stream/components/RightSettings.vue:51
 msgid "Cancel"
 msgid "Cancel"
@@ -411,6 +434,7 @@ msgid "Create Folder"
 msgstr "創建資料夾"
 msgstr "創建資料夾"
 
 
 #: src/views/notification/notificationColumns.tsx:48
 #: src/views/notification/notificationColumns.tsx:48
+#: src/views/preference/components/Passkey.vue:101
 #: src/views/user/userColumns.tsx:48
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgid "Created at"
 msgstr "建立時間"
 msgstr "建立時間"
@@ -431,12 +455,14 @@ msgstr "認證"
 msgid "Credentials"
 msgid "Credentials"
 msgstr "認證資訊"
 msgstr "認證資訊"
 
 
-#: src/views/preference/components/TOTP.vue:98
-msgid "Current account is enabled 2FA."
+#: src/views/preference/components/TOTP.vue:99
+#, fuzzy
+msgid "Current account is enabled TOTP."
 msgstr "當前帳戶已啟用多因素身份驗證。"
 msgstr "當前帳戶已啟用多因素身份驗證。"
 
 
-#: src/views/preference/components/TOTP.vue:95
-msgid "Current account is not enabled 2FA."
+#: src/views/preference/components/TOTP.vue:96
+#, fuzzy
+msgid "Current account is not enabled TOTP."
 msgstr "當前帳戶未啟用多因素身份驗證。"
 msgstr "當前帳戶未啟用多因素身份驗證。"
 
 
 #: src/views/system/Upgrade.vue:167
 #: src/views/system/Upgrade.vue:167
@@ -644,6 +670,12 @@ msgstr "正在下載最新版本"
 msgid "Dry run mode enabled"
 msgid "Dry run mode enabled"
 msgstr "試運轉模式已啟用"
 msgstr "試運轉模式已啟用"
 
 
+#: src/views/preference/components/AddPasskey.vue:107
+msgid ""
+"Due to the security policies of some browsers, you cannot use passkeys on "
+"non-HTTPS websites, except when running on localhost."
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/components/SiteDuplicate.vue:122
 #: src/views/domain/DomainList.vue:140
 #: src/views/domain/DomainList.vue:140
 #: src/views/stream/components/StreamDuplicate.vue:122
 #: src/views/stream/components/StreamDuplicate.vue:122
@@ -712,11 +744,7 @@ msgstr "在 %{node_name} 啟用 %{conf_name} 失敗"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgid "Enable %{conf_name} in %{node_name} successfully"
 msgstr "成功在 %{node_name} 啟用 %{conf_name}"
 msgstr "成功在 %{node_name} 啟用 %{conf_name}"
 
 
-#: src/views/preference/components/TOTP.vue:124
-msgid "Enable 2FA"
-msgstr "啟用多因素身份驗證"
-
-#: src/views/preference/components/TOTP.vue:54
+#: src/views/preference/components/TOTP.vue:55
 msgid "Enable 2FA successfully"
 msgid "Enable 2FA successfully"
 msgstr "啟用多因素身份驗證成功"
 msgstr "啟用多因素身份驗證成功"
 
 
@@ -737,6 +765,11 @@ msgstr "啟用成功"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "啟用 TLS"
 msgstr "啟用 TLS"
 
 
+#: src/views/preference/components/TOTP.vue:125
+#, fuzzy
+msgid "Enable TOTP"
+msgstr "啟用 TLS"
+
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/components/RightSettings.vue:77
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/domain/DomainEdit.vue:175 src/views/domain/DomainList.vue:29
 #: src/views/environment/envColumns.tsx:104
 #: src/views/environment/envColumns.tsx:104
@@ -850,6 +883,12 @@ msgstr "篩選"
 msgid "Finished"
 msgid "Finished"
 msgstr "完成"
 msgstr "完成"
 
 
+#: src/views/preference/components/AddPasskey.vue:77
+msgid ""
+"Follow the instructions in the dialog to complete the passkey registration "
+"process."
+msgstr ""
+
 #: src/views/preference/BasicSettings.vue:43
 #: src/views/preference/BasicSettings.vue:43
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "中國使用者:https://mirror.ghproxy.com/"
 msgstr "中國使用者:https://mirror.ghproxy.com/"
@@ -918,7 +957,7 @@ msgstr "HTTP01"
 msgid "If left blank, the default CA Dir will be used."
 msgid "If left blank, the default CA Dir will be used."
 msgstr "如果留空,將使用默認的 CA Dir。"
 msgstr "如果留空,將使用默認的 CA Dir。"
 
 
-#: src/views/preference/AuthSettings.vue:62
+#: src/views/preference/AuthSettings.vue:68
 msgid ""
 msgid ""
 "If the number of login failed attempts from a ip reach the max attempts in "
 "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."
 "ban threshold minutes, the ip will be banned for a period of time."
@@ -926,12 +965,16 @@ msgstr ""
 "如果來自某個 IP 的登錄失敗次數在禁止閾值分鐘內達到最大嘗試次數,該 IP 將被禁"
 "如果來自某個 IP 的登錄失敗次數在禁止閾值分鐘內達到最大嘗試次數,該 IP 將被禁"
 "止一段時間。"
 "止一段時間。"
 
 
-#: src/views/preference/components/TOTP.vue:110
+#: src/views/preference/components/TOTP.vue:111
 msgid ""
 msgid ""
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "If you lose your mobile phone, you can use the recovery code to reset your "
 "2FA."
 "2FA."
 msgstr "如果您丟失了手機,可以使用恢復碼重置您的多重因素驗證驗證。"
 msgstr "如果您丟失了手機,可以使用恢復碼重置您的多重因素驗證驗證。"
 
 
+#: src/views/preference/components/AddPasskey.vue:76
+msgid "If your browser supports WebAuthn Passkey, a dialog box will appear."
+msgstr ""
+
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 #: src/views/domain/cert/components/AutoCertStepOne.vue:109
 msgid ""
 msgid ""
 "If your domain has CNAME records and you cannot obtain certificates, you "
 "If your domain has CNAME records and you cannot obtain certificates, you "
@@ -946,7 +989,7 @@ msgstr "導入"
 msgid "Import Certificate"
 msgid "Import Certificate"
 msgstr "導入憑證"
 msgstr "導入憑證"
 
 
-#: src/views/other/Login.vue:76
+#: src/views/other/Login.vue:79
 msgid "Incorrect username or password"
 msgid "Incorrect username or password"
 msgstr "使用者名稱或密碼不正確"
 msgstr "使用者名稱或密碼不正確"
 
 
@@ -962,12 +1005,12 @@ msgstr "初始化核心升級程式錯誤"
 msgid "Initialing core upgrader"
 msgid "Initialing core upgrader"
 msgstr "正在初始化核心升級程式"
 msgstr "正在初始化核心升級程式"
 
 
-#: src/views/preference/components/TOTP.vue:157
+#: src/views/preference/components/TOTP.vue:158
 msgid "Input the code from the app:"
 msgid "Input the code from the app:"
 msgstr "請輸入應用程式中的代碼:"
 msgstr "請輸入應用程式中的代碼:"
 
 
-#: src/components/OTP/OTPAuthorization.vue:49
-#: src/views/preference/components/TOTP.vue:170
+#: src/components/2FA/Authorization.vue:87
+#: src/views/preference/components/TOTP.vue:171
 msgid "Input the recovery code:"
 msgid "Input the recovery code:"
 msgstr "輸入恢復碼:"
 msgstr "輸入恢復碼:"
 
 
@@ -987,7 +1030,7 @@ msgstr "間隔"
 msgid "Invalid"
 msgid "Invalid"
 msgstr "無效"
 msgstr "無效"
 
 
-#: src/views/other/Login.vue:86
+#: src/views/other/Login.vue:89
 msgid "Invalid 2FA or recovery code"
 msgid "Invalid 2FA or recovery code"
 msgstr "無效的多重因素驗證或恢復碼"
 msgstr "無效的多重因素驗證或恢復碼"
 
 
@@ -1000,11 +1043,11 @@ msgstr "無效的檔案名"
 msgid "Invalid folder name"
 msgid "Invalid folder name"
 msgstr "無效的資料夾名稱"
 msgstr "無效的資料夾名稱"
 
 
-#: src/components/OTP/useOTPModal.ts:61
+#: src/components/2FA/use2FAModal.ts:65
 msgid "Invalid passcode or recovery code"
 msgid "Invalid passcode or recovery code"
 msgstr "無效的密碼或恢復碼"
 msgstr "無效的密碼或恢復碼"
 
 
-#: src/views/preference/AuthSettings.vue:14
+#: src/views/preference/AuthSettings.vue:15
 msgid "IP"
 msgid "IP"
 msgstr "IP"
 msgstr "IP"
 
 
@@ -1037,6 +1080,11 @@ msgstr "密鑰類型"
 msgid "Last checked at"
 msgid "Last checked at"
 msgstr "上次檢查時間"
 msgstr "上次檢查時間"
 
 
+#: src/views/preference/components/Passkey.vue:102
+#, fuzzy
+msgid "Last used at"
+msgstr "上次檢查時間"
+
 #: src/views/user/userColumns.tsx:25
 #: src/views/user/userColumns.tsx:25
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "留空表示不修改"
 msgstr "留空表示不修改"
@@ -1096,11 +1144,11 @@ msgstr "Locations"
 msgid "Log"
 msgid "Log"
 msgstr "日誌"
 msgstr "日誌"
 
 
-#: src/routes/index.ts:305 src/views/other/Login.vue:207
+#: src/routes/index.ts:305 src/views/other/Login.vue:247
 msgid "Login"
 msgid "Login"
 msgstr "登入"
 msgstr "登入"
 
 
-#: src/views/other/Login.vue:130 src/views/other/Login.vue:63
+#: src/views/other/Login.vue:133 src/views/other/Login.vue:66
 msgid "Login successful"
 msgid "Login successful"
 msgstr "登入成功"
 msgstr "登入成功"
 
 
@@ -1154,7 +1202,7 @@ msgstr "管理使用者"
 msgid "Managed Certificate"
 msgid "Managed Certificate"
 msgstr "受管理的憑證"
 msgstr "受管理的憑證"
 
 
-#: src/views/preference/AuthSettings.vue:76
+#: src/views/preference/AuthSettings.vue:82
 msgid "Max Attempts"
 msgid "Max Attempts"
 msgstr "最大嘗試次數"
 msgstr "最大嘗試次數"
 
 
@@ -1209,6 +1257,7 @@ msgstr "多行指令"
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/DomainList.vue:13
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/domain/ngx_conf/NgxUpstream.vue:175
 #: src/views/environment/envColumns.tsx:9
 #: src/views/environment/envColumns.tsx:9
+#: src/views/preference/components/AddPasskey.vue:81
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/RightSettings.vue:82
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/components/StreamDuplicate.vue:129
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
 #: src/views/stream/StreamList.vue:13 src/views/stream/StreamList.vue:188
@@ -1290,7 +1339,7 @@ msgstr "Nginx 重啟成功"
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/domain/ngx_conf/LocationEditor.vue:70
 #: src/views/notification/Notification.vue:38
 #: src/views/notification/Notification.vue:38
-#: src/views/preference/AuthSettings.vue:98
+#: src/views/preference/AuthSettings.vue:104
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/preference/BasicSettings.vue:101
 #: src/views/stream/StreamList.vue:165
 #: src/views/stream/StreamList.vue:165
 msgid "No"
 msgid "No"
@@ -1389,6 +1438,10 @@ msgstr "線上"
 msgid "OpenAI"
 msgid "OpenAI"
 msgstr "OpenAI"
 msgstr "OpenAI"
 
 
+#: src/components/2FA/Authorization.vue:117 src/views/other/Login.vue:256
+msgid "Or"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:69
 #: src/views/config/components/Rename.vue:69
 msgid "Original name"
 msgid "Original name"
 msgstr "原始名稱"
 msgstr "原始名稱"
@@ -1417,7 +1470,18 @@ msgstr "覆蓋現有檔案"
 msgid "Params"
 msgid "Params"
 msgstr "參數"
 msgstr "參數"
 
 
-#: src/views/other/Login.vue:174 src/views/user/userColumns.tsx:18
+#: src/views/preference/components/Passkey.vue:65
+msgid "Passkey"
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:68
+msgid ""
+"Passkeys are webauthn credentials that validate your identity using touch, "
+"facial recognition, a device password, or a PIN. They can be used as a "
+"password replacement or as a 2FA method."
+msgstr ""
+
+#: src/views/other/Login.vue:208 src/views/user/userColumns.tsx:18
 msgid "Password"
 msgid "Password"
 msgstr "密碼"
 msgstr "密碼"
 
 
@@ -1443,8 +1507,15 @@ msgstr "執行核心升級錯誤"
 msgid "Performing core upgrade"
 msgid "Performing core upgrade"
 msgstr "正在執行核心升級"
 msgstr "正在執行核心升級"
 
 
-#: src/components/OTP/OTPAuthorization.vue:37
-msgid "Please enter the 2FA code:"
+#: src/views/preference/components/AddPasskey.vue:75
+msgid ""
+"Please enter a name for the passkey you wish to create and click the OK "
+"button below."
+msgstr ""
+
+#: src/components/2FA/Authorization.vue:75
+#, fuzzy
+msgid "Please enter the OTP code:"
 msgstr "請輸入多重因素驗證碼:"
 msgstr "請輸入多重因素驗證碼:"
 
 
 #: src/views/certificate/DNSCredential.vue:53
 #: src/views/certificate/DNSCredential.vue:53
@@ -1485,11 +1556,11 @@ msgstr "請輸入名稱,這將作為新設定的檔名!"
 msgid "Please input your E-mail!"
 msgid "Please input your E-mail!"
 msgstr "請輸入您的電子郵件!"
 msgstr "請輸入您的電子郵件!"
 
 
-#: src/views/other/Install.vue:44 src/views/other/Login.vue:44
+#: src/views/other/Install.vue:44 src/views/other/Login.vue:47
 msgid "Please input your password!"
 msgid "Please input your password!"
 msgstr "請輸入您的密碼!"
 msgstr "請輸入您的密碼!"
 
 
-#: src/views/other/Install.vue:38 src/views/other/Login.vue:38
+#: src/views/other/Install.vue:38 src/views/other/Login.vue:41
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "請輸入您的使用者名稱!"
 msgstr "請輸入您的使用者名稱!"
 
 
@@ -1551,16 +1622,16 @@ msgstr "恢復"
 msgid "Recovered Successfully"
 msgid "Recovered Successfully"
 msgstr "恢復成功"
 msgstr "恢復成功"
 
 
-#: src/components/OTP/OTPAuthorization.vue:56
-#: src/views/preference/components/TOTP.vue:177
+#: src/components/2FA/Authorization.vue:94
+#: src/views/preference/components/TOTP.vue:178
 msgid "Recovery"
 msgid "Recovery"
 msgstr "恢復"
 msgstr "恢復"
 
 
-#: src/views/preference/components/TOTP.vue:103
+#: src/views/preference/components/TOTP.vue:104
 msgid "Recovery Code"
 msgid "Recovery Code"
 msgstr "恢復碼"
 msgstr "恢復碼"
 
 
-#: src/views/preference/components/TOTP.vue:112
+#: src/views/preference/components/TOTP.vue:113
 msgid "Recovery Code:"
 msgid "Recovery Code:"
 msgstr "恢復碼:"
 msgstr "恢復碼:"
 
 
@@ -1580,6 +1651,11 @@ msgstr "註冊"
 msgid "Register failed"
 msgid "Register failed"
 msgstr "註冊失敗"
 msgstr "註冊失敗"
 
 
+#: src/views/preference/components/AddPasskey.vue:26
+#, fuzzy
+msgid "Register passkey successfully"
+msgstr "註冊成功"
+
 #: src/views/certificate/ACMEUser.vue:67
 #: src/views/certificate/ACMEUser.vue:67
 msgid "Register successfully"
 msgid "Register successfully"
 msgstr "註冊成功"
 msgstr "註冊成功"
@@ -1613,11 +1689,12 @@ msgstr "重新載入中"
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr "正在重新載入 Nginx"
 msgstr "正在重新載入 Nginx"
 
 
-#: src/views/preference/AuthSettings.vue:103
+#: src/views/preference/AuthSettings.vue:109
 msgid "Remove"
 msgid "Remove"
 msgstr "移除"
 msgstr "移除"
 
 
-#: src/views/preference/AuthSettings.vue:47
+#: src/views/preference/AuthSettings.vue:48
+#: src/views/preference/components/Passkey.vue:50
 msgid "Remove successfully"
 msgid "Remove successfully"
 msgstr "移除成功"
 msgstr "移除成功"
 
 
@@ -1687,7 +1764,7 @@ msgstr "請求參數錯誤"
 msgid "Reset"
 msgid "Reset"
 msgstr "重設"
 msgstr "重設"
 
 
-#: src/views/preference/components/TOTP.vue:132
+#: src/views/preference/components/TOTP.vue:133
 msgid "Reset 2FA"
 msgid "Reset 2FA"
 msgstr "重置多重因素驗證"
 msgstr "重置多重因素驗證"
 
 
@@ -1711,6 +1788,7 @@ msgstr "執行中"
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/certificate/CertificateEditor.vue:254
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/config/ConfigEditor.vue:222 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
+#: src/views/preference/components/Passkey.vue:136
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 #: src/views/preference/Preference.vue:151 src/views/stream/StreamEdit.vue:252
 msgid "Save"
 msgid "Save"
 msgstr "儲存"
 msgstr "儲存"
@@ -1738,7 +1816,7 @@ msgstr "儲存成功"
 msgid "Saved successfully"
 msgid "Saved successfully"
 msgstr "儲存成功"
 msgstr "儲存成功"
 
 
-#: src/views/preference/components/TOTP.vue:93
+#: src/views/preference/components/TOTP.vue:94
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgid "Scan the QR code with your mobile phone to add the account to the app."
 msgstr "用手機掃描二維碼將賬戶添加到應用程序中。"
 msgstr "用手機掃描二維碼將賬戶添加到應用程序中。"
 
 
@@ -1746,7 +1824,7 @@ msgstr "用手機掃描二維碼將賬戶添加到應用程序中。"
 msgid "SDK"
 msgid "SDK"
 msgstr "SDK"
 msgstr "SDK"
 
 
-#: src/views/preference/components/TOTP.vue:149
+#: src/views/preference/components/TOTP.vue:150
 msgid "Secret has been copied"
 msgid "Secret has been copied"
 msgstr ""
 msgstr ""
 
 
@@ -1771,9 +1849,12 @@ msgstr "傳送"
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/config/ConfigEditor.vue:108 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
-#: src/views/preference/AuthSettings.vue:49
-#: src/views/preference/components/TOTP.vue:44
-#: src/views/preference/components/TOTP.vue:57
+#: src/views/preference/AuthSettings.vue:50
+#: src/views/preference/components/Passkey.vue:26
+#: src/views/preference/components/Passkey.vue:43
+#: src/views/preference/components/Passkey.vue:56
+#: src/views/preference/components/TOTP.vue:45
+#: src/views/preference/components/TOTP.vue:58
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/preference/Preference.vue:83 src/views/stream/StreamList.vue:113
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42
 msgid "Server error"
 msgid "Server error"
@@ -1817,6 +1898,10 @@ msgstr "使用 HTTP01 挑戰提供者"
 msgid "Show"
 msgid "Show"
 msgstr "顯示"
 msgstr "顯示"
 
 
+#: src/views/other/Login.vue:265
+msgid "Sign in with a passkey"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:51
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "單一指令"
 msgstr "單一指令"
@@ -1845,7 +1930,7 @@ msgstr "SSL 憑證金鑰路徑"
 msgid "SSL Certificate Path"
 msgid "SSL Certificate Path"
 msgstr "SSL 憑證路徑"
 msgstr "SSL 憑證路徑"
 
 
-#: src/views/other/Login.vue:189
+#: src/views/other/Login.vue:223
 msgid "SSO Login"
 msgid "SSO Login"
 msgstr "SSO 登錄"
 msgstr "SSO 登錄"
 
 
@@ -2003,7 +2088,7 @@ msgstr "路徑存在,但檔案不是憑證"
 msgid "The path exists, but the file is not a private key"
 msgid "The path exists, but the file is not a private key"
 msgstr "路徑存在,但檔案不是金鑰"
 msgstr "路徑存在,但檔案不是金鑰"
 
 
-#: src/views/preference/components/TOTP.vue:111
+#: src/views/preference/components/TOTP.vue:112
 msgid ""
 msgid ""
 "The recovery code is only displayed once, please save it in a safe place."
 "The recovery code is only displayed once, please save it in a safe place."
 msgstr "恢復碼僅顯示一次,請將其保存在安全的地方。"
 msgstr "恢復碼僅顯示一次,請將其保存在安全的地方。"
@@ -2066,7 +2151,8 @@ msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr "這將在 %{nodeNames} 上升級或重新安裝 Nginx UI 到 %{version}。"
 msgstr "這將在 %{nodeNames} 上升級或重新安裝 Nginx UI 到 %{version}。"
 
 
-#: src/views/preference/AuthSettings.vue:61
+#: src/views/preference/AuthSettings.vue:67
+#: src/views/preference/components/AddPasskey.vue:71
 #: src/views/preference/LogrotateSettings.vue:11
 #: src/views/preference/LogrotateSettings.vue:11
 msgid "Tips"
 msgid "Tips"
 msgstr "提示"
 msgstr "提示"
@@ -2075,13 +2161,20 @@ msgstr "提示"
 msgid "Title"
 msgid "Title"
 msgstr "標題"
 msgstr "標題"
 
 
-#: src/views/preference/components/TOTP.vue:92
+#: src/views/preference/components/TOTP.vue:93
 msgid ""
 msgid ""
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "To enable it, you need to install the Google or Microsoft Authenticator app "
 "on your mobile phone."
 "on your mobile phone."
 msgstr ""
 msgstr ""
 "要啟用它,您需要在手機上安裝 Google 或 Microsoft Authenticator 應用程序。"
 "要啟用它,您需要在手機上安裝 Google 或 Microsoft Authenticator 應用程序。"
 
 
+#: src/views/preference/components/AddPasskey.vue:95
+msgid ""
+"To ensure security, Webauthn configuration cannot be added through the UI. "
+"Please manually configure the following in the app.ini configuration file "
+"and restart Nginx UI."
+msgstr ""
+
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44
 msgid ""
 msgid ""
 "To make sure the certification auto-renewal can work normally, we need to "
 "To make sure the certification auto-renewal can work normally, we need to "
@@ -2096,11 +2189,15 @@ msgstr ""
 msgid "Token is not valid"
 msgid "Token is not valid"
 msgstr "令牌無效"
 msgstr "令牌無效"
 
 
-#: src/views/other/Login.vue:79
+#: src/views/other/Login.vue:82
 msgid "Too many login failed attempts, please try again later"
 msgid "Too many login failed attempts, please try again later"
 msgstr "登錄失敗次數過多,請稍後再試"
 msgstr "登錄失敗次數過多,請稍後再試"
 
 
 #: src/views/preference/components/TOTP.vue:91
 #: src/views/preference/components/TOTP.vue:91
+msgid "TOTP"
+msgstr ""
+
+#: src/views/preference/components/TOTP.vue:92
 msgid ""
 msgid ""
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "TOTP is a two-factor authentication method that uses a time-based one-time "
 "password algorithm."
 "password algorithm."
@@ -2110,7 +2207,7 @@ msgstr "TOTP 是一種使用基於時間的一次性密碼算法的多重因素
 msgid "Trash"
 msgid "Trash"
 msgstr "垃圾桶"
 msgstr "垃圾桶"
 
 
-#: src/components/OTP/useOTPModal.ts:67
+#: src/components/2FA/use2FAModal.ts:71
 msgid "Two-factor authentication required"
 msgid "Two-factor authentication required"
 msgstr "需要多重因素驗證"
 msgstr "需要多重因素驗證"
 
 
@@ -2120,6 +2217,11 @@ msgstr "需要多重因素驗證"
 msgid "Type"
 msgid "Type"
 msgstr "類型"
 msgstr "類型"
 
 
+#: src/views/preference/components/Passkey.vue:41
+#, fuzzy
+msgid "Update successfully"
+msgstr "更新成功"
+
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/certificate/DNSCredential.vue:27
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
 #: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:275
@@ -2163,11 +2265,11 @@ msgstr "運作時間:"
 msgid "URL"
 msgid "URL"
 msgstr "URL"
 msgstr "URL"
 
 
-#: src/components/OTP/OTPAuthorization.vue:69
+#: src/components/2FA/Authorization.vue:107
 msgid "Use OTP"
 msgid "Use OTP"
 msgstr "使用一次性密碼"
 msgstr "使用一次性密碼"
 
 
-#: src/components/OTP/OTPAuthorization.vue:65
+#: src/components/2FA/Authorization.vue:103
 msgid "Use recovery code"
 msgid "Use recovery code"
 msgstr "使用恢復碼"
 msgstr "使用恢復碼"
 
 
@@ -2175,11 +2277,11 @@ msgstr "使用恢復碼"
 msgid "User"
 msgid "User"
 msgstr "使用者名稱"
 msgstr "使用者名稱"
 
 
-#: src/views/other/Login.vue:82
+#: src/views/other/Login.vue:85
 msgid "User is banned"
 msgid "User is banned"
 msgstr "用戶被禁止"
 msgstr "用戶被禁止"
 
 
-#: src/views/other/Login.vue:164 src/views/user/userColumns.tsx:9
+#: src/views/other/Login.vue:198 src/views/user/userColumns.tsx:9
 msgid "Username"
 msgid "Username"
 msgstr "使用者名稱"
 msgstr "使用者名稱"
 
 
@@ -2217,6 +2319,7 @@ msgstr "查看模式"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/domain/DomainAdd.vue:112
 #: src/views/notification/notificationColumns.tsx:19
 #: src/views/notification/notificationColumns.tsx:19
+#: src/views/preference/components/AddPasskey.vue:88
 msgid "Warning"
 msgid "Warning"
 msgstr "警告"
 msgstr "警告"
 
 
@@ -2249,7 +2352,7 @@ msgstr "將憑證寫入磁碟"
 
 
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
 #: src/views/domain/ngx_conf/LocationEditor.vue:69
-#: src/views/preference/AuthSettings.vue:97
+#: src/views/preference/AuthSettings.vue:103
 #: src/views/preference/BasicSettings.vue:100
 #: src/views/preference/BasicSettings.vue:100
 msgid "Yes"
 msgid "Yes"
 msgstr "是的"
 msgstr "是的"
@@ -2262,6 +2365,19 @@ msgstr "您正在使用最新版本"
 msgid "You can check Nginx UI upgrade at this page."
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "您可以在此頁面檢查 Nginx UI 的升級。"
 msgstr "您可以在此頁面檢查 Nginx UI 的升級。"
 
 
+#: src/views/preference/components/AddPasskey.vue:93
+msgid ""
+"You have not configured the settings of Webauthn, so you cannot add a "
+"passkey."
+msgstr ""
+
+#: src/views/preference/components/Passkey.vue:81
+msgid "Your passkeys"
+msgstr ""
+
+#~ msgid "Enable 2FA"
+#~ msgstr "啟用多因素身份驗證"
+
 #, fuzzy
 #, fuzzy
 #~ msgid "Rename "
 #~ msgid "Rename "
 #~ msgstr "使用者名稱"
 #~ msgstr "使用者名稱"

+ 2 - 2
app/src/layouts/SideBar.vue

@@ -81,8 +81,8 @@ const visible: ComputedRef<sidebar[]> = computed(() => {
     <Logo />
     <Logo />
 
 
     <AMenu
     <AMenu
-      v-model:openKeys="openKeys"
-      v-model:selectedKeys="selectedKey"
+      v-model:open-keys="openKeys"
+      v-model:selected-keys="selectedKey"
       mode="inline"
       mode="inline"
     >
     >
       <EnvIndicator />
       <EnvIndicator />

+ 2 - 2
app/src/lib/http/index.ts

@@ -7,7 +7,7 @@ import { useSettingsStore, useUserStore } from '@/pinia'
 import 'nprogress/nprogress.css'
 import 'nprogress/nprogress.css'
 
 
 import router from '@/routes'
 import router from '@/routes'
-import useOTPModal from '@/components/OTP/useOTPModal'
+import use2FAModal from '@/components/2FA/use2FAModal'
 
 
 const user = useUserStore()
 const user = useUserStore()
 const settings = useSettingsStore()
 const settings = useSettingsStore()
@@ -61,7 +61,7 @@ instance.interceptors.response.use(
   async error => {
   async error => {
     NProgress.done()
     NProgress.done()
 
 
-    const otpModal = useOTPModal()
+    const otpModal = use2FAModal()
     const cookies = useCookies(['nginx-ui-2fa'])
     const cookies = useCookies(['nginx-ui-2fa'])
     switch (error.response.status) {
     switch (error.response.status) {
       case 401:
       case 401:

+ 12 - 1
app/src/pinia/moudule/user.ts

@@ -5,18 +5,29 @@ export const useUserStore = defineStore('user', {
     token: '',
     token: '',
     unreadCount: 0,
     unreadCount: 0,
     secureSessionId: '',
     secureSessionId: '',
+    passkeyRawId: '',
   }),
   }),
   getters: {
   getters: {
-    is_login(state): boolean {
+    isLogin(state): boolean {
       return !!state.token
       return !!state.token
     },
     },
+    passkeyLoginAvailable(state): boolean {
+      return !!state.passkeyRawId
+    },
   },
   },
   actions: {
   actions: {
+    passkeyLogin(rawId: string, token: string) {
+      this.passkeyRawId = rawId
+      this.login(token)
+    },
     login(token: string) {
     login(token: string) {
       this.token = token
       this.token = token
     },
     },
     logout() {
     logout() {
       this.token = ''
       this.token = ''
+      this.passkeyRawId = ''
+      this.secureSessionId = ''
+      this.unreadCount = 0
     },
     },
   },
   },
   persist: true,
   persist: true,

+ 1 - 2
app/src/routes/index.ts

@@ -325,9 +325,8 @@ router.beforeEach((to, _, next) => {
   NProgress.start()
   NProgress.start()
 
 
   const user = useUserStore()
   const user = useUserStore()
-  const { is_login } = user
 
 
-  if (to.meta.noAuth || is_login)
+  if (to.meta.noAuth || user.isLogin)
     next()
     next()
   else
   else
     next({ path: '/login', query: { next: to.fullPath } })
     next({ path: '/login', query: { next: to.fullPath } })

+ 1 - 1
app/src/views/config/ConfigEditor.vue

@@ -233,7 +233,7 @@ function goBack() {
     >
     >
       <ACard class="col-right">
       <ACard class="col-right">
         <ACollapse
         <ACollapse
-          v-model:activeKey="activeKey"
+          v-model:active-key="activeKey"
           ghost
           ghost
         >
         >
           <ACollapsePanel
           <ACollapsePanel

+ 2 - 2
app/src/views/config/components/Mkdir.vue

@@ -2,7 +2,7 @@
 
 
 import { message } from 'ant-design-vue'
 import { message } from 'ant-design-vue'
 import config from '@/api/config'
 import config from '@/api/config'
-import useOTPModal from '@/components/OTP/useOTPModal'
+import use2FAModal from '@/components/2FA/use2FAModal'
 
 
 const emit = defineEmits(['created'])
 const emit = defineEmits(['created'])
 const visible = ref(false)
 const visible = ref(false)
@@ -25,7 +25,7 @@ defineExpose({
 
 
 function ok() {
 function ok() {
   refForm.value.validate().then(() => {
   refForm.value.validate().then(() => {
-    const otpModal = useOTPModal()
+    const otpModal = use2FAModal()
 
 
     otpModal.open().then(() => {
     otpModal.open().then(() => {
       config.mkdir(data.value.basePath, data.value.name).then(() => {
       config.mkdir(data.value.basePath, data.value.name).then(() => {

+ 2 - 2
app/src/views/config/components/Rename.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { message } from 'ant-design-vue'
 import { message } from 'ant-design-vue'
 import config from '@/api/config'
 import config from '@/api/config'
-import useOTPModal from '@/components/OTP/useOTPModal'
+import use2FAModal from '@/components/2FA/use2FAModal'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 
 
 const emit = defineEmits(['renamed'])
 const emit = defineEmits(['renamed'])
@@ -33,7 +33,7 @@ function ok() {
   refForm.value.validate().then(() => {
   refForm.value.validate().then(() => {
     const { basePath, orig_name, new_name, sync_node_ids } = data.value
     const { basePath, orig_name, new_name, sync_node_ids } = data.value
 
 
-    const otpModal = useOTPModal()
+    const otpModal = use2FAModal()
 
 
     otpModal.open().then(() => {
     otpModal.open().then(() => {
       config.rename(basePath, orig_name, new_name, sync_node_ids).then(() => {
       config.rename(basePath, orig_name, new_name, sync_node_ids).then(() => {

+ 1 - 1
app/src/views/domain/components/RightSettings.vue

@@ -67,7 +67,7 @@ function on_change_enabled(checked: CheckedType) {
   >
   >
     <ContextHolder />
     <ContextHolder />
     <ACollapse
     <ACollapse
-      v-model:activeKey="active_key"
+      v-model:active-key="active_key"
       ghost
       ghost
     >
     >
       <ACollapsePanel
       <ACollapsePanel

+ 1 - 1
app/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -183,7 +183,7 @@ const activeKey = ref(['3'])
     </AFormItem>
     </AFormItem>
 
 
     <ACollapse
     <ACollapse
-      v-model:activeKey="activeKey"
+      v-model:active-key="activeKey"
       ghost
       ghost
     >
     >
       <ACollapsePanel
       <ACollapsePanel

+ 1 - 1
app/src/views/domain/ngx_conf/NgxServer.vue

@@ -95,7 +95,7 @@ provide('ngx_directives', ngx_directives)
 <template>
 <template>
   <div>
   <div>
     <ContextHolder />
     <ContextHolder />
-    <ATabs v-model:activeKey="current_server_index">
+    <ATabs v-model:active-key="current_server_index">
       <ATabPane
       <ATabPane
         v-for="(v, k) in ngx_config.servers"
         v-for="(v, k) in ngx_config.servers"
         :key="k"
         :key="k"

+ 1 - 1
app/src/views/domain/ngx_conf/NgxUpstream.vue

@@ -107,7 +107,7 @@ watch(ngx_directives, () => {
     <ContextHolder />
     <ContextHolder />
     <ATabs
     <ATabs
       v-if="ngx_config.upstreams && ngx_config.upstreams.length > 0"
       v-if="ngx_config.upstreams && ngx_config.upstreams.length > 0"
-      v-model:activeKey="current_upstream_index"
+      v-model:active-key="current_upstream_index"
     >
     >
       <ATabPane
       <ATabPane
         v-for="(v, k) in ngx_config.upstreams"
         v-for="(v, k) in ngx_config.upstreams"

+ 65 - 6
app/src/views/other/Login.vue

@@ -1,14 +1,16 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
+import { KeyOutlined, LockOutlined, UserOutlined } from '@ant-design/icons-vue'
 import { Form, message } from 'ant-design-vue'
 import { Form, message } from 'ant-design-vue'
 import { useCookies } from '@vueuse/integrations/useCookies'
 import { useCookies } from '@vueuse/integrations/useCookies'
+import { startAuthentication } from '@simplewebauthn/browser'
 import { useUserStore } from '@/pinia'
 import { useUserStore } from '@/pinia'
 import auth from '@/api/auth'
 import auth from '@/api/auth'
 import install from '@/api/install'
 import install from '@/api/install'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
-import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
+import Authorization from '@/components/2FA/Authorization.vue'
 import gettext from '@/gettext'
 import gettext from '@/gettext'
+import passkey from '@/api/passkey'
 
 
 const thisYear = new Date().getFullYear()
 const thisYear = new Date().getFullYear()
 
 
@@ -25,6 +27,7 @@ const enabled2FA = ref(false)
 const refOTP = ref()
 const refOTP = ref()
 const passcode = ref('')
 const passcode = ref('')
 const recoveryCode = ref('')
 const recoveryCode = ref('')
+const passkeyConfigStatus = ref(false)
 
 
 const modelRef = reactive({
 const modelRef = reactive({
   username: '',
   username: '',
@@ -48,7 +51,7 @@ const rulesRef = reactive({
 
 
 const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
 const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
 const userStore = useUserStore()
 const userStore = useUserStore()
-const { login } = userStore
+const { login, passkeyLogin } = userStore
 const { secureSessionId } = storeToRefs(userStore)
 const { secureSessionId } = storeToRefs(userStore)
 
 
 const onSubmit = () => {
 const onSubmit = () => {
@@ -96,7 +99,7 @@ const onSubmit = () => {
 
 
 const user = useUserStore()
 const user = useUserStore()
 
 
-if (user.is_login) {
+if (user.isLogin) {
   const next = (route.query?.next || '').toString() || '/dashboard'
   const next = (route.query?.next || '').toString() || '/dashboard'
 
 
   router.push(next)
   router.push(next)
@@ -146,6 +149,37 @@ function handleOTPSubmit(code: string, recovery: string) {
     onSubmit()
     onSubmit()
   })
   })
 }
 }
+
+passkey.get_config_status().then(r => {
+  passkeyConfigStatus.value = r.status
+})
+
+const passkeyLoginLoading = ref(false)
+async function handlePasskeyLogin() {
+  passkeyLoginLoading.value = true
+  try {
+    const begin = await auth.begin_passkey_login()
+    const asseResp = await startAuthentication(begin.options.publicKey)
+
+    const r = await auth.finish_passkey_login({
+      session_id: begin.session_id,
+      options: asseResp,
+    })
+
+    if (r.token) {
+      const next = (route.query?.next || '').toString() || '/'
+
+      passkeyLogin(asseResp.rawId, r.token)
+
+      await router.push(next)
+    }
+  }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  catch (e: any) {
+    message.error($gettext(e.message ?? 'Server error'))
+  }
+  passkeyLoginLoading.value = false
+}
 </script>
 </script>
 
 
 <template>
 <template>
@@ -190,9 +224,14 @@ function handleOTPSubmit(code: string, recovery: string) {
               </AButton>
               </AButton>
             </template>
             </template>
             <div v-else>
             <div v-else>
-              <OTPAuthorization
+              <Authorization
                 ref="refOTP"
                 ref="refOTP"
-                @on-submit="handleOTPSubmit"
+                :two-f-a-status="{
+                  enabled: true,
+                  otp_status: true,
+                  passkey_status: true,
+                }"
+                @submit-o-t-p="handleOTPSubmit"
               />
               />
             </div>
             </div>
 
 
@@ -202,10 +241,30 @@ function handleOTPSubmit(code: string, recovery: string) {
                 block
                 block
                 html-type="submit"
                 html-type="submit"
                 :loading="loading"
                 :loading="loading"
+                class="mb-2"
                 @click="onSubmit"
                 @click="onSubmit"
               >
               >
                 {{ $gettext('Login') }}
                 {{ $gettext('Login') }}
               </AButton>
               </AButton>
+
+              <div
+                v-if="passkeyConfigStatus"
+                class="flex flex-col justify-center"
+              >
+                <ADivider>
+                  <div class="text-sm font-normal opacity-75">
+                    {{ $gettext('Or') }}
+                  </div>
+                </ADivider>
+
+                <AButton
+                  :loading="passkeyLoginLoading"
+                  @click="handlePasskeyLogin"
+                >
+                  <KeyOutlined />
+                  {{ $gettext('Sign in with a passkey') }}
+                </AButton>
+              </div>
             </AFormItem>
             </AFormItem>
           </AForm>
           </AForm>
           <div class="footer">
           <div class="footer">

+ 6 - 0
app/src/views/preference/AuthSettings.vue

@@ -2,6 +2,7 @@
 import { message } from 'ant-design-vue'
 import { message } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import type { Ref } from 'vue'
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
+import PasskeyRegistration from './components/Passkey.vue'
 import type { BannedIP } from '@/api/settings'
 import type { BannedIP } from '@/api/settings'
 import setting from '@/api/settings'
 import setting from '@/api/settings'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
@@ -54,8 +55,13 @@ function removeBannedIP(ip: string) {
 <template>
 <template>
   <div class="flex justify-center">
   <div class="flex justify-center">
     <div>
     <div>
+      <h2>{{ $gettext('2FA Settings') }}</h2>
+      <PasskeyRegistration class="mb-4" />
       <TOTP class="mb-4" />
       <TOTP class="mb-4" />
 
 
+      <h2>
+        {{ $gettext('Authentication Settings') }}
+      </h2>
       <AAlert
       <AAlert
         class="mb-4"
         class="mb-4"
         :message="$gettext('Tips')"
         :message="$gettext('Tips')"

+ 3 - 3
app/src/views/preference/Preference.vue

@@ -11,7 +11,7 @@ import type { Settings } from '@/views/preference/typedef'
 import LogrotateSettings from '@/views/preference/LogrotateSettings.vue'
 import LogrotateSettings from '@/views/preference/LogrotateSettings.vue'
 import { useSettingsStore } from '@/pinia'
 import { useSettingsStore } from '@/pinia'
 import AuthSettings from '@/views/preference/AuthSettings.vue'
 import AuthSettings from '@/views/preference/AuthSettings.vue'
-import useOTPModal from '@/components/OTP/useOTPModal'
+import use2FAModal from '@/components/2FA/use2FAModal'
 
 
 const data = ref<Settings>({
 const data = ref<Settings>({
   server: {
   server: {
@@ -68,7 +68,7 @@ async function save() {
   // fix type
   // fix type
   data.value.server.http_challenge_port = data.value.server.http_challenge_port.toString()
   data.value.server.http_challenge_port = data.value.server.http_challenge_port.toString()
 
 
-  const otpModal = useOTPModal()
+  const otpModal = use2FAModal()
 
 
   otpModal.open().then(() => {
   otpModal.open().then(() => {
     settings.save(data.value).then(r => {
     settings.save(data.value).then(r => {
@@ -110,7 +110,7 @@ onMounted(() => {
 <template>
 <template>
   <ACard :title="$gettext('Preference')">
   <ACard :title="$gettext('Preference')">
     <div class="preference-container">
     <div class="preference-container">
-      <ATabs v-model:activeKey="activeKey">
+      <ATabs v-model:active-key="activeKey">
         <ATabPane
         <ATabPane
           key="basic"
           key="basic"
           :tab="$gettext('Basic')"
           :tab="$gettext('Basic')"

+ 118 - 0
app/src/views/preference/components/AddPasskey.vue

@@ -0,0 +1,118 @@
+<script setup lang="ts">
+import { startRegistration } from '@simplewebauthn/browser'
+import { message } from 'ant-design-vue'
+import passkey from '@/api/passkey'
+import { useUserStore } from '@/pinia'
+
+const emit = defineEmits(['created'])
+
+const user = useUserStore()
+const passkeyName = ref('')
+const addPasskeyModelOpen = ref(false)
+const passkeyEnabled = ref(false)
+
+const regLoading = ref(false)
+async function registerPasskey() {
+  regLoading.value = true
+  try {
+    const options = await passkey.begin_registration()
+
+    const attestationResponse = await startRegistration(options.publicKey)
+
+    await passkey.finish_registration(attestationResponse, passkeyName.value)
+
+    emit('created')
+
+    message.success($gettext('Register passkey successfully'))
+    addPasskeyModelOpen.value = false
+
+    user.passkeyRawId = attestationResponse.rawId
+  }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  catch (e: any) {
+    message.error($gettext(e.message ?? 'Server error'))
+  }
+  regLoading.value = false
+}
+
+function addPasskey() {
+  addPasskeyModelOpen.value = true
+  passkeyName.value = ''
+}
+
+passkey.get_config_status().then(r => {
+  passkeyEnabled.value = r.status
+})
+</script>
+
+<template>
+  <div>
+    <AButton @click="addPasskey">
+      {{ $gettext('Add a passkey') }}
+    </AButton>
+    <AModal
+      v-model:open="addPasskeyModelOpen"
+      :title="$gettext('Add a passkey')"
+      centered
+      :mask="false"
+      :mask-closable="!passkeyEnabled"
+      :closable="!passkeyEnabled"
+      :footer="passkeyEnabled ? undefined : false"
+      :confirm-loading="regLoading"
+      @ok="registerPasskey"
+    >
+      <AForm
+        v-if="passkeyEnabled"
+        layout="vertical"
+      >
+        <div>
+          <AAlert
+            class="mb-4"
+            :message="$gettext('Tips')"
+            type="info"
+          >
+            <template #description>
+              <p>{{ $gettext('Please enter a name for the passkey you wish to create and click the OK button below.') }}</p>
+              <p>{{ $gettext('If your browser supports WebAuthn Passkey, a dialog box will appear.') }}</p>
+              <p>{{ $gettext('Follow the instructions in the dialog to complete the passkey registration process.') }}</p>
+            </template>
+          </AAlert>
+        </div>
+        <AFormItem :label="$gettext('Name')">
+          <AInput v-model:value="passkeyName" />
+        </AFormItem>
+      </AForm>
+      <div v-else>
+        <AAlert
+          class="mb-4"
+          :message="$gettext('Warning')"
+          type="warning"
+          show-icon
+        >
+          <template #description>
+            <p>{{ $gettext('You have not configured the settings of Webauthn, so you cannot add a passkey.') }}</p>
+            <p>
+              {{ $gettext('To ensure security, Webauthn configuration cannot be added through the UI. '
+                + 'Please manually configure the following in the app.ini configuration file and restart Nginx UI.') }}
+            </p>
+            <pre>[webauthn]
+# This is the display name
+RPDisplayName = Nginx UI
+# The domain name of Nginx UI
+RPID          = localhost
+# The list of origin addresses
+RPOrigins     = http://localhost:3002</pre>
+            <p>{{ $gettext('Afterwards, refresh this page and click add passkey again.') }}</p>
+            <p>
+              {{ $gettext(`Due to the security policies of some browsers, you cannot use passkeys on non-HTTPS websites, except when running on localhost.`) }}
+            </p>
+          </template>
+        </AAlert>
+      </div>
+    </AModal>
+  </div>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 159 - 0
app/src/views/preference/components/Passkey.vue

@@ -0,0 +1,159 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue'
+import { DeleteOutlined, EditOutlined, KeyOutlined } from '@ant-design/icons-vue'
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import { formatDateTime } from '@/lib/helper'
+import type { Passkey } from '@/api/passkey'
+import passkey from '@/api/passkey'
+import ReactiveFromNow from '@/components/ReactiveFromNow/ReactiveFromNow.vue'
+import { useUserStore } from '@/pinia'
+import AddPasskey from '@/views/preference/components/AddPasskey.vue'
+
+dayjs.extend(relativeTime)
+
+const user = useUserStore()
+
+const getListLoading = ref(true)
+const data = ref([]) as Ref<Passkey[]>
+const passkeyName = ref('')
+
+function getList() {
+  getListLoading.value = true
+  passkey.get_list().then(r => {
+    data.value = r
+  }).catch((e: { message?: string }) => {
+    message.error(e?.message ?? $gettext('Server error'))
+  }).finally(() => {
+    getListLoading.value = false
+  })
+}
+
+onMounted(() => {
+  getList()
+})
+
+const modifyIdx = ref(-1)
+function update(id: number, record: Passkey) {
+  passkey.update(id, record).then(() => {
+    getList()
+    modifyIdx.value = -1
+    message.success($gettext('Update successfully'))
+  }).catch((e: { message?: string }) => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
+}
+
+function remove(item: Passkey) {
+  passkey.remove(item.id).then(() => {
+    getList()
+    message.success($gettext('Remove successfully'))
+
+    // if current passkey is removed, clear it from user store
+    if (user.passkeyLoginAvailable && user.passkeyRawId === item.raw_id)
+      user.passkeyRawId = ''
+  }).catch((e: { message?: string }) => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
+}
+</script>
+
+<template>
+  <div>
+    <div>
+      <h3>
+        {{ $gettext('Passkey') }}
+      </h3>
+      <p>
+        {{ $gettext('Passkeys are webauthn credentials that validate your identity using touch, '
+          + 'facial recognition, a device password, or a PIN. '
+          + 'They can be used as a password replacement or as a 2FA method.') }}
+      </p>
+    </div>
+    <AList
+      class="mt-4"
+      bordered
+      :data-source="data"
+    >
+      <template #header>
+        <div class="flex items-center justify-between">
+          <div class="font-bold">
+            {{ $gettext('Your passkeys') }}
+          </div>
+          <AddPasskey @created="() => getList()" />
+        </div>
+      </template>
+      <template #renderItem="{ item, index }">
+        <AListItem>
+          <AListItemMeta>
+            <template #title>
+              <div class="flex gap-2">
+                <KeyOutlined />
+                <div v-if="index !== modifyIdx">
+                  {{ item.name }}
+                </div>
+                <div v-else>
+                  <AInput v-model:value="passkeyName" />
+                </div>
+              </div>
+            </template>
+            <template #description>
+              {{ $gettext('Created at') }}: {{ formatDateTime(item.created_at) }} · {{
+                $gettext('Last used at') }}: <ReactiveFromNow :time="item.last_used_at" />
+            </template>
+          </AListItemMeta>
+          <template #extra>
+            <div v-if="modifyIdx !== index">
+              <AButton
+                type="link"
+                size="small"
+                @click="() => {
+                  modifyIdx = index
+                  passkeyName = item.name
+                }"
+              >
+                <EditOutlined />
+              </AButton>
+
+              <APopconfirm
+                :title="$gettext('Are you sure to delete this passkey immediately?')"
+                @confirm="() => remove(item)"
+              >
+                <AButton
+                  type="link"
+                  danger
+                  size="small"
+                >
+                  <DeleteOutlined />
+                </AButton>
+              </APopconfirm>
+            </div>
+            <div v-else>
+              <AButton
+                size="small"
+                @click="() => update(item.id, { ...item, name: passkeyName })"
+              >
+                {{ $gettext('Save') }}
+              </AButton>
+
+              <AButton
+                type="link"
+                size="small"
+                @click="() => {
+                  modifyIdx = -1
+                  passkeyName = item.name
+                }"
+              >
+                {{ $gettext('Cancel') }}
+              </AButton>
+            </div>
+          </template>
+        </AListItem>
+      </template>
+    </AList>
+  </div>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 8 - 7
app/src/views/preference/components/TOTP.vue

@@ -4,7 +4,8 @@ import { CheckCircleOutlined } from '@ant-design/icons-vue'
 import { UseClipboard } from '@vueuse/components'
 import { UseClipboard } from '@vueuse/components'
 import otp from '@/api/otp'
 import otp from '@/api/otp'
 import OTPInput from '@/components/OTPInput/OTPInput.vue'
 import OTPInput from '@/components/OTPInput/OTPInput.vue'
-import { $gettext } from '@/gettext'
+
+import twoFA from '@/api/2fa'
 
 
 const status = ref(false)
 const status = ref(false)
 const enrolling = ref(false)
 const enrolling = ref(false)
@@ -59,8 +60,8 @@ function enroll(code: string) {
 }
 }
 
 
 function get2FAStatus() {
 function get2FAStatus() {
-  otp.status().then(r => {
-    status.value = r.status
+  twoFA.status().then(r => {
+    status.value = r.otp_status
   })
   })
 }
 }
 
 
@@ -87,15 +88,15 @@ function reset2FA() {
 
 
 <template>
 <template>
   <div>
   <div>
-    <h3>{{ $gettext('2FA Settings') }}</h3>
+    <h3>{{ $gettext('TOTP') }}</h3>
     <p>{{ $gettext('TOTP is a two-factor authentication method that uses a time-based one-time password algorithm.') }}</p>
     <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('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>{{ $gettext('Scan the QR code with your mobile phone to add the account to the app.') }}</p>
     <p v-if="!status">
     <p v-if="!status">
-      {{ $gettext('Current account is not enabled 2FA.') }}
+      {{ $gettext('Current account is not enabled TOTP.') }}
     </p>
     </p>
     <div v-else>
     <div v-else>
-      <p><CheckCircleOutlined class="mr-2 text-green-600" />{{ $gettext('Current account is enabled 2FA.') }}</p>
+      <p><CheckCircleOutlined class="mr-2 text-green-600" />{{ $gettext('Current account is enabled TOTP.') }}</p>
     </div>
     </div>
 
 
     <AAlert
     <AAlert
@@ -121,7 +122,7 @@ function reset2FA() {
       ghost
       ghost
       @click="clickEnable2FA"
       @click="clickEnable2FA"
     >
     >
-      {{ $gettext('Enable 2FA') }}
+      {{ $gettext('Enable TOTP') }}
     </AButton>
     </AButton>
     <AButton
     <AButton
       v-if="status && !resetting"
       v-if="status && !resetting"

+ 4 - 4
app/src/views/pty/Terminal.vue

@@ -4,8 +4,8 @@ import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
 import { FitAddon } from '@xterm/addon-fit'
 import _ from 'lodash'
 import _ from 'lodash'
 import ws from '@/lib/websocket'
 import ws from '@/lib/websocket'
-import useOTPModal from '@/components/OTP/useOTPModal'
-import otp from '@/api/otp'
+import use2FAModal from '@/components/2FA/use2FAModal'
+import twoFA from '@/api/2fa'
 
 
 let term: Terminal | null
 let term: Terminal | null
 let ping: NodeJS.Timeout
 let ping: NodeJS.Timeout
@@ -15,9 +15,9 @@ const websocket = shallowRef()
 const lostConnection = ref(false)
 const lostConnection = ref(false)
 
 
 onMounted(() => {
 onMounted(() => {
-  otp.secure_session_status()
+  twoFA.secure_session_status()
 
 
-  const otpModal = useOTPModal()
+  const otpModal = use2FAModal()
 
 
   otpModal.open().then(secureSessionId => {
   otpModal.open().then(secureSessionId => {
     websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
     websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)

+ 1 - 1
app/src/views/stream/components/RightSettings.vue

@@ -66,7 +66,7 @@ function on_change_enabled(checked: CheckedType) {
   >
   >
     <ContextHolder />
     <ContextHolder />
     <ACollapse
     <ACollapse
-      v-model:activeKey="active_key"
+      v-model:active-key="active_key"
       ghost
       ghost
     >
     >
       <ACollapsePanel
       <ACollapsePanel

+ 5 - 1
go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/BurntSushi/toml v1.4.0
 	github.com/BurntSushi/toml v1.4.0
 	github.com/caarlos0/env/v11 v11.2.2
 	github.com/caarlos0/env/v11 v11.2.2
 	github.com/casdoor/casdoor-go-sdk v0.50.0
 	github.com/casdoor/casdoor-go-sdk v0.50.0
+	github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476
 	github.com/creack/pty v1.1.23
 	github.com/creack/pty v1.1.23
 	github.com/dgraph-io/ristretto v0.1.1
 	github.com/dgraph-io/ristretto v0.1.1
 	github.com/dustin/go-humanize v1.0.1
 	github.com/dustin/go-humanize v1.0.1
@@ -16,6 +17,7 @@ require (
 	github.com/go-acme/lego/v4 v4.18.0
 	github.com/go-acme/lego/v4 v4.18.0
 	github.com/go-co-op/gocron v1.37.0
 	github.com/go-co-op/gocron v1.37.0
 	github.com/go-playground/validator/v10 v10.22.1
 	github.com/go-playground/validator/v10 v10.22.1
+	github.com/go-webauthn/webauthn v0.11.2
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/websocket v1.5.3
 	github.com/gorilla/websocket v1.5.3
@@ -100,6 +102,7 @@ require (
 	github.com/bytedance/sonic/loader v0.2.0 // indirect
 	github.com/bytedance/sonic/loader v0.2.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/chromedp/sysutil v1.0.0 // indirect
 	github.com/civo/civogo v0.3.77 // indirect
 	github.com/civo/civogo v0.3.77 // indirect
 	github.com/cloudflare/cloudflare-go v0.104.0 // indirect
 	github.com/cloudflare/cloudflare-go v0.104.0 // indirect
 	github.com/cloudwego/base64x v0.1.4 // indirect
 	github.com/cloudwego/base64x v0.1.4 // indirect
@@ -128,6 +131,7 @@ require (
 	github.com/go-resty/resty/v2 v2.15.0 // indirect
 	github.com/go-resty/resty/v2 v2.15.0 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
 	github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
+	github.com/go-webauthn/x v0.1.14 // indirect
 	github.com/goccy/go-json v0.10.3 // indirect
 	github.com/goccy/go-json v0.10.3 // indirect
 	github.com/gofrs/flock v0.12.1 // indirect
 	github.com/gofrs/flock v0.12.1 // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
@@ -138,6 +142,7 @@ require (
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 // indirect
 	github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/google/go-tpm v0.9.1 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/s2a-go v0.1.8 // indirect
 	github.com/google/s2a-go v0.1.8 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
@@ -243,7 +248,6 @@ require (
 	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
 	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
 	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
 	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
-	github.com/vultr/govultr/v2 v2.17.2 // indirect
 	github.com/vultr/govultr/v3 v3.9.1 // indirect
 	github.com/vultr/govultr/v3 v3.9.1 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
 	github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect

+ 12 - 252
go.sum

@@ -39,8 +39,6 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
 cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
 cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
-cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
-cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
 cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
 cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
 cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
 cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
@@ -102,12 +100,6 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
 cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
 cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
 cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
 cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
 cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
 cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
-cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
-cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
-cloud.google.com/go/auth v0.9.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w=
-cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk=
-cloud.google.com/go/auth v0.9.2 h1:I+Rq388FYU8QdbVB1IiPd+6KNdrqtAPE/asiKHShBLM=
-cloud.google.com/go/auth v0.9.2/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
 cloud.google.com/go/auth v0.9.4 h1:DxF7imbEbiFu9+zdKC6cKBko1e8XeJnipNqIbWZ+kDI=
 cloud.google.com/go/auth v0.9.4 h1:DxF7imbEbiFu9+zdKC6cKBko1e8XeJnipNqIbWZ+kDI=
 cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA=
 cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA=
 cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
 cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
@@ -186,14 +178,10 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj
 cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
 cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
 cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
 cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
 cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
 cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
-cloud.google.com/go/compute v1.28.0 h1:OPtBxMcheSS+DWfci803qvPly3d4w7Eu5ztKBcFfzwk=
-cloud.google.com/go/compute v1.28.0/go.mod h1:DEqZBtYrDnD5PvjsKwb3onnhX+qjdCVM7eshj1XdjV4=
 cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
 cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
-cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
 cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs=
 cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs=
 cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
 cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
 cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
 cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
@@ -705,12 +693,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.0 h1:GIwkDPfeF/IBh5lZ5Mig50r1LXomNXR7t/oKGSMJWns=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.0/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.7 h1:MOFLOVlBI1MvP4I0cwb9cXf83GNcMss1btQqjbp11CM=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.7/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.11 h1:U+8nVd9AEZrxpn3iuZNQq1NKhO65oZAsbcVgdvILxkI=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.11/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 github.com/aliyun/alibaba-cloud-sdk-go v1.63.16 h1:LNaqt0rxIcqHYarmdIZ3ZM7lqUWaWZ1Sqi1XPV1zMko=
 github.com/aliyun/alibaba-cloud-sdk-go v1.63.16 h1:LNaqt0rxIcqHYarmdIZ3ZM7lqUWaWZ1Sqi1XPV1zMko=
 github.com/aliyun/alibaba-cloud-sdk-go v1.63.16/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 github.com/aliyun/alibaba-cloud-sdk-go v1.63.16/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@@ -727,84 +709,32 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
 github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
 github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
-github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
-github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
-github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
 github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
 github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
 github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
 github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
-github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
-github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
-github.com/aws/aws-sdk-go-v2/config v1.27.31 h1:kxBoRsjhT3pq0cKthgj6RU6bXTm/2SgdoUMyrVw0rAI=
-github.com/aws/aws-sdk-go-v2/config v1.27.31/go.mod h1:z04nZdSWFPaDwK3DdJOG2r+scLQzMYuJeW0CujEm9FM=
 github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
 github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
 github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
 github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.30 h1:aau/oYFtibVovr2rDt8FHlU17BTicFEMAi29V1U+L5Q=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.30/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.3 h1:dy4sbyGy7BS4c0KaPZwg1P5ZP+lW+auTVcPiwrmbn8M=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.3/go.mod h1:EMgqMhof+RuaYvQavxKC0ZWvP7yB4B4NJhP+dbm13u0=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.4 h1:nR4GnokNdp25C6Z6xvXz5VqmzIhp4+aWMcM4w5FhlJ4=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.4/go.mod h1:w/6Ddm5GNEn0uLR6Wc35MGTvUXKDz8uNEMRrrdDB2ps=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 h1:ea6TO3HgVeVTB2Ie1djyBFWBOc9CohpKbo/QZbGTCJQ=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 h1:ea6TO3HgVeVTB2Ie1djyBFWBOc9CohpKbo/QZbGTCJQ=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6/go.mod h1:D2TUTD3v6AWmE5LzdCXLWNFtoYbSf6IEjKh1ggbuVdw=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6/go.mod h1:D2TUTD3v6AWmE5LzdCXLWNFtoYbSf6IEjKh1ggbuVdw=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 h1:MmLCRqP4U4Cw9gJ4bNrCG0mWqEtBlmAVleyelcHARMU=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3/go.mod h1:AMPjK2YnRh0YgOID3PqhJA1BRNfXDfGOnSsKHtAe8yA=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.43.0 h1:xtp7jye7KhWu4ptBs5yh1Vep0vLAGSNGmArOUp997DU=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.43.0/go.mod h1:QN7tFo/W8QjLCR6aPZqMZKaVQJiAp95r/g78x1LWtkA=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 h1:957e1/SwXIfPi/0OUJkH9YnPZRe9G6Kisd/xUhF7AUE=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 h1:957e1/SwXIfPi/0OUJkH9YnPZRe9G6Kisd/xUhF7AUE=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2/go.mod h1:343vcjcyOTuHTBBgUrOxPM36/jE96qLZnGL447ldrB0=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2/go.mod h1:343vcjcyOTuHTBBgUrOxPM36/jE96qLZnGL447ldrB0=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
 github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
 github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
 github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
 github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 h1:OMsEmCyz2i89XwRwPouAJvhj81wINh+4UK+k/0Yo/q8=
-github.com/aws/aws-sdk-go-v2/service/sts v1.30.5/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
 github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
 github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
@@ -825,8 +755,6 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW
 github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
 github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
 github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
-github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
 github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
 github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
 github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
 github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -837,8 +765,6 @@ github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdf
 github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
 github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
 github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
 github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
 github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
 github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
-github.com/casdoor/casdoor-go-sdk v0.49.0 h1:TD2VhOinkCaLII0RJglN58eihLXgDRWGoofZ+S1eqyc=
-github.com/casdoor/casdoor-go-sdk v0.49.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
 github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
 github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -846,28 +772,23 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
 github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
+github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
-github.com/civo/civogo v0.3.73 h1:thkNnkziU+xh+MEOChIUwRZI1forN20+SSAPe/VFDME=
-github.com/civo/civogo v0.3.73/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
-github.com/civo/civogo v0.3.75 h1:hrF+ALGDV5Be/jG9NmDo2wLhL4yuD8kIOxUbVRklGNU=
-github.com/civo/civogo v0.3.75/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
 github.com/civo/civogo v0.3.77 h1:1rl5cpQruPhh+w8BBMpGQsaovjDvA44udPoDTAa45rk=
 github.com/civo/civogo v0.3.77 h1:1rl5cpQruPhh+w8BBMpGQsaovjDvA44udPoDTAa45rk=
 github.com/civo/civogo v0.3.77/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
 github.com/civo/civogo v0.3.77/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cloudflare/cloudflare-go v0.102.0 h1:+0MGbkirM/yzVLOYpWMgW7CDdKzesSbdwA2Y+rABrWI=
-github.com/cloudflare/cloudflare-go v0.102.0/go.mod h1:BOB41tXf31ti/qtBO9paYhyapotQbGRDbQoLOAF7pSg=
-github.com/cloudflare/cloudflare-go v0.103.0 h1:XXKzgXeUbAo7UTtM4T5wuD2bJPBtNZv7TlZAEy5QI4k=
-github.com/cloudflare/cloudflare-go v0.103.0/go.mod h1:0DrjT4g8wgYFYIxhlqR8xi8dNWfyHFGilUkU3+XV8h0=
 github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
 github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
 github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
 github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
 github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
 github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
@@ -971,8 +892,6 @@ github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0Nglqm
 github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
 github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
-github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q=
-github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U=
 github.com/go-acme/lego/v4 v4.18.0 h1:2hH8KcdRBSb+p5o9VZIm61GAOXYALgILUCSs1Q+OYsk=
 github.com/go-acme/lego/v4 v4.18.0 h1:2hH8KcdRBSb+p5o9VZIm61GAOXYALgILUCSs1Q+OYsk=
 github.com/go-acme/lego/v4 v4.18.0/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
 github.com/go-acme/lego/v4 v4.18.0/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
@@ -1017,12 +936,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
-github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
 github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
-github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU=
-github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg=
 github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90=
 github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90=
 github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
 github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
@@ -1035,6 +950,10 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
 github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
 github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
+github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
+github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
+github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -1053,7 +972,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
-github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
@@ -1133,6 +1051,8 @@ github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
+github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -1174,10 +1094,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
 github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
-github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
-github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
-github.com/googleapis/enterprise-certificate-proxy v0.3.3 h1:QRje2j5GZimBzlbhGA2V2QlGNgL8G6e+wGo/+/2bWI0=
-github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
 github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
 github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
 github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
 github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -1381,8 +1297,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/linode/linodego v1.39.0 h1:gRsj2PXX+HTO3eYQaXEuQGsLeeLFDSBDontC5JL3Nn8=
-github.com/linode/linodego v1.39.0/go.mod h1:da8KzAQKSm5obwa06yXk5CZSDFMP9Wb08GA/O+aR9W0=
 github.com/linode/linodego v1.40.0 h1:7ESY0PwK94hoggoCtIroT1Xk6b1flrFBNZ6KwqbTqlI=
 github.com/linode/linodego v1.40.0 h1:7ESY0PwK94hoggoCtIroT1Xk6b1flrFBNZ6KwqbTqlI=
 github.com/linode/linodego v1.40.0/go.mod h1:NsUw4l8QrLdIofRg1NYFBbW5ZERnmbZykVBszPZLORM=
 github.com/linode/linodego v1.40.0/go.mod h1:NsUw4l8QrLdIofRg1NYFBbW5ZERnmbZykVBszPZLORM=
 github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
 github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
@@ -1392,10 +1306,6 @@ github.com/liquidweb/liquidweb-cli v0.7.0 h1:7j1r1U0MZa1TXiWo3IMU5V1YQwnBHMVxU+x
 github.com/liquidweb/liquidweb-cli v0.7.0/go.mod h1:+uU7L6BhaQtgo4cYKhhsP5UNCq/imNvjBjlf76Vqpb0=
 github.com/liquidweb/liquidweb-cli v0.7.0/go.mod h1:+uU7L6BhaQtgo4cYKhhsP5UNCq/imNvjBjlf76Vqpb0=
 github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc=
 github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc=
 github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4=
 github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4=
-github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
-github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
-github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 h1:5RK988zAqB3/AN3opGfRpoQgAVqr6/A5+qRTi67VUZY=
-github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
 github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
 github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
 github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
 github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
 github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
 github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
@@ -1431,8 +1341,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
-github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
 github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
 github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
 github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
@@ -1529,10 +1437,6 @@ github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
 github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
 github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
-github.com/oracle/oci-go-sdk/v65 v65.71.1 h1:t1GpyLYaD/x2OrUoSyxNwBQaDaQP4F084FX8LQMXA/s=
-github.com/oracle/oci-go-sdk/v65 v65.71.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
-github.com/oracle/oci-go-sdk/v65 v65.72.0 h1:gPCb5fBUsZMyafIilPPB2B36yqjkKnnwwiJT4xexUMg=
-github.com/oracle/oci-go-sdk/v65 v65.72.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
 github.com/oracle/oci-go-sdk/v65 v65.73.0 h1:C7uel6CoKk4A1KPkdhFBAyvVyFRTHAmX8m0o64RmfPg=
 github.com/oracle/oci-go-sdk/v65 v65.73.0 h1:C7uel6CoKk4A1KPkdhFBAyvVyFRTHAmX8m0o64RmfPg=
 github.com/oracle/oci-go-sdk/v65 v65.73.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
 github.com/oracle/oci-go-sdk/v65 v65.73.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
 github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
 github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
@@ -1543,10 +1447,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
 github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
@@ -1633,16 +1534,8 @@ github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
 github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
 github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
 github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
 github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
 github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
 github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
-github.com/sashabaranov/go-openai v1.28.1 h1:aREx6faUTeOZNMDTNGAY8B9vNmmN7qoGvDV0Ke2J1Mc=
-github.com/sashabaranov/go-openai v1.28.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
-github.com/sashabaranov/go-openai v1.29.0 h1:eBH6LSjtX4md5ImDCX8hNhHQvaRf22zujiERoQpsvLo=
-github.com/sashabaranov/go-openai v1.29.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
-github.com/sashabaranov/go-openai v1.29.1 h1:AlB+vwpg1tibwr83OKXLsI4V1rnafVyTlw0BjR+6WUM=
-github.com/sashabaranov/go-openai v1.29.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/sashabaranov/go-openai v1.29.2 h1:jYpp1wktFoOvxHnum24f/w4+DFzUdJnu83trr5+Slh0=
 github.com/sashabaranov/go-openai v1.29.2 h1:jYpp1wktFoOvxHnum24f/w4+DFzUdJnu83trr5+Slh0=
 github.com/sashabaranov/go-openai v1.29.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/sashabaranov/go-openai v1.29.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29 h1:BkTk4gynLjguayxrYxZoMZjBnAOh7ntQvUkOFmkMqPU=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
 github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
 github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
@@ -1738,20 +1631,8 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W
 github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
 github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
 github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
 github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
 github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
 github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984 h1:QLSx+ibsV68NXKgzofPuo1gxFwYSWk2++rvxZxNjbVo=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.991 h1:0Xg2IUktDgGsjBv82WTmTQdHZFRwS2XDUnuOHexCxVw=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.991/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.993 h1:+iJMmF0q1MPyhLs0+J7CcJ47w/vq6ICsCxnV4gc0dKw=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.993/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.984 h1:ABZeSsOOkkBn+gToVp8KkMt4E69hQkBMEFegCD4w15Q=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.984/go.mod h1:r++X8dKvTZWltr4J83TIwqGlyvG5fKaVh7RGC2+BryI=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.991 h1:GTf6Cp2beg/zfxuhj5qwEHrR1AhBJrk+CYGzt6pRxJo=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.991/go.mod h1:9v9MJPZQHh7XMr7cESUHcIXpIJb/sFtp++OsanrwhaU=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.993 h1:x2nkr/Kok+DV1K1DHqnvNgZTXDjOZVgkBXwtqVptKWk=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.993/go.mod h1:lEQPVB5HPTf8LU4EE9C7VpYtOwM0xpaFQerX0b+a9Z4=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
 github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
 github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
@@ -1759,8 +1640,6 @@ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYN
 github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
 github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
 github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
 github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/transip/gotransip/v6 v6.25.0 h1:/H+SjMq/9HNZ0/maE1OLhJpxLaCGHsxq0PWaMPJHxK4=
-github.com/transip/gotransip/v6 v6.25.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
 github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
 github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
 github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
 github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
@@ -1772,8 +1651,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
 github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
-github.com/ultradns/ultradns-go-sdk v1.6.2-20240501171831-432d643 h1:Y2gOdFNdP0QrXN7HkhrT42686bxBmDPqq5Xu8RgeU2s=
-github.com/ultradns/ultradns-go-sdk v1.6.2-20240501171831-432d643/go.mod h1:mqka31zT/P4yfNKj1qbOXUqamham/YO05GgUc/dOrl8=
 github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a h1:R6IR+Vj/RnGZLnX8PpPQsbbQthctO7Ah2q4tj5eoe2o=
 github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a h1:R6IR+Vj/RnGZLnX8PpPQsbbQthctO7Ah2q4tj5eoe2o=
 github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
 github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -1786,8 +1663,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
 github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
 github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
-github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
-github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
 github.com/vultr/govultr/v3 v3.9.1 h1:uxSIb8Miel7tqTs3ee+z3t+JelZikwqBBsZzCOPBy/8=
 github.com/vultr/govultr/v3 v3.9.1 h1:uxSIb8Miel7tqTs3ee+z3t+JelZikwqBBsZzCOPBy/8=
 github.com/vultr/govultr/v3 v3.9.1/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+HeUMfHm2o=
 github.com/vultr/govultr/v3 v3.9.1/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+HeUMfHm2o=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -1801,20 +1676,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
-github.com/yandex-cloud/go-genproto v0.0.0-20240813143603-58770ef469b7 h1:PSXr/xm10ZZ0f2pDWCX6wtY7EXfyBtoAGAD5Rzxstb0=
-github.com/yandex-cloud/go-genproto v0.0.0-20240813143603-58770ef469b7/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
-github.com/yandex-cloud/go-genproto v0.0.0-20240819112322-98a264d392f6 h1:w57l27dDkJTVSi8hM3H/WVkiv+CsJwAIweqO6pFdljk=
-github.com/yandex-cloud/go-genproto v0.0.0-20240819112322-98a264d392f6/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
-github.com/yandex-cloud/go-genproto v0.0.0-20240829130658-0568052c5a6a h1:GCVnt5H4CB4np3ReSNH0GpBg5HDaLz1rZKnjhQjQGL4=
-github.com/yandex-cloud/go-genproto v0.0.0-20240829130658-0568052c5a6a/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
 github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 h1:WgeEP+8WizCQyccJNHOMLONq23qVAzYHtyg5qTdUWmg=
 github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 h1:WgeEP+8WizCQyccJNHOMLONq23qVAzYHtyg5qTdUWmg=
 github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
 github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
-github.com/yandex-cloud/go-sdk v0.0.0-20240813144531-905aa41b481f h1:oetXcQPVH/CfyBD5MXnxOQY7IFvhTZpLLQKKLmTVRPM=
-github.com/yandex-cloud/go-sdk v0.0.0-20240813144531-905aa41b481f/go.mod h1:9sGM6Epw7DGLLs57/XVQzzwY1ZRP3U5xyqg8j8wdn3M=
-github.com/yandex-cloud/go-sdk v0.0.0-20240819112606-8a626cdc403d h1:eYs6TKjvjzYgAar7n2Ic4a+jIP08IfswtvCZ8iJqdKk=
-github.com/yandex-cloud/go-sdk v0.0.0-20240819112606-8a626cdc403d/go.mod h1:WYdfvXcvRn3kbVcwpav4J3jd1STOYtYvkTqx0wm4leM=
-github.com/yandex-cloud/go-sdk v0.0.0-20240829131820-fa8ad79f88a4 h1:l9x2SuRwFBvCTZvIlr8JqnjrHlr0a2UF/m/zdGnl+cs=
-github.com/yandex-cloud/go-sdk v0.0.0-20240829131820-fa8ad79f88a4/go.mod h1:/kMfiARiUXWqYG9EX1g5cZuvW+vY5M/oFROiUg0na+0=
 github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 h1:Q4LvUMF4kzaGtopoIdXReL9/qGtmzOewBhF3dQvuHMU=
 github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 h1:Q4LvUMF4kzaGtopoIdXReL9/qGtmzOewBhF3dQvuHMU=
 github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5/go.mod h1:9dt2V80cfJGRZA+5SKP3Ky+R/DxH02XfKObi2Uy2uPc=
 github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5/go.mod h1:9dt2V80cfJGRZA+5SKP3Ky+R/DxH02XfKObi2Uy2uPc=
 github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
 github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
@@ -1850,28 +1713,12 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
-go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
-go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
-go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
-go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
 go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
 go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
 go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
 go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
-go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
-go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
-go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
-go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
 go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
 go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
 go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
 go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
-go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
-go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
-go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
-go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
 go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
 go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
 go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
 go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
@@ -1897,8 +1744,6 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
 go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
-golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
 golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
 golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1924,11 +1769,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
 golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
-golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
-golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
 golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
 golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1946,10 +1786,6 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
-golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
-golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
-golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
-golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -1994,11 +1830,6 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -2074,12 +1905,6 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
-golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
 golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
 golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
 golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -2111,8 +1936,6 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
 golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
-golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
-golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
 golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
 golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2131,9 +1954,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2251,14 +2071,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
-golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2272,11 +2086,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
-golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
-golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
-golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
+golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2295,10 +2106,6 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
-golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2378,10 +2185,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
-golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
 golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
 golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
 golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
 golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2458,12 +2261,6 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
 google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
 google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
 google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
 google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
-google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0=
-google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ=
-google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s=
-google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0=
-google.golang.org/api v0.195.0 h1:Ude4N8FvTKnnQJHU48RFI40jOBgIrL8Zqr3/QeST6yU=
-google.golang.org/api v0.195.0/go.mod h1:DOGRWuv3P8TU8Lnz7uQc4hyNqrBpMtD9ppW3wBJurgc=
 google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
 google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
 google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
 google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -2606,28 +2403,10 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
 google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
-google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0=
-google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4=
-google.golang.org/genproto v0.0.0-20240826202546-f6391c0de4c7 h1:f9Ho9PuVgvteqb4gfM3WOeMUZG6n4Lq8xfZ1Ja2dohQ=
-google.golang.org/genproto v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:ICjniACoWvcDz8c8bOsHVKuuSGDJy1z5M4G0DM3HzTc=
-google.golang.org/genproto v0.0.0-20240827150818-7e3bb234dfed h1:4C4dbrVFtfIp3GXJdMX1Sj25mahfn5DywOo65/2ISQ8=
-google.golang.org/genproto v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:ICjniACoWvcDz8c8bOsHVKuuSGDJy1z5M4G0DM3HzTc=
 google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
 google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
 google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
 google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
-google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
-google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
-google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
-google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
 google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
 google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
 google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
 google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -2672,10 +2451,6 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
 google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
 google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
-google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
-google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
-google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
-google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
 google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
 google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
 google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
 google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
@@ -2740,11 +2515,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/datatypes v1.2.1 h1:r+g0bk4LPCW2v4+Ls7aeNgGme7JYdNDQ2VtvlNUfBh0=
-gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs=
 gorm.io/datatypes v1.2.2 h1:sdn7ZmG4l7JWtMDUb3L98f2Ym7CO5F8mZLlrQJMfF9g=
 gorm.io/datatypes v1.2.2 h1:sdn7ZmG4l7JWtMDUb3L98f2Ym7CO5F8mZLlrQJMfF9g=
 gorm.io/datatypes v1.2.2/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
 gorm.io/datatypes v1.2.2/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
-gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
 gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
 gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU=
 gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU=
@@ -2759,14 +2531,10 @@ gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
 gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
-gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
 gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
 gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
 gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
 gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
 gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
 gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
 gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
 gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
-gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII=
-gorm.io/plugin/dbresolver v1.5.2/go.mod h1:jPh59GOQbO7v7v28ZKZPd45tr+u3vyT+8tHdfdfOWcU=
 gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
 gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
 gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
 gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
@@ -2779,20 +2547,12 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
-k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
-k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
 k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
 k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
 k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
 k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
-k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
-k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
 k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
 k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
 k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
 k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
-k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-k8s.io/utils v0.0.0-20240821151609-f90d01438635 h1:2wThSvJoW/Ncn9TmQEYXRnevZXi2duqHWf5OX9S3zjI=
-k8s.io/utils v0.0.0-20240821151609-f90d01438635/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA=
 k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA=
 k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

+ 4 - 0
internal/cache/cache.go

@@ -29,3 +29,7 @@ func Set(key interface{}, value interface{}, ttl time.Duration) {
 func Get(key interface{}) (value interface{}, ok bool) {
 func Get(key interface{}) (value interface{}, ok bool) {
 	return cache.Get(key)
 	return cache.Get(key)
 }
 }
+
+func Del(key interface{}) {
+	cache.Del(key)
+}

+ 2 - 0
internal/kernal/boot.go

@@ -9,6 +9,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/internal/cluster"
 	"github.com/0xJacky/Nginx-UI/internal/cluster"
 	"github.com/0xJacky/Nginx-UI/internal/cron"
 	"github.com/0xJacky/Nginx-UI/internal/cron"
 	"github.com/0xJacky/Nginx-UI/internal/logger"
 	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/internal/passkey"
 	"github.com/0xJacky/Nginx-UI/internal/validation"
 	"github.com/0xJacky/Nginx-UI/internal/validation"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/0xJacky/Nginx-UI/query"
@@ -50,6 +51,7 @@ func InitAfterDatabase() {
 		cron.InitCronJobs,
 		cron.InitCronJobs,
 		cluster.RegisterPredefinedNodes,
 		cluster.RegisterPredefinedNodes,
 		analytic.RetrieveNodesStatus,
 		analytic.RetrieveNodesStatus,
+		passkey.Init,
 	}
 	}
 
 
 	for _, v := range syncs {
 	for _, v := range syncs {

+ 2 - 2
internal/kernal/skip_install.go

@@ -51,7 +51,7 @@ func registerPredefinedUser() {
 		logger.Fatal(err)
 		logger.Fatal(err)
 	}
 	}
 
 
-	u := query.Auth
+	u := query.User
 
 
 	_, err = u.First()
 	_, err = u.First()
 
 
@@ -63,7 +63,7 @@ func registerPredefinedUser() {
 	// Create a new user with the predefined name and password
 	// Create a new user with the predefined name and password
 	pwd, _ := bcrypt.GenerateFromPassword([]byte(pUser.Password), bcrypt.DefaultCost)
 	pwd, _ := bcrypt.GenerateFromPassword([]byte(pUser.Password), bcrypt.DefaultCost)
 
 
-	err = u.Create(&model.Auth{
+	err = u.Create(&model.User{
 		Name:     pUser.Name,
 		Name:     pUser.Name,
 		Password: string(pwd),
 		Password: string(pwd),
 	})
 	})

+ 1 - 1
internal/middleware/secure_session.go

@@ -14,7 +14,7 @@ func RequireSecureSession() gin.HandlerFunc {
 			c.Next()
 			c.Next()
 			return
 			return
 		}
 		}
-		cUser := u.(*model.Auth)
+		cUser := u.(*model.User)
 		if !cUser.EnabledOTP() {
 		if !cUser.EnabledOTP() {
 			c.Next()
 			c.Next()
 			return
 			return

+ 46 - 0
internal/passkey/webauthn.go

@@ -0,0 +1,46 @@
+package passkey
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/go-webauthn/webauthn/protocol"
+	"github.com/go-webauthn/webauthn/webauthn"
+)
+
+var instance *webauthn.WebAuthn
+
+func Init() {
+	options := &settings.WebAuthnSettings
+
+	if !Enabled() {
+		logger.Debug("WebAuthn settings are not configured")
+		return
+	}
+	requireResidentKey := true
+	var err error
+	instance, err = webauthn.New(&webauthn.Config{
+		RPDisplayName: options.RPDisplayName,
+		RPID:          options.RPID,
+		RPOrigins:     options.RPOrigins,
+		AuthenticatorSelection: protocol.AuthenticatorSelection{
+			RequireResidentKey: &requireResidentKey,
+			UserVerification:   "required",
+		},
+	})
+
+	if err != nil {
+		logger.Fatal(err)
+	}
+}
+
+func Enabled() bool {
+	options := &settings.WebAuthnSettings
+	if options.RPDisplayName == "" || options.RPID == "" || len(options.RPOrigins) == 0 {
+		return false
+	}
+	return true
+}
+
+func GetInstance() *webauthn.WebAuthn {
+	return instance
+}

+ 2 - 2
internal/user/login.go

@@ -14,8 +14,8 @@ var (
 	ErrUserBanned        = errors.New("user banned")
 	ErrUserBanned        = errors.New("user banned")
 )
 )
 
 
-func Login(name string, password string) (user *model.Auth, err error) {
-	u := query.Auth
+func Login(name string, password string) (user *model.User, err error) {
+	u := query.User
 
 
 	user, err = u.Where(u.Name.Eq(name)).First()
 	user, err = u.Where(u.Name.Eq(name)).First()
 	if err != nil {
 	if err != nil {

+ 2 - 2
internal/user/otp.go

@@ -19,7 +19,7 @@ var (
 	ErrRecoveryCode = errors.New("invalid recovery code")
 	ErrRecoveryCode = errors.New("invalid recovery code")
 )
 )
 
 
-func VerifyOTP(user *model.Auth, otp, recoveryCode string) (err error) {
+func VerifyOTP(user *model.User, otp, recoveryCode string) (err error) {
 	if otp != "" {
 	if otp != "" {
 		decrypted, err := crypto.AesDecrypt(user.OTPSecret)
 		decrypted, err := crypto.AesDecrypt(user.OTPSecret)
 		if err != nil {
 		if err != nil {
@@ -43,7 +43,7 @@ func VerifyOTP(user *model.Auth, otp, recoveryCode string) (err error) {
 }
 }
 
 
 func secureSessionIDCacheKey(sessionId string) string {
 func secureSessionIDCacheKey(sessionId string) string {
-	return fmt.Sprintf("otp_secure_session:_%s", sessionId)
+	return fmt.Sprintf("2fa_secure_session:_%s", sessionId)
 }
 }
 
 
 func SetSecureSessionID(userId int) (sessionId string) {
 func SetSecureSessionID(userId int) (sessionId string) {

+ 7 - 7
internal/user/user.go

@@ -26,9 +26,9 @@ func BuildCacheTokenKey(token string) string {
 	return sb.String()
 	return sb.String()
 }
 }
 
 
-func GetUser(name string) (user *model.Auth, err error) {
+func GetUser(name string) (user *model.User, err error) {
 	db := model.UseDB()
 	db := model.UseDB()
-	user = &model.Auth{}
+	user = &model.User{}
 	err = db.Where("name", name).First(user).Error
 	err = db.Where("name", name).First(user).Error
 	if err != nil {
 	if err != nil {
 		return
 		return
@@ -41,7 +41,7 @@ func DeleteToken(token string) {
 	_, _ = q.Where(q.Token.Eq(token)).Delete()
 	_, _ = q.Where(q.Token.Eq(token)).Delete()
 }
 }
 
 
-func GetTokenUser(token string) (*model.Auth, bool) {
+func GetTokenUser(token string) (*model.User, bool) {
 	q := query.AuthToken
 	q := query.AuthToken
 	authToken, err := q.Where(q.Token.Eq(token)).First()
 	authToken, err := q.Where(q.Token.Eq(token)).First()
 	if err != nil {
 	if err != nil {
@@ -53,12 +53,12 @@ func GetTokenUser(token string) (*model.Auth, bool) {
 		return nil, false
 		return nil, false
 	}
 	}
 
 
-	u := query.Auth
+	u := query.User
 	user, err := u.FirstByID(authToken.UserID)
 	user, err := u.FirstByID(authToken.UserID)
 	return user, err == nil
 	return user, err == nil
 }
 }
 
 
-func GenerateJWT(user *model.Auth) (string, error) {
+func GenerateJWT(user *model.User) (string, error) {
 	claims := JWTClaims{
 	claims := JWTClaims{
 		Name:   user.Name,
 		Name:   user.Name,
 		UserID: user.ID,
 		UserID: user.ID,
@@ -114,7 +114,7 @@ func ValidateJWT(token string) (claims *JWTClaims, err error) {
 	return
 	return
 }
 }
 
 
-func CurrentUser(token string) (u *model.Auth, err error) {
+func CurrentUser(token string) (u *model.User, err error) {
 	// validate token
 	// validate token
 	var claims *JWTClaims
 	var claims *JWTClaims
 	claims, err = ValidateJWT(token)
 	claims, err = ValidateJWT(token)
@@ -123,7 +123,7 @@ func CurrentUser(token string) (u *model.Auth, err error) {
 	}
 	}
 
 
 	// get user by id
 	// get user by id
-	user := query.Auth
+	user := query.User
 	u, err = user.FirstByID(claims.UserID)
 	u, err = user.FirstByID(claims.UserID)
 	if err != nil {
 	if err != nil {
 		return
 		return

+ 43 - 11
model/auth.go

@@ -1,15 +1,17 @@
 package model
 package model
 
 
-import "gorm.io/gorm"
+import (
+	"github.com/go-webauthn/webauthn/webauthn"
+	"github.com/spf13/cast"
+)
 
 
-type Auth struct {
+type User struct {
 	Model
 	Model
 
 
-	Name       string `json:"name"`
-	Password   string `json:"-"`
-	Status     bool   `json:"status" gorm:"default:1"`
-	OTPSecret  []byte `json:"-" gorm:"type:blob"`
-	Enabled2FA bool   `json:"enabled_2fa" gorm:"-"`
+	Name      string `json:"name"`
+	Password  string `json:"-"`
+	Status    bool   `json:"status" gorm:"default:1"`
+	OTPSecret []byte `json:"-" gorm:"type:blob"`
 }
 }
 
 
 type AuthToken struct {
 type AuthToken struct {
@@ -18,11 +20,41 @@ type AuthToken struct {
 	ExpiredAt int64  `json:"expired_at" gorm:"default:0"`
 	ExpiredAt int64  `json:"expired_at" gorm:"default:0"`
 }
 }
 
 
-func (u *Auth) AfterFind(tx *gorm.DB) error {
-    u.Enabled2FA = u.EnabledOTP()
-    return nil
+func (u *User) TableName() string {
+	return "auths"
 }
 }
 
 
-func (u *Auth) EnabledOTP() bool {
+func (u *User) EnabledOTP() bool {
 	return len(u.OTPSecret) != 0
 	return len(u.OTPSecret) != 0
 }
 }
+
+func (u *User) EnabledPasskey() bool {
+	var passkeys Passkey
+	db.Where("user_id", u.ID).First(&passkeys)
+	return passkeys.ID != 0
+}
+
+func (u *User) Enabled2FA() bool {
+	return u.EnabledOTP() || u.EnabledPasskey()
+}
+
+func (u *User) WebAuthnID() []byte {
+	return []byte(cast.ToString(u.ID))
+}
+
+func (u *User) WebAuthnName() string {
+	return u.Name
+}
+
+func (u *User) WebAuthnDisplayName() string {
+	return u.Name
+}
+
+func (u *User) WebAuthnCredentials() (credentials []webauthn.Credential) {
+	var passkeys []Passkey
+	db.Where("user_id", u.ID).Find(&passkeys)
+	for _, passkey := range passkeys {
+		credentials = append(credentials, *passkey.Credential)
+	}
+	return
+}

+ 2 - 1
model/model.go

@@ -25,7 +25,7 @@ type Model struct {
 func GenerateAllModel() []any {
 func GenerateAllModel() []any {
 	return []any{
 	return []any{
 		ConfigBackup{},
 		ConfigBackup{},
-		Auth{},
+		User{},
 		AuthToken{},
 		AuthToken{},
 		Cert{},
 		Cert{},
 		ChatGPTLog{},
 		ChatGPTLog{},
@@ -37,6 +37,7 @@ func GenerateAllModel() []any {
 		AcmeUser{},
 		AcmeUser{},
 		BanIP{},
 		BanIP{},
 		Config{},
 		Config{},
+		Passkey{},
 	}
 	}
 }
 }
 
 

+ 13 - 0
model/passkey.go

@@ -0,0 +1,13 @@
+package model
+
+import "github.com/go-webauthn/webauthn/webauthn"
+
+type Passkey struct {
+	Model
+
+	Name       string               `json:"name"`
+	UserID     int                  `json:"user_id"`
+	RawID      string               `json:"raw_id"`
+	Credential *webauthn.Credential `json:"-" gorm:"serializer:json"`
+	LastUsedAt int64                `json:"last_used_at" gorm:"default:0"`
+}

+ 157 - 157
query/auths.gen.go

@@ -20,30 +20,30 @@ import (
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/model"
 )
 )
 
 
-func newAuth(db *gorm.DB, opts ...gen.DOOption) auth {
-	_auth := auth{}
+func newUser(db *gorm.DB, opts ...gen.DOOption) user {
+	_user := user{}
 
 
-	_auth.authDo.UseDB(db, opts...)
-	_auth.authDo.UseModel(&model.Auth{})
+	_user.userDo.UseDB(db, opts...)
+	_user.userDo.UseModel(&model.User{})
 
 
-	tableName := _auth.authDo.TableName()
-	_auth.ALL = field.NewAsterisk(tableName)
-	_auth.ID = field.NewInt(tableName, "id")
-	_auth.CreatedAt = field.NewTime(tableName, "created_at")
-	_auth.UpdatedAt = field.NewTime(tableName, "updated_at")
-	_auth.DeletedAt = field.NewField(tableName, "deleted_at")
-	_auth.Name = field.NewString(tableName, "name")
-	_auth.Password = field.NewString(tableName, "password")
-	_auth.Status = field.NewBool(tableName, "status")
-	_auth.OTPSecret = field.NewBytes(tableName, "otp_secret")
+	tableName := _user.userDo.TableName()
+	_user.ALL = field.NewAsterisk(tableName)
+	_user.ID = field.NewInt(tableName, "id")
+	_user.CreatedAt = field.NewTime(tableName, "created_at")
+	_user.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_user.DeletedAt = field.NewField(tableName, "deleted_at")
+	_user.Name = field.NewString(tableName, "name")
+	_user.Password = field.NewString(tableName, "password")
+	_user.Status = field.NewBool(tableName, "status")
+	_user.OTPSecret = field.NewBytes(tableName, "otp_secret")
 
 
-	_auth.fillFieldMap()
+	_user.fillFieldMap()
 
 
-	return _auth
+	return _user
 }
 }
 
 
-type auth struct {
-	authDo
+type user struct {
+	userDo
 
 
 	ALL       field.Asterisk
 	ALL       field.Asterisk
 	ID        field.Int
 	ID        field.Int
@@ -58,34 +58,34 @@ type auth struct {
 	fieldMap map[string]field.Expr
 	fieldMap map[string]field.Expr
 }
 }
 
 
-func (a auth) Table(newTableName string) *auth {
-	a.authDo.UseTable(newTableName)
-	return a.updateTableName(newTableName)
+func (u user) Table(newTableName string) *user {
+	u.userDo.UseTable(newTableName)
+	return u.updateTableName(newTableName)
 }
 }
 
 
-func (a auth) As(alias string) *auth {
-	a.authDo.DO = *(a.authDo.As(alias).(*gen.DO))
-	return a.updateTableName(alias)
+func (u user) As(alias string) *user {
+	u.userDo.DO = *(u.userDo.As(alias).(*gen.DO))
+	return u.updateTableName(alias)
 }
 }
 
 
-func (a *auth) updateTableName(table string) *auth {
-	a.ALL = field.NewAsterisk(table)
-	a.ID = field.NewInt(table, "id")
-	a.CreatedAt = field.NewTime(table, "created_at")
-	a.UpdatedAt = field.NewTime(table, "updated_at")
-	a.DeletedAt = field.NewField(table, "deleted_at")
-	a.Name = field.NewString(table, "name")
-	a.Password = field.NewString(table, "password")
-	a.Status = field.NewBool(table, "status")
-	a.OTPSecret = field.NewBytes(table, "otp_secret")
+func (u *user) updateTableName(table string) *user {
+	u.ALL = field.NewAsterisk(table)
+	u.ID = field.NewInt(table, "id")
+	u.CreatedAt = field.NewTime(table, "created_at")
+	u.UpdatedAt = field.NewTime(table, "updated_at")
+	u.DeletedAt = field.NewField(table, "deleted_at")
+	u.Name = field.NewString(table, "name")
+	u.Password = field.NewString(table, "password")
+	u.Status = field.NewBool(table, "status")
+	u.OTPSecret = field.NewBytes(table, "otp_secret")
 
 
-	a.fillFieldMap()
+	u.fillFieldMap()
 
 
-	return a
+	return u
 }
 }
 
 
-func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
-	_f, ok := a.fieldMap[fieldName]
+func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := u.fieldMap[fieldName]
 	if !ok || _f == nil {
 	if !ok || _f == nil {
 		return nil, false
 		return nil, false
 	}
 	}
@@ -93,32 +93,32 @@ func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 	return _oe, ok
 	return _oe, ok
 }
 }
 
 
-func (a *auth) fillFieldMap() {
-	a.fieldMap = make(map[string]field.Expr, 8)
-	a.fieldMap["id"] = a.ID
-	a.fieldMap["created_at"] = a.CreatedAt
-	a.fieldMap["updated_at"] = a.UpdatedAt
-	a.fieldMap["deleted_at"] = a.DeletedAt
-	a.fieldMap["name"] = a.Name
-	a.fieldMap["password"] = a.Password
-	a.fieldMap["status"] = a.Status
-	a.fieldMap["otp_secret"] = a.OTPSecret
+func (u *user) fillFieldMap() {
+	u.fieldMap = make(map[string]field.Expr, 8)
+	u.fieldMap["id"] = u.ID
+	u.fieldMap["created_at"] = u.CreatedAt
+	u.fieldMap["updated_at"] = u.UpdatedAt
+	u.fieldMap["deleted_at"] = u.DeletedAt
+	u.fieldMap["name"] = u.Name
+	u.fieldMap["password"] = u.Password
+	u.fieldMap["status"] = u.Status
+	u.fieldMap["otp_secret"] = u.OTPSecret
 }
 }
 
 
-func (a auth) clone(db *gorm.DB) auth {
-	a.authDo.ReplaceConnPool(db.Statement.ConnPool)
-	return a
+func (u user) clone(db *gorm.DB) user {
+	u.userDo.ReplaceConnPool(db.Statement.ConnPool)
+	return u
 }
 }
 
 
-func (a auth) replaceDB(db *gorm.DB) auth {
-	a.authDo.ReplaceDB(db)
-	return a
+func (u user) replaceDB(db *gorm.DB) user {
+	u.userDo.ReplaceDB(db)
+	return u
 }
 }
 
 
-type authDo struct{ gen.DO }
+type userDo struct{ gen.DO }
 
 
 // FirstByID Where("id=@id")
 // FirstByID Where("id=@id")
-func (a authDo) FirstByID(id int) (result *model.Auth, err error) {
+func (u userDo) FirstByID(id int) (result *model.User, err error) {
 	var params []interface{}
 	var params []interface{}
 
 
 	var generateSQL strings.Builder
 	var generateSQL strings.Builder
@@ -126,14 +126,14 @@ func (a authDo) FirstByID(id int) (result *model.Auth, err error) {
 	generateSQL.WriteString("id=? ")
 	generateSQL.WriteString("id=? ")
 
 
 	var executeSQL *gorm.DB
 	var executeSQL *gorm.DB
-	executeSQL = a.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	executeSQL = u.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
 	err = executeSQL.Error
 	err = executeSQL.Error
 
 
 	return
 	return
 }
 }
 
 
 // DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
 // DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
-func (a authDo) DeleteByID(id int) (err error) {
+func (u userDo) DeleteByID(id int) (err error) {
 	var params []interface{}
 	var params []interface{}
 
 
 	var generateSQL strings.Builder
 	var generateSQL strings.Builder
@@ -141,206 +141,206 @@ func (a authDo) DeleteByID(id int) (err error) {
 	generateSQL.WriteString("update auths set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
 	generateSQL.WriteString("update auths set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
 
 
 	var executeSQL *gorm.DB
 	var executeSQL *gorm.DB
-	executeSQL = a.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	executeSQL = u.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
 	err = executeSQL.Error
 	err = executeSQL.Error
 
 
 	return
 	return
 }
 }
 
 
-func (a authDo) Debug() *authDo {
-	return a.withDO(a.DO.Debug())
+func (u userDo) Debug() *userDo {
+	return u.withDO(u.DO.Debug())
 }
 }
 
 
-func (a authDo) WithContext(ctx context.Context) *authDo {
-	return a.withDO(a.DO.WithContext(ctx))
+func (u userDo) WithContext(ctx context.Context) *userDo {
+	return u.withDO(u.DO.WithContext(ctx))
 }
 }
 
 
-func (a authDo) ReadDB() *authDo {
-	return a.Clauses(dbresolver.Read)
+func (u userDo) ReadDB() *userDo {
+	return u.Clauses(dbresolver.Read)
 }
 }
 
 
-func (a authDo) WriteDB() *authDo {
-	return a.Clauses(dbresolver.Write)
+func (u userDo) WriteDB() *userDo {
+	return u.Clauses(dbresolver.Write)
 }
 }
 
 
-func (a authDo) Session(config *gorm.Session) *authDo {
-	return a.withDO(a.DO.Session(config))
+func (u userDo) Session(config *gorm.Session) *userDo {
+	return u.withDO(u.DO.Session(config))
 }
 }
 
 
-func (a authDo) Clauses(conds ...clause.Expression) *authDo {
-	return a.withDO(a.DO.Clauses(conds...))
+func (u userDo) Clauses(conds ...clause.Expression) *userDo {
+	return u.withDO(u.DO.Clauses(conds...))
 }
 }
 
 
-func (a authDo) Returning(value interface{}, columns ...string) *authDo {
-	return a.withDO(a.DO.Returning(value, columns...))
+func (u userDo) Returning(value interface{}, columns ...string) *userDo {
+	return u.withDO(u.DO.Returning(value, columns...))
 }
 }
 
 
-func (a authDo) Not(conds ...gen.Condition) *authDo {
-	return a.withDO(a.DO.Not(conds...))
+func (u userDo) Not(conds ...gen.Condition) *userDo {
+	return u.withDO(u.DO.Not(conds...))
 }
 }
 
 
-func (a authDo) Or(conds ...gen.Condition) *authDo {
-	return a.withDO(a.DO.Or(conds...))
+func (u userDo) Or(conds ...gen.Condition) *userDo {
+	return u.withDO(u.DO.Or(conds...))
 }
 }
 
 
-func (a authDo) Select(conds ...field.Expr) *authDo {
-	return a.withDO(a.DO.Select(conds...))
+func (u userDo) Select(conds ...field.Expr) *userDo {
+	return u.withDO(u.DO.Select(conds...))
 }
 }
 
 
-func (a authDo) Where(conds ...gen.Condition) *authDo {
-	return a.withDO(a.DO.Where(conds...))
+func (u userDo) Where(conds ...gen.Condition) *userDo {
+	return u.withDO(u.DO.Where(conds...))
 }
 }
 
 
-func (a authDo) Order(conds ...field.Expr) *authDo {
-	return a.withDO(a.DO.Order(conds...))
+func (u userDo) Order(conds ...field.Expr) *userDo {
+	return u.withDO(u.DO.Order(conds...))
 }
 }
 
 
-func (a authDo) Distinct(cols ...field.Expr) *authDo {
-	return a.withDO(a.DO.Distinct(cols...))
+func (u userDo) Distinct(cols ...field.Expr) *userDo {
+	return u.withDO(u.DO.Distinct(cols...))
 }
 }
 
 
-func (a authDo) Omit(cols ...field.Expr) *authDo {
-	return a.withDO(a.DO.Omit(cols...))
+func (u userDo) Omit(cols ...field.Expr) *userDo {
+	return u.withDO(u.DO.Omit(cols...))
 }
 }
 
 
-func (a authDo) Join(table schema.Tabler, on ...field.Expr) *authDo {
-	return a.withDO(a.DO.Join(table, on...))
+func (u userDo) Join(table schema.Tabler, on ...field.Expr) *userDo {
+	return u.withDO(u.DO.Join(table, on...))
 }
 }
 
 
-func (a authDo) LeftJoin(table schema.Tabler, on ...field.Expr) *authDo {
-	return a.withDO(a.DO.LeftJoin(table, on...))
+func (u userDo) LeftJoin(table schema.Tabler, on ...field.Expr) *userDo {
+	return u.withDO(u.DO.LeftJoin(table, on...))
 }
 }
 
 
-func (a authDo) RightJoin(table schema.Tabler, on ...field.Expr) *authDo {
-	return a.withDO(a.DO.RightJoin(table, on...))
+func (u userDo) RightJoin(table schema.Tabler, on ...field.Expr) *userDo {
+	return u.withDO(u.DO.RightJoin(table, on...))
 }
 }
 
 
-func (a authDo) Group(cols ...field.Expr) *authDo {
-	return a.withDO(a.DO.Group(cols...))
+func (u userDo) Group(cols ...field.Expr) *userDo {
+	return u.withDO(u.DO.Group(cols...))
 }
 }
 
 
-func (a authDo) Having(conds ...gen.Condition) *authDo {
-	return a.withDO(a.DO.Having(conds...))
+func (u userDo) Having(conds ...gen.Condition) *userDo {
+	return u.withDO(u.DO.Having(conds...))
 }
 }
 
 
-func (a authDo) Limit(limit int) *authDo {
-	return a.withDO(a.DO.Limit(limit))
+func (u userDo) Limit(limit int) *userDo {
+	return u.withDO(u.DO.Limit(limit))
 }
 }
 
 
-func (a authDo) Offset(offset int) *authDo {
-	return a.withDO(a.DO.Offset(offset))
+func (u userDo) Offset(offset int) *userDo {
+	return u.withDO(u.DO.Offset(offset))
 }
 }
 
 
-func (a authDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *authDo {
-	return a.withDO(a.DO.Scopes(funcs...))
+func (u userDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *userDo {
+	return u.withDO(u.DO.Scopes(funcs...))
 }
 }
 
 
-func (a authDo) Unscoped() *authDo {
-	return a.withDO(a.DO.Unscoped())
+func (u userDo) Unscoped() *userDo {
+	return u.withDO(u.DO.Unscoped())
 }
 }
 
 
-func (a authDo) Create(values ...*model.Auth) error {
+func (u userDo) Create(values ...*model.User) error {
 	if len(values) == 0 {
 	if len(values) == 0 {
 		return nil
 		return nil
 	}
 	}
-	return a.DO.Create(values)
+	return u.DO.Create(values)
 }
 }
 
 
-func (a authDo) CreateInBatches(values []*model.Auth, batchSize int) error {
-	return a.DO.CreateInBatches(values, batchSize)
+func (u userDo) CreateInBatches(values []*model.User, batchSize int) error {
+	return u.DO.CreateInBatches(values, batchSize)
 }
 }
 
 
 // Save : !!! underlying implementation is different with GORM
 // Save : !!! underlying implementation is different with GORM
 // The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
 // The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
-func (a authDo) Save(values ...*model.Auth) error {
+func (u userDo) Save(values ...*model.User) error {
 	if len(values) == 0 {
 	if len(values) == 0 {
 		return nil
 		return nil
 	}
 	}
-	return a.DO.Save(values)
+	return u.DO.Save(values)
 }
 }
 
 
-func (a authDo) First() (*model.Auth, error) {
-	if result, err := a.DO.First(); err != nil {
+func (u userDo) First() (*model.User, error) {
+	if result, err := u.DO.First(); err != nil {
 		return nil, err
 		return nil, err
 	} else {
 	} else {
-		return result.(*model.Auth), nil
+		return result.(*model.User), nil
 	}
 	}
 }
 }
 
 
-func (a authDo) Take() (*model.Auth, error) {
-	if result, err := a.DO.Take(); err != nil {
+func (u userDo) Take() (*model.User, error) {
+	if result, err := u.DO.Take(); err != nil {
 		return nil, err
 		return nil, err
 	} else {
 	} else {
-		return result.(*model.Auth), nil
+		return result.(*model.User), nil
 	}
 	}
 }
 }
 
 
-func (a authDo) Last() (*model.Auth, error) {
-	if result, err := a.DO.Last(); err != nil {
+func (u userDo) Last() (*model.User, error) {
+	if result, err := u.DO.Last(); err != nil {
 		return nil, err
 		return nil, err
 	} else {
 	} else {
-		return result.(*model.Auth), nil
+		return result.(*model.User), nil
 	}
 	}
 }
 }
 
 
-func (a authDo) Find() ([]*model.Auth, error) {
-	result, err := a.DO.Find()
-	return result.([]*model.Auth), err
+func (u userDo) Find() ([]*model.User, error) {
+	result, err := u.DO.Find()
+	return result.([]*model.User), err
 }
 }
 
 
-func (a authDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Auth, err error) {
-	buf := make([]*model.Auth, 0, batchSize)
-	err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+func (u userDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.User, err error) {
+	buf := make([]*model.User, 0, batchSize)
+	err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
 		defer func() { results = append(results, buf...) }()
 		defer func() { results = append(results, buf...) }()
 		return fc(tx, batch)
 		return fc(tx, batch)
 	})
 	})
 	return results, err
 	return results, err
 }
 }
 
 
-func (a authDo) FindInBatches(result *[]*model.Auth, batchSize int, fc func(tx gen.Dao, batch int) error) error {
-	return a.DO.FindInBatches(result, batchSize, fc)
+func (u userDo) FindInBatches(result *[]*model.User, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return u.DO.FindInBatches(result, batchSize, fc)
 }
 }
 
 
-func (a authDo) Attrs(attrs ...field.AssignExpr) *authDo {
-	return a.withDO(a.DO.Attrs(attrs...))
+func (u userDo) Attrs(attrs ...field.AssignExpr) *userDo {
+	return u.withDO(u.DO.Attrs(attrs...))
 }
 }
 
 
-func (a authDo) Assign(attrs ...field.AssignExpr) *authDo {
-	return a.withDO(a.DO.Assign(attrs...))
+func (u userDo) Assign(attrs ...field.AssignExpr) *userDo {
+	return u.withDO(u.DO.Assign(attrs...))
 }
 }
 
 
-func (a authDo) Joins(fields ...field.RelationField) *authDo {
+func (u userDo) Joins(fields ...field.RelationField) *userDo {
 	for _, _f := range fields {
 	for _, _f := range fields {
-		a = *a.withDO(a.DO.Joins(_f))
+		u = *u.withDO(u.DO.Joins(_f))
 	}
 	}
-	return &a
+	return &u
 }
 }
 
 
-func (a authDo) Preload(fields ...field.RelationField) *authDo {
+func (u userDo) Preload(fields ...field.RelationField) *userDo {
 	for _, _f := range fields {
 	for _, _f := range fields {
-		a = *a.withDO(a.DO.Preload(_f))
+		u = *u.withDO(u.DO.Preload(_f))
 	}
 	}
-	return &a
+	return &u
 }
 }
 
 
-func (a authDo) FirstOrInit() (*model.Auth, error) {
-	if result, err := a.DO.FirstOrInit(); err != nil {
+func (u userDo) FirstOrInit() (*model.User, error) {
+	if result, err := u.DO.FirstOrInit(); err != nil {
 		return nil, err
 		return nil, err
 	} else {
 	} else {
-		return result.(*model.Auth), nil
+		return result.(*model.User), nil
 	}
 	}
 }
 }
 
 
-func (a authDo) FirstOrCreate() (*model.Auth, error) {
-	if result, err := a.DO.FirstOrCreate(); err != nil {
+func (u userDo) FirstOrCreate() (*model.User, error) {
+	if result, err := u.DO.FirstOrCreate(); err != nil {
 		return nil, err
 		return nil, err
 	} else {
 	} else {
-		return result.(*model.Auth), nil
+		return result.(*model.User), nil
 	}
 	}
 }
 }
 
 
-func (a authDo) FindByPage(offset int, limit int) (result []*model.Auth, count int64, err error) {
-	result, err = a.Offset(offset).Limit(limit).Find()
+func (u userDo) FindByPage(offset int, limit int) (result []*model.User, count int64, err error) {
+	result, err = u.Offset(offset).Limit(limit).Find()
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -350,29 +350,29 @@ func (a authDo) FindByPage(offset int, limit int) (result []*model.Auth, count i
 		return
 		return
 	}
 	}
 
 
-	count, err = a.Offset(-1).Limit(-1).Count()
+	count, err = u.Offset(-1).Limit(-1).Count()
 	return
 	return
 }
 }
 
 
-func (a authDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
-	count, err = a.Count()
+func (u userDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = u.Count()
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
 
 
-	err = a.Offset(offset).Limit(limit).Scan(result)
+	err = u.Offset(offset).Limit(limit).Scan(result)
 	return
 	return
 }
 }
 
 
-func (a authDo) Scan(result interface{}) (err error) {
-	return a.DO.Scan(result)
+func (u userDo) Scan(result interface{}) (err error) {
+	return u.DO.Scan(result)
 }
 }
 
 
-func (a authDo) Delete(models ...*model.Auth) (result gen.ResultInfo, err error) {
-	return a.DO.Delete(models)
+func (u userDo) Delete(models ...*model.User) (result gen.ResultInfo, err error) {
+	return u.DO.Delete(models)
 }
 }
 
 
-func (a *authDo) withDO(do gen.Dao) *authDo {
-	a.DO = *do.(*gen.DO)
-	return a
+func (u *userDo) withDO(do gen.Dao) *userDo {
+	u.DO = *do.(*gen.DO)
+	return u
 }
 }

+ 16 - 8
query/gen.go

@@ -18,7 +18,6 @@ import (
 var (
 var (
 	Q             = new(Query)
 	Q             = new(Query)
 	AcmeUser      *acmeUser
 	AcmeUser      *acmeUser
-	Auth          *auth
 	AuthToken     *authToken
 	AuthToken     *authToken
 	BanIP         *banIP
 	BanIP         *banIP
 	Cert          *cert
 	Cert          *cert
@@ -28,14 +27,15 @@ var (
 	DnsCredential *dnsCredential
 	DnsCredential *dnsCredential
 	Environment   *environment
 	Environment   *environment
 	Notification  *notification
 	Notification  *notification
+	Passkey       *passkey
 	Site          *site
 	Site          *site
 	Stream        *stream
 	Stream        *stream
+	User          *user
 )
 )
 
 
 func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	*Q = *Use(db, opts...)
 	*Q = *Use(db, opts...)
 	AcmeUser = &Q.AcmeUser
 	AcmeUser = &Q.AcmeUser
-	Auth = &Q.Auth
 	AuthToken = &Q.AuthToken
 	AuthToken = &Q.AuthToken
 	BanIP = &Q.BanIP
 	BanIP = &Q.BanIP
 	Cert = &Q.Cert
 	Cert = &Q.Cert
@@ -45,15 +45,16 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	DnsCredential = &Q.DnsCredential
 	DnsCredential = &Q.DnsCredential
 	Environment = &Q.Environment
 	Environment = &Q.Environment
 	Notification = &Q.Notification
 	Notification = &Q.Notification
+	Passkey = &Q.Passkey
 	Site = &Q.Site
 	Site = &Q.Site
 	Stream = &Q.Stream
 	Stream = &Q.Stream
+	User = &Q.User
 }
 }
 
 
 func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 	return &Query{
 	return &Query{
 		db:            db,
 		db:            db,
 		AcmeUser:      newAcmeUser(db, opts...),
 		AcmeUser:      newAcmeUser(db, opts...),
-		Auth:          newAuth(db, opts...),
 		AuthToken:     newAuthToken(db, opts...),
 		AuthToken:     newAuthToken(db, opts...),
 		BanIP:         newBanIP(db, opts...),
 		BanIP:         newBanIP(db, opts...),
 		Cert:          newCert(db, opts...),
 		Cert:          newCert(db, opts...),
@@ -63,8 +64,10 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		DnsCredential: newDnsCredential(db, opts...),
 		DnsCredential: newDnsCredential(db, opts...),
 		Environment:   newEnvironment(db, opts...),
 		Environment:   newEnvironment(db, opts...),
 		Notification:  newNotification(db, opts...),
 		Notification:  newNotification(db, opts...),
+		Passkey:       newPasskey(db, opts...),
 		Site:          newSite(db, opts...),
 		Site:          newSite(db, opts...),
 		Stream:        newStream(db, opts...),
 		Stream:        newStream(db, opts...),
+		User:          newUser(db, opts...),
 	}
 	}
 }
 }
 
 
@@ -72,7 +75,6 @@ type Query struct {
 	db *gorm.DB
 	db *gorm.DB
 
 
 	AcmeUser      acmeUser
 	AcmeUser      acmeUser
-	Auth          auth
 	AuthToken     authToken
 	AuthToken     authToken
 	BanIP         banIP
 	BanIP         banIP
 	Cert          cert
 	Cert          cert
@@ -82,8 +84,10 @@ type Query struct {
 	DnsCredential dnsCredential
 	DnsCredential dnsCredential
 	Environment   environment
 	Environment   environment
 	Notification  notification
 	Notification  notification
+	Passkey       passkey
 	Site          site
 	Site          site
 	Stream        stream
 	Stream        stream
+	User          user
 }
 }
 
 
 func (q *Query) Available() bool { return q.db != nil }
 func (q *Query) Available() bool { return q.db != nil }
@@ -92,7 +96,6 @@ func (q *Query) clone(db *gorm.DB) *Query {
 	return &Query{
 	return &Query{
 		db:            db,
 		db:            db,
 		AcmeUser:      q.AcmeUser.clone(db),
 		AcmeUser:      q.AcmeUser.clone(db),
-		Auth:          q.Auth.clone(db),
 		AuthToken:     q.AuthToken.clone(db),
 		AuthToken:     q.AuthToken.clone(db),
 		BanIP:         q.BanIP.clone(db),
 		BanIP:         q.BanIP.clone(db),
 		Cert:          q.Cert.clone(db),
 		Cert:          q.Cert.clone(db),
@@ -102,8 +105,10 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		DnsCredential: q.DnsCredential.clone(db),
 		DnsCredential: q.DnsCredential.clone(db),
 		Environment:   q.Environment.clone(db),
 		Environment:   q.Environment.clone(db),
 		Notification:  q.Notification.clone(db),
 		Notification:  q.Notification.clone(db),
+		Passkey:       q.Passkey.clone(db),
 		Site:          q.Site.clone(db),
 		Site:          q.Site.clone(db),
 		Stream:        q.Stream.clone(db),
 		Stream:        q.Stream.clone(db),
+		User:          q.User.clone(db),
 	}
 	}
 }
 }
 
 
@@ -119,7 +124,6 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 	return &Query{
 	return &Query{
 		db:            db,
 		db:            db,
 		AcmeUser:      q.AcmeUser.replaceDB(db),
 		AcmeUser:      q.AcmeUser.replaceDB(db),
-		Auth:          q.Auth.replaceDB(db),
 		AuthToken:     q.AuthToken.replaceDB(db),
 		AuthToken:     q.AuthToken.replaceDB(db),
 		BanIP:         q.BanIP.replaceDB(db),
 		BanIP:         q.BanIP.replaceDB(db),
 		Cert:          q.Cert.replaceDB(db),
 		Cert:          q.Cert.replaceDB(db),
@@ -129,14 +133,15 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		DnsCredential: q.DnsCredential.replaceDB(db),
 		DnsCredential: q.DnsCredential.replaceDB(db),
 		Environment:   q.Environment.replaceDB(db),
 		Environment:   q.Environment.replaceDB(db),
 		Notification:  q.Notification.replaceDB(db),
 		Notification:  q.Notification.replaceDB(db),
+		Passkey:       q.Passkey.replaceDB(db),
 		Site:          q.Site.replaceDB(db),
 		Site:          q.Site.replaceDB(db),
 		Stream:        q.Stream.replaceDB(db),
 		Stream:        q.Stream.replaceDB(db),
+		User:          q.User.replaceDB(db),
 	}
 	}
 }
 }
 
 
 type queryCtx struct {
 type queryCtx struct {
 	AcmeUser      *acmeUserDo
 	AcmeUser      *acmeUserDo
-	Auth          *authDo
 	AuthToken     *authTokenDo
 	AuthToken     *authTokenDo
 	BanIP         *banIPDo
 	BanIP         *banIPDo
 	Cert          *certDo
 	Cert          *certDo
@@ -146,14 +151,15 @@ type queryCtx struct {
 	DnsCredential *dnsCredentialDo
 	DnsCredential *dnsCredentialDo
 	Environment   *environmentDo
 	Environment   *environmentDo
 	Notification  *notificationDo
 	Notification  *notificationDo
+	Passkey       *passkeyDo
 	Site          *siteDo
 	Site          *siteDo
 	Stream        *streamDo
 	Stream        *streamDo
+	User          *userDo
 }
 }
 
 
 func (q *Query) WithContext(ctx context.Context) *queryCtx {
 func (q *Query) WithContext(ctx context.Context) *queryCtx {
 	return &queryCtx{
 	return &queryCtx{
 		AcmeUser:      q.AcmeUser.WithContext(ctx),
 		AcmeUser:      q.AcmeUser.WithContext(ctx),
-		Auth:          q.Auth.WithContext(ctx),
 		AuthToken:     q.AuthToken.WithContext(ctx),
 		AuthToken:     q.AuthToken.WithContext(ctx),
 		BanIP:         q.BanIP.WithContext(ctx),
 		BanIP:         q.BanIP.WithContext(ctx),
 		Cert:          q.Cert.WithContext(ctx),
 		Cert:          q.Cert.WithContext(ctx),
@@ -163,8 +169,10 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		DnsCredential: q.DnsCredential.WithContext(ctx),
 		DnsCredential: q.DnsCredential.WithContext(ctx),
 		Environment:   q.Environment.WithContext(ctx),
 		Environment:   q.Environment.WithContext(ctx),
 		Notification:  q.Notification.WithContext(ctx),
 		Notification:  q.Notification.WithContext(ctx),
+		Passkey:       q.Passkey.WithContext(ctx),
 		Site:          q.Site.WithContext(ctx),
 		Site:          q.Site.WithContext(ctx),
 		Stream:        q.Stream.WithContext(ctx),
 		Stream:        q.Stream.WithContext(ctx),
+		User:          q.User.WithContext(ctx),
 	}
 	}
 }
 }
 
 

+ 382 - 0
query/passkeys.gen.go

@@ -0,0 +1,382 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/model"
+)
+
+func newPasskey(db *gorm.DB, opts ...gen.DOOption) passkey {
+	_passkey := passkey{}
+
+	_passkey.passkeyDo.UseDB(db, opts...)
+	_passkey.passkeyDo.UseModel(&model.Passkey{})
+
+	tableName := _passkey.passkeyDo.TableName()
+	_passkey.ALL = field.NewAsterisk(tableName)
+	_passkey.ID = field.NewInt(tableName, "id")
+	_passkey.CreatedAt = field.NewTime(tableName, "created_at")
+	_passkey.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_passkey.DeletedAt = field.NewField(tableName, "deleted_at")
+	_passkey.Name = field.NewString(tableName, "name")
+	_passkey.UserID = field.NewInt(tableName, "user_id")
+	_passkey.RawID = field.NewString(tableName, "raw_id")
+	_passkey.Credential = field.NewField(tableName, "credential")
+	_passkey.LastUsedAt = field.NewInt64(tableName, "last_used_at")
+
+	_passkey.fillFieldMap()
+
+	return _passkey
+}
+
+type passkey struct {
+	passkeyDo
+
+	ALL        field.Asterisk
+	ID         field.Int
+	CreatedAt  field.Time
+	UpdatedAt  field.Time
+	DeletedAt  field.Field
+	Name       field.String
+	UserID     field.Int
+	RawID      field.String
+	Credential field.Field
+	LastUsedAt field.Int64
+
+	fieldMap map[string]field.Expr
+}
+
+func (p passkey) Table(newTableName string) *passkey {
+	p.passkeyDo.UseTable(newTableName)
+	return p.updateTableName(newTableName)
+}
+
+func (p passkey) As(alias string) *passkey {
+	p.passkeyDo.DO = *(p.passkeyDo.As(alias).(*gen.DO))
+	return p.updateTableName(alias)
+}
+
+func (p *passkey) updateTableName(table string) *passkey {
+	p.ALL = field.NewAsterisk(table)
+	p.ID = field.NewInt(table, "id")
+	p.CreatedAt = field.NewTime(table, "created_at")
+	p.UpdatedAt = field.NewTime(table, "updated_at")
+	p.DeletedAt = field.NewField(table, "deleted_at")
+	p.Name = field.NewString(table, "name")
+	p.UserID = field.NewInt(table, "user_id")
+	p.RawID = field.NewString(table, "raw_id")
+	p.Credential = field.NewField(table, "credential")
+	p.LastUsedAt = field.NewInt64(table, "last_used_at")
+
+	p.fillFieldMap()
+
+	return p
+}
+
+func (p *passkey) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := p.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (p *passkey) fillFieldMap() {
+	p.fieldMap = make(map[string]field.Expr, 9)
+	p.fieldMap["id"] = p.ID
+	p.fieldMap["created_at"] = p.CreatedAt
+	p.fieldMap["updated_at"] = p.UpdatedAt
+	p.fieldMap["deleted_at"] = p.DeletedAt
+	p.fieldMap["name"] = p.Name
+	p.fieldMap["user_id"] = p.UserID
+	p.fieldMap["raw_id"] = p.RawID
+	p.fieldMap["credential"] = p.Credential
+	p.fieldMap["last_used_at"] = p.LastUsedAt
+}
+
+func (p passkey) clone(db *gorm.DB) passkey {
+	p.passkeyDo.ReplaceConnPool(db.Statement.ConnPool)
+	return p
+}
+
+func (p passkey) replaceDB(db *gorm.DB) passkey {
+	p.passkeyDo.ReplaceDB(db)
+	return p
+}
+
+type passkeyDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (p passkeyDo) FirstByID(id int) (result *model.Passkey, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = p.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
+func (p passkeyDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update passkeys set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = p.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (p passkeyDo) Debug() *passkeyDo {
+	return p.withDO(p.DO.Debug())
+}
+
+func (p passkeyDo) WithContext(ctx context.Context) *passkeyDo {
+	return p.withDO(p.DO.WithContext(ctx))
+}
+
+func (p passkeyDo) ReadDB() *passkeyDo {
+	return p.Clauses(dbresolver.Read)
+}
+
+func (p passkeyDo) WriteDB() *passkeyDo {
+	return p.Clauses(dbresolver.Write)
+}
+
+func (p passkeyDo) Session(config *gorm.Session) *passkeyDo {
+	return p.withDO(p.DO.Session(config))
+}
+
+func (p passkeyDo) Clauses(conds ...clause.Expression) *passkeyDo {
+	return p.withDO(p.DO.Clauses(conds...))
+}
+
+func (p passkeyDo) Returning(value interface{}, columns ...string) *passkeyDo {
+	return p.withDO(p.DO.Returning(value, columns...))
+}
+
+func (p passkeyDo) Not(conds ...gen.Condition) *passkeyDo {
+	return p.withDO(p.DO.Not(conds...))
+}
+
+func (p passkeyDo) Or(conds ...gen.Condition) *passkeyDo {
+	return p.withDO(p.DO.Or(conds...))
+}
+
+func (p passkeyDo) Select(conds ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.Select(conds...))
+}
+
+func (p passkeyDo) Where(conds ...gen.Condition) *passkeyDo {
+	return p.withDO(p.DO.Where(conds...))
+}
+
+func (p passkeyDo) Order(conds ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.Order(conds...))
+}
+
+func (p passkeyDo) Distinct(cols ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.Distinct(cols...))
+}
+
+func (p passkeyDo) Omit(cols ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.Omit(cols...))
+}
+
+func (p passkeyDo) Join(table schema.Tabler, on ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.Join(table, on...))
+}
+
+func (p passkeyDo) LeftJoin(table schema.Tabler, on ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.LeftJoin(table, on...))
+}
+
+func (p passkeyDo) RightJoin(table schema.Tabler, on ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.RightJoin(table, on...))
+}
+
+func (p passkeyDo) Group(cols ...field.Expr) *passkeyDo {
+	return p.withDO(p.DO.Group(cols...))
+}
+
+func (p passkeyDo) Having(conds ...gen.Condition) *passkeyDo {
+	return p.withDO(p.DO.Having(conds...))
+}
+
+func (p passkeyDo) Limit(limit int) *passkeyDo {
+	return p.withDO(p.DO.Limit(limit))
+}
+
+func (p passkeyDo) Offset(offset int) *passkeyDo {
+	return p.withDO(p.DO.Offset(offset))
+}
+
+func (p passkeyDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *passkeyDo {
+	return p.withDO(p.DO.Scopes(funcs...))
+}
+
+func (p passkeyDo) Unscoped() *passkeyDo {
+	return p.withDO(p.DO.Unscoped())
+}
+
+func (p passkeyDo) Create(values ...*model.Passkey) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return p.DO.Create(values)
+}
+
+func (p passkeyDo) CreateInBatches(values []*model.Passkey, batchSize int) error {
+	return p.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (p passkeyDo) Save(values ...*model.Passkey) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return p.DO.Save(values)
+}
+
+func (p passkeyDo) First() (*model.Passkey, error) {
+	if result, err := p.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Passkey), nil
+	}
+}
+
+func (p passkeyDo) Take() (*model.Passkey, error) {
+	if result, err := p.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Passkey), nil
+	}
+}
+
+func (p passkeyDo) Last() (*model.Passkey, error) {
+	if result, err := p.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Passkey), nil
+	}
+}
+
+func (p passkeyDo) Find() ([]*model.Passkey, error) {
+	result, err := p.DO.Find()
+	return result.([]*model.Passkey), err
+}
+
+func (p passkeyDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Passkey, err error) {
+	buf := make([]*model.Passkey, 0, batchSize)
+	err = p.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (p passkeyDo) FindInBatches(result *[]*model.Passkey, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return p.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (p passkeyDo) Attrs(attrs ...field.AssignExpr) *passkeyDo {
+	return p.withDO(p.DO.Attrs(attrs...))
+}
+
+func (p passkeyDo) Assign(attrs ...field.AssignExpr) *passkeyDo {
+	return p.withDO(p.DO.Assign(attrs...))
+}
+
+func (p passkeyDo) Joins(fields ...field.RelationField) *passkeyDo {
+	for _, _f := range fields {
+		p = *p.withDO(p.DO.Joins(_f))
+	}
+	return &p
+}
+
+func (p passkeyDo) Preload(fields ...field.RelationField) *passkeyDo {
+	for _, _f := range fields {
+		p = *p.withDO(p.DO.Preload(_f))
+	}
+	return &p
+}
+
+func (p passkeyDo) FirstOrInit() (*model.Passkey, error) {
+	if result, err := p.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Passkey), nil
+	}
+}
+
+func (p passkeyDo) FirstOrCreate() (*model.Passkey, error) {
+	if result, err := p.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Passkey), nil
+	}
+}
+
+func (p passkeyDo) FindByPage(offset int, limit int) (result []*model.Passkey, count int64, err error) {
+	result, err = p.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = p.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (p passkeyDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = p.Count()
+	if err != nil {
+		return
+	}
+
+	err = p.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (p passkeyDo) Scan(result interface{}) (err error) {
+	return p.DO.Scan(result)
+}
+
+func (p passkeyDo) Delete(models ...*model.Passkey) (result gen.ResultInfo, err error) {
+	return p.DO.Delete(models)
+}
+
+func (p *passkeyDo) withDO(do gen.Dao) *passkeyDo {
+	p.DO = *do.(*gen.DO)
+	return p
+}

+ 71 - 71
router/routers.go

@@ -1,83 +1,83 @@
 package router
 package router
 
 
 import (
 import (
-    "github.com/0xJacky/Nginx-UI/api/analytic"
-    "github.com/0xJacky/Nginx-UI/api/certificate"
-    "github.com/0xJacky/Nginx-UI/api/cluster"
-    "github.com/0xJacky/Nginx-UI/api/config"
-    "github.com/0xJacky/Nginx-UI/api/nginx"
-    "github.com/0xJacky/Nginx-UI/api/notification"
-    "github.com/0xJacky/Nginx-UI/api/openai"
-    "github.com/0xJacky/Nginx-UI/api/settings"
-    "github.com/0xJacky/Nginx-UI/api/sites"
-    "github.com/0xJacky/Nginx-UI/api/streams"
-    "github.com/0xJacky/Nginx-UI/api/system"
-    "github.com/0xJacky/Nginx-UI/api/template"
-    "github.com/0xJacky/Nginx-UI/api/terminal"
-    "github.com/0xJacky/Nginx-UI/api/upstream"
-    "github.com/0xJacky/Nginx-UI/api/user"
-    "github.com/0xJacky/Nginx-UI/internal/middleware"
-    "github.com/gin-contrib/static"
-    "github.com/gin-gonic/gin"
-    "net/http"
+	"github.com/0xJacky/Nginx-UI/api/analytic"
+	"github.com/0xJacky/Nginx-UI/api/certificate"
+	"github.com/0xJacky/Nginx-UI/api/cluster"
+	"github.com/0xJacky/Nginx-UI/api/config"
+	"github.com/0xJacky/Nginx-UI/api/nginx"
+	"github.com/0xJacky/Nginx-UI/api/notification"
+	"github.com/0xJacky/Nginx-UI/api/openai"
+	"github.com/0xJacky/Nginx-UI/api/settings"
+	"github.com/0xJacky/Nginx-UI/api/sites"
+	"github.com/0xJacky/Nginx-UI/api/streams"
+	"github.com/0xJacky/Nginx-UI/api/system"
+	"github.com/0xJacky/Nginx-UI/api/template"
+	"github.com/0xJacky/Nginx-UI/api/terminal"
+	"github.com/0xJacky/Nginx-UI/api/upstream"
+	"github.com/0xJacky/Nginx-UI/api/user"
+	"github.com/0xJacky/Nginx-UI/internal/middleware"
+	"github.com/gin-contrib/static"
+	"github.com/gin-gonic/gin"
+	"net/http"
 )
 )
 
 
 func InitRouter() *gin.Engine {
 func InitRouter() *gin.Engine {
-    r := gin.New()
-    r.Use(
-        gin.Logger(),
-        middleware.Recovery(),
-        middleware.CacheJs(),
-        middleware.IPWhiteList(),
-        static.Serve("/", middleware.MustFs("")),
-    )
+	r := gin.New()
+	r.Use(
+		gin.Logger(),
+		middleware.Recovery(),
+		middleware.CacheJs(),
+		middleware.IPWhiteList(),
+		static.Serve("/", middleware.MustFs("")),
+	)
 
 
-    r.NoRoute(func(c *gin.Context) {
-        c.JSON(http.StatusNotFound, gin.H{
-            "message": "not found",
-        })
-    })
+	r.NoRoute(func(c *gin.Context) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "not found",
+		})
+	})
 
 
-    root := r.Group("/api")
-    {
-        system.InitPublicRouter(root)
-        user.InitAuthRouter(root)
+	root := r.Group("/api")
+	{
+		system.InitPublicRouter(root)
+		user.InitAuthRouter(root)
 
 
-        // Authorization required not websocket request
-        g := root.Group("/", middleware.AuthRequired(), middleware.Proxy())
-        {
-            user.InitUserRouter(g)
-            analytic.InitRouter(g)
-            user.InitManageUserRouter(g)
-            nginx.InitRouter(g)
-            sites.InitRouter(g)
-            streams.InitRouter(g)
-            config.InitRouter(g)
-            template.InitRouter(g)
-            certificate.InitCertificateRouter(g)
-            certificate.InitDNSCredentialRouter(g)
-            certificate.InitAcmeUserRouter(g)
-            system.InitPrivateRouter(g)
-            settings.InitRouter(g)
-            openai.InitRouter(g)
-            cluster.InitRouter(g)
-            notification.InitRouter(g)
-        }
+		// Authorization required and not websocket request
+		g := root.Group("/", middleware.AuthRequired(), middleware.Proxy())
+		{
+			user.InitUserRouter(g)
+			analytic.InitRouter(g)
+			user.InitManageUserRouter(g)
+			nginx.InitRouter(g)
+			sites.InitRouter(g)
+			streams.InitRouter(g)
+			config.InitRouter(g)
+			template.InitRouter(g)
+			certificate.InitCertificateRouter(g)
+			certificate.InitDNSCredentialRouter(g)
+			certificate.InitAcmeUserRouter(g)
+			system.InitPrivateRouter(g)
+			settings.InitRouter(g)
+			openai.InitRouter(g)
+			cluster.InitRouter(g)
+			notification.InitRouter(g)
+		}
 
 
-        // Authorization required and websocket request
-        w := root.Group("/", middleware.AuthRequired(), middleware.ProxyWs())
-        {
-            analytic.InitWebSocketRouter(w)
-            certificate.InitCertificateWebSocketRouter(w)
-            o := w.Group("", middleware.RequireSecureSession())
-            {
-                terminal.InitRouter(o)
-            }
-            nginx.InitNginxLogRouter(w)
-            upstream.InitRouter(w)
-            system.InitWebSocketRouter(w)
-        }
-    }
+		// Authorization required and websocket request
+		w := root.Group("/", middleware.AuthRequired(), middleware.ProxyWs())
+		{
+			analytic.InitWebSocketRouter(w)
+			certificate.InitCertificateWebSocketRouter(w)
+			o := w.Group("", middleware.RequireSecureSession())
+			{
+				terminal.InitRouter(o)
+			}
+			nginx.InitNginxLogRouter(w)
+			upstream.InitRouter(w)
+			system.InitWebSocketRouter(w)
+		}
+	}
 
 
-    return r
+	return r
 }
 }

+ 1 - 1
settings/auth.go

@@ -1,7 +1,7 @@
 package settings
 package settings
 
 
 type Auth struct {
 type Auth struct {
-	IPWhiteList         []string `json:"ip_white_list" binding:"omitempty,dive,ip" ini:",,allowshadow"`
+	IPWhiteList         []string `json:"ip_white_list" binding:"omitempty,dive,ip" ini:",,allowshadow" protected:"true"`
 	BanThresholdMinutes int      `json:"ban_threshold_minutes" binding:"min=1"`
 	BanThresholdMinutes int      `json:"ban_threshold_minutes" binding:"min=1"`
 	MaxAttempts         int      `json:"max_attempts" binding:"min=1"`
 	MaxAttempts         int      `json:"max_attempts" binding:"min=1"`
 }
 }

+ 7 - 0
settings/settings.go

@@ -29,6 +29,7 @@ var sections = map[string]interface{}{
 	"cluster":   &ClusterSettings,
 	"cluster":   &ClusterSettings,
 	"auth":      &AuthSettings,
 	"auth":      &AuthSettings,
 	"crypto":    &CryptoSettings,
 	"crypto":    &CryptoSettings,
+	"webauthn":  &WebAuthnSettings,
 }
 }
 
 
 func init() {
 func init() {
@@ -66,6 +67,7 @@ func Setup() {
 	parseEnv(&LogrotateSettings, "LOGROTATE_")
 	parseEnv(&LogrotateSettings, "LOGROTATE_")
 	parseEnv(&AuthSettings, "AUTH_")
 	parseEnv(&AuthSettings, "AUTH_")
 	parseEnv(&CryptoSettings, "CRYPTO_")
 	parseEnv(&CryptoSettings, "CRYPTO_")
+	parseEnv(&WebAuthnSettings, "WEBAUTHN_")
 
 
 	// if in official docker, set the restart cmd of nginx to "nginx -s stop",
 	// if in official docker, set the restart cmd of nginx to "nginx -s stop",
 	// then the supervisor of s6-overlay will start the nginx again.
 	// then the supervisor of s6-overlay will start the nginx again.
@@ -102,6 +104,11 @@ func Save() (err error) {
 		reflectFrom(k, v)
 		reflectFrom(k, v)
 	}
 	}
 
 
+	// fix unable to save empty slice
+	if len(ServerSettings.RecursiveNameservers) == 0 {
+		Conf.Section("server").Key("RecursiveNameservers").SetValue("")
+	}
+
 	err = Conf.SaveTo(ConfPath)
 	err = Conf.SaveTo(ConfPath)
 	if err != nil {
 	if err != nil {
 		return
 		return

+ 8 - 0
settings/settings_test.go

@@ -53,6 +53,10 @@ func TestSetup(t *testing.T) {
 	_ = os.Setenv("NGINX_UI_LOGROTATE_CMD", "logrotate /custom/logrotate.conf")
 	_ = os.Setenv("NGINX_UI_LOGROTATE_CMD", "logrotate /custom/logrotate.conf")
 	_ = os.Setenv("NGINX_UI_LOGROTATE_INTERVAL", "60")
 	_ = os.Setenv("NGINX_UI_LOGROTATE_INTERVAL", "60")
 
 
+	_ = os.Setenv("NGINX_UI_WEBAUTHN_RP_DISPLAY_NAME", "WebAuthn")
+	_ = os.Setenv("NGINX_UI_WEBAUTHN_RPID", "localhost")
+	_ = os.Setenv("NGINX_UI_WEBAUTHN_RP_ORIGINS", "http://localhost:3002")
+
 	ConfPath = "app.testing.ini"
 	ConfPath = "app.testing.ini"
 	Setup()
 	Setup()
 
 
@@ -98,6 +102,10 @@ func TestSetup(t *testing.T) {
 	assert.Equal(t, "logrotate /custom/logrotate.conf", LogrotateSettings.CMD)
 	assert.Equal(t, "logrotate /custom/logrotate.conf", LogrotateSettings.CMD)
 	assert.Equal(t, 60, LogrotateSettings.Interval)
 	assert.Equal(t, 60, LogrotateSettings.Interval)
 
 
+	assert.Equal(t, "WebAuthn", WebAuthnSettings.RPDisplayName)
+	assert.Equal(t, "localhost", WebAuthnSettings.RPID)
+	assert.Equal(t, []string{"http://localhost:3002"}, WebAuthnSettings.RPOrigins)
+
 	os.Clearenv()
 	os.Clearenv()
 	_ = os.Remove("app.testing.ini")
 	_ = os.Remove("app.testing.ini")
 }
 }

+ 9 - 0
settings/webauthn.go

@@ -0,0 +1,9 @@
+package settings
+
+type WebAuthn struct {
+	RPDisplayName string
+	RPID          string
+	RPOrigins     []string
+}
+
+var WebAuthnSettings = WebAuthn{}