瀏覽代碼

feat: login via passkey

Jacky 7 月之前
父節點
當前提交
bdfbbd0e8f

+ 2 - 2
api/api.go

@@ -12,8 +12,8 @@ import (
 	"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) {

+ 2 - 2
api/system/install.go

@@ -61,8 +61,8 @@ func InstallNginxUI(c *gin.Context) {
 
 	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,
 		Password: string(pwd),
 	})

+ 206 - 206
api/user/otp.go

@@ -1,228 +1,228 @@
 package user
 
 import (
-    "bytes"
-    "crypto/sha1"
-    "encoding/base64"
-    "encoding/hex"
-    "fmt"
-    "github.com/0xJacky/Nginx-UI/api"
-    "github.com/0xJacky/Nginx-UI/internal/crypto"
-    "github.com/0xJacky/Nginx-UI/internal/user"
-    "github.com/0xJacky/Nginx-UI/model"
-    "github.com/0xJacky/Nginx-UI/query"
-    "github.com/0xJacky/Nginx-UI/settings"
-    "github.com/gin-gonic/gin"
-    "github.com/pquerna/otp"
-    "github.com/pquerna/otp/totp"
-    "image/jpeg"
-    "net/http"
-    "strings"
+	"bytes"
+	"crypto/sha1"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/crypto"
+	"github.com/0xJacky/Nginx-UI/internal/user"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/pquerna/otp"
+	"github.com/pquerna/otp/totp"
+	"image/jpeg"
+	"net/http"
+	"strings"
 )
 
 func GenerateTOTP(c *gin.Context) {
-    u := api.CurrentUser(c)
-
-    issuer := fmt.Sprintf("Nginx UI %s", settings.ServerSettings.Name)
-    issuer = strings.TrimSpace(issuer)
-
-    otpOpts := totp.GenerateOpts{
-        Issuer:      issuer,
-        AccountName: u.Name,
-        Period:      30, // seconds
-        Digits:      otp.DigitsSix,
-        Algorithm:   otp.AlgorithmSHA1,
-    }
-    otpKey, err := totp.Generate(otpOpts)
-    if err != nil {
-        api.ErrHandler(c, err)
-        return
-    }
-
-    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) {
-    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) {
-    var json struct {
-        RecoveryCode string `json:"recovery_code"`
-    }
-    if !api.BindAndValid(c, &json) {
-        return
-    }
-    recoverCode, err := hex.DecodeString(json.RecoveryCode)
-    if err != nil {
-        api.ErrHandler(c, err)
-        return
-    }
-    cUser := api.CurrentUser(c)
-    k := sha1.Sum(cUser.OTPSecret)
-    if !bytes.Equal(k[:], recoverCode) {
-        c.JSON(http.StatusBadRequest, gin.H{
-            "message": "Invalid recovery code",
-        })
-        return
-    }
-
-    u := query.Auth
-    _, err = u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null())
-    if err != nil {
-        api.ErrHandler(c, err)
-        return
-    }
-
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	var json struct {
+		RecoveryCode string `json:"recovery_code"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	recoverCode, err := hex.DecodeString(json.RecoveryCode)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	cUser := api.CurrentUser(c)
+	k := sha1.Sum(cUser.OTPSecret)
+	if !bytes.Equal(k[:], recoverCode) {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"message": "Invalid recovery code",
+		})
+		return
+	}
+
+	u := query.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",
+	})
 }
 
 func OTPStatus(c *gin.Context) {
-    status := false
-    u, ok := c.Get("user")
-    if ok {
-        status = u.(*model.Auth).EnabledOTP()
-    }
-    c.JSON(http.StatusOK, gin.H{
-        "status": status,
-    })
+	status := false
+	u, ok := c.Get("user")
+	if ok {
+		status = u.(*model.User).EnabledOTP()
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"status": status,
+	})
 }
 
 func SecureSessionStatus(c *gin.Context) {
-    u, ok := c.Get("user")
-    if !ok || !u.(*model.Auth).EnabledOTP() {
-        c.JSON(http.StatusOK, gin.H{
-            "status": false,
-        })
-        return
-    }
-    ssid := c.GetHeader("X-Secure-Session-ID")
-    if ssid == "" {
-        ssid = c.Query("X-Secure-Session-ID")
-    }
-    if ssid == "" {
-        c.JSON(http.StatusOK, gin.H{
-            "status": false,
-        })
-        return
-    }
-
-    if user.VerifySecureSessionID(ssid, u.(*model.Auth).ID) {
-        c.JSON(http.StatusOK, gin.H{
-            "status": true,
-        })
-        return
-    }
-
-    c.JSON(http.StatusOK, gin.H{
-        "status": false,
-    })
+	u, ok := c.Get("user")
+	if !ok || !u.(*model.User).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.User).ID) {
+		c.JSON(http.StatusOK, gin.H{
+			"status": true,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"status": false,
+	})
 }
 
 func StartSecure2FASession(c *gin.Context) {
-    var json struct {
-        OTP          string `json:"otp"`
-        RecoveryCode string `json:"recovery_code"`
-    }
-    if !api.BindAndValid(c, &json) {
-        return
-    }
-    u := api.CurrentUser(c)
-    if !u.EnabledOTP() {
-        c.JSON(http.StatusBadRequest, gin.H{
-            "message": "User not configured with 2FA",
-        })
-        return
-    }
-
-    if json.OTP == "" && json.RecoveryCode == "" {
-        c.JSON(http.StatusBadRequest, LoginResponse{
-            Message: "The user has enabled 2FA",
-        })
-        return
-    }
-
-    if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
-        c.JSON(http.StatusBadRequest, LoginResponse{
-            Message: "Invalid 2FA or recovery code",
-        })
-        return
-    }
-
-    sessionId := user.SetSecureSessionID(u.ID)
-
-    c.JSON(http.StatusOK, gin.H{
-        "session_id": sessionId,
-    })
+	var json struct {
+		OTP          string `json:"otp"`
+		RecoveryCode string `json:"recovery_code"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	u := api.CurrentUser(c)
+	if !u.EnabledOTP() {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"message": "User not configured with 2FA",
+		})
+		return
+	}
+
+	if json.OTP == "" && json.RecoveryCode == "" {
+		c.JSON(http.StatusBadRequest, LoginResponse{
+			Message: "The user has enabled 2FA",
+		})
+		return
+	}
+
+	if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
+		c.JSON(http.StatusBadRequest, LoginResponse{
+			Message: "Invalid 2FA or recovery code",
+		})
+		return
+	}
+
+	sessionId := user.SetSecureSessionID(u.ID)
+
+	c.JSON(http.StatusOK, gin.H{
+		"session_id": sessionId,
+	})
 }

+ 185 - 0
api/user/passkey.go

@@ -0,0 +1,185 @@
+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 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) {
+	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()
+}

+ 10 - 0
api/user/router.go

@@ -8,6 +8,9 @@ func InitAuthRouter(r *gin.RouterGroup) {
 	r.POST("/login", Login)
 	r.DELETE("/logout", Logout)
 
+	r.GET("/begin_passkey_login", BeginPasskeyLogin)
+	r.POST("/finish_passkey_login", FinishPasskeyLogin)
+
 	r.GET("/casdoor_uri", GetCasdoorUri)
 	r.POST("/casdoor_callback", CasdoorCallback)
 }
@@ -29,4 +32,11 @@ func InitUserRouter(r *gin.RouterGroup) {
 
 	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) {
-	cosy.Core[model.Auth](c).SetFussy("name").PagingList()
+	cosy.Core[model.User](c).SetFussy("name").PagingList()
 }
 
 func GetUser(c *gin.Context) {
 	id := cast.ToInt(c.Param("id"))
 
-	u := query.Auth
+	u := query.User
 
 	user, err := u.FirstByID(id)
 
@@ -43,7 +43,7 @@ func AddUser(c *gin.Context) {
 		return
 	}
 
-	u := query.Auth
+	u := query.User
 
 	pwd, err := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
 	if err != nil {
@@ -52,7 +52,7 @@ func AddUser(c *gin.Context) {
 	}
 	json.Password = string(pwd)
 
-	user := model.Auth{
+	user := model.User{
 		Name:     json.Name,
 		Password: json.Password,
 	}
@@ -84,14 +84,14 @@ func EditUser(c *gin.Context) {
 		return
 	}
 
-	u := query.Auth
+	u := query.User
 	user, err := u.FirstByID(userId)
 
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
-	edit := &model.Auth{
+	edit := &model.User{
 		Name: json.Name,
 	}
 
@@ -124,9 +124,9 @@ func DeleteUser(c *gin.Context) {
 		})
 		return
 	}
-	cosy.Core[model.Auth](c).Destroy()
+	cosy.Core[model.User](c).Destroy()
 }
 
 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 = 
 SkipInstallation     = false
 Name                 = 
+InsecureSkipVerify   = false
 
 [nginx]
 AccessLogPath = /var/log/nginx/access.log
@@ -59,3 +60,8 @@ MaxAttempts         = 10
 
 [crypto]
 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']
     OTPOTPAuthorization: typeof import('./src/components/OTP/OTPAuthorization.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']
     RouterView: typeof import('vue-router')['RouterView']
     SensitiveStringSensitiveString: typeof import('./src/components/SensitiveString/SensitiveString.vue')['default']

+ 2 - 0
app/package.json

@@ -14,6 +14,7 @@
     "@0xjacky/vue-github-button": "^3.1.1",
     "@ant-design/icons-vue": "^7.0.1",
     "@formkit/auto-animate": "^0.8.2",
+    "@simplewebauthn/browser": "^10.0.0",
     "@vue/reactivity": "^3.5.5",
     "@vue/shared": "^3.5.5",
     "@vueuse/components": "^11.0.3",
@@ -46,6 +47,7 @@
   },
   "devDependencies": {
     "@antfu/eslint-config-vue": "^0.43.1",
+    "@simplewebauthn/types": "^10.0.0",
     "@types/lodash": "^4.17.7",
     "@types/nprogress": "^0.2.3",
     "@types/sortablejs": "^1.15.8",

+ 18 - 0
app/pnpm-lock.yaml

@@ -17,6 +17,9 @@ importers:
       '@formkit/auto-animate':
         specifier: ^0.8.2
         version: 0.8.2
+      '@simplewebauthn/browser':
+        specifier: ^10.0.0
+        version: 10.0.0
       '@vue/reactivity':
         specifier: ^3.5.5
         version: 3.5.5
@@ -108,6 +111,9 @@ importers:
       '@antfu/eslint-config-vue':
         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)
+      '@simplewebauthn/types':
+        specifier: ^10.0.0
+        version: 10.0.0
       '@types/lodash':
         specifier: ^4.17.7
         version: 4.17.7
@@ -684,6 +690,12 @@ packages:
   '@simonwep/pickr@1.8.2':
     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':
     resolution: {integrity: sha512-W1rq2xxlFNhgZZJO+L59wtvlDI0xARYxx0WD8EeWNBO7NDybUSYSozCIcY9XvxQbTAsEXBjwqokeYm0crt7RxQ==}
 
@@ -3651,6 +3663,12 @@ snapshots:
       core-js: 3.38.1
       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':
     dependencies:
       acorn: 8.12.1

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

@@ -1,3 +1,4 @@
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types'
 import http from '@/lib/http'
 import { useUserStore } from '@/pinia'
 
@@ -37,6 +38,16 @@ const auth = {
   async get_casdoor_uri(): Promise<{ uri: string }> {
     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

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

@@ -0,0 +1,35 @@
+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
+}
+
+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_passkey_enabled() {
+    return http.get('/passkey_enabled')
+  },
+}
+
+export default passkey

+ 186 - 0
app/src/components/Passkey/PasskeyRegistration.vue

@@ -0,0 +1,186 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue'
+import { startRegistration } from '@simplewebauthn/browser'
+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 { $gettext } from '@/gettext'
+
+dayjs.extend(relativeTime)
+
+const passkeyName = ref('')
+const addPasskeyModelOpen = 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)
+
+    getList()
+
+    message.success($gettext('Register passkey successfully'))
+    addPasskeyModelOpen.value = false
+  }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  catch (e: any) {
+    message.error($gettext(e.message ?? 'Server error'))
+  }
+  regLoading.value = false
+}
+
+const getListLoading = ref(true)
+const data = ref([]) as Ref<Passkey[]>
+
+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(id: number) {
+  passkey.remove(id).then(() => {
+    getList()
+    message.success($gettext('Remove successfully'))
+  }).catch((e: { message?: string }) => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
+}
+
+function addPasskey() {
+  addPasskeyModelOpen.value = true
+  passkeyName.value = ''
+}
+</script>
+
+<template>
+  <div>
+    <div class="flex justify-between items-center">
+      <h3 class="mb-0">
+        {{ $gettext('Passkey') }}
+      </h3>
+      <AButton @click="addPasskey">
+        {{ $gettext('Add a passkey') }}
+      </AButton>
+    </div>
+    <AList
+      class="mt-4"
+      bordered
+      :data-source="data"
+    >
+      <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"
+              >
+                <EditOutlined />
+              </AButton>
+
+              <APopconfirm
+                :title="$gettext('Are you sure to delete this passkey immediately?')"
+                @confirm="() => remove(item.id)"
+              >
+                <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>
+
+    <AModal
+      v-model:open="addPasskeyModelOpen"
+      :title="$gettext('Add a passkey')"
+      centered
+      :mask="false"
+      :mask-closable="false"
+      :closable="false"
+      :confirm-loading="regLoading"
+      @ok="registerPasskey"
+    >
+      <AForm layout="vertical">
+        <AFormItem :label="$gettext('Name')">
+          <AInput v-model:value="passkeyName" />
+        </AFormItem>
+      </AForm>
+    </AModal>
+  </div>
+</template>
+
+<style scoped lang="less">
+
+</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>

+ 49 - 2
app/src/components/SetLanguage/SetLanguage.vue

@@ -1,10 +1,19 @@
 <script setup lang="ts">
-import { watch } from 'vue'
-
+import dayjs from 'dayjs'
 import { useSettingsStore } from '@/pinia'
 import gettext from '@/gettext'
 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 route = useRoute()
@@ -29,6 +38,44 @@ watch(current, v => {
 
   document.title = `${name()} | Nginx UI`
 })
+
+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>
 
 <template>

+ 48 - 2
app/src/views/other/Login.vue

@@ -1,14 +1,15 @@
 <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 { useCookies } from '@vueuse/integrations/useCookies'
+import { startAuthentication } from '@simplewebauthn/browser'
 import { useUserStore } from '@/pinia'
 import auth from '@/api/auth'
 import install from '@/api/install'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
 import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
-import gettext from '@/gettext'
+import gettext, { $gettext } from '@/gettext'
 
 const thisYear = new Date().getFullYear()
 
@@ -146,6 +147,34 @@ function handleOTPSubmit(code: string, recovery: string) {
     onSubmit()
   })
 }
+const passkeyLoginLoading = ref(false)
+async function passkeyLogin() {
+  passkeyLoginLoading.value = true
+  try {
+    const begin = await auth.begin_passkey_login()
+    const asseResp = await startAuthentication(begin.options.publicKey)
+
+    console.log(asseResp)
+
+    const r = await auth.finish_passkey_login({
+      session_id: begin.session_id,
+      options: asseResp,
+    })
+
+    if (r.token) {
+      const next = (route.query?.next || '').toString() || '/'
+
+      login(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>
 
 <template>
@@ -202,10 +231,27 @@ function handleOTPSubmit(code: string, recovery: string) {
                 block
                 html-type="submit"
                 :loading="loading"
+                class="mb-2"
                 @click="onSubmit"
               >
                 {{ $gettext('Login') }}
               </AButton>
+
+              <div class="flex flex-col justify-center">
+                <ADivider>
+                  <div class="text-sm font-normal opacity-75">
+                    {{ $gettext('Or') }}
+                  </div>
+                </ADivider>
+
+                <AButton
+                  :loading="passkeyLoginLoading"
+                  @click="passkeyLogin"
+                >
+                  <KeyOutlined />
+                  {{ $gettext('Sign in with a passkey') }}
+                </AButton>
+              </div>
             </AFormItem>
           </AForm>
           <div class="footer">

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

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

+ 4 - 4
app/src/views/preference/components/TOTP.vue

@@ -87,15 +87,15 @@ function reset2FA() {
 
 <template>
   <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('To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone.') }}</p>
     <p>{{ $gettext('Scan the QR code with your mobile phone to add the account to the app.') }}</p>
     <p v-if="!status">
-      {{ $gettext('Current account is not enabled 2FA.') }}
+      {{ $gettext('Current account is not enabled TOTP.') }}
     </p>
     <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>
 
     <AAlert
@@ -121,7 +121,7 @@ function reset2FA() {
       ghost
       @click="clickEnable2FA"
     >
-      {{ $gettext('Enable 2FA') }}
+      {{ $gettext('Enable TOTP') }}
     </AButton>
     <AButton
       v-if="status && !resetting"

+ 5 - 1
go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/BurntSushi/toml v1.4.0
 	github.com/caarlos0/env/v11 v11.2.2
 	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/dgraph-io/ristretto v0.1.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-co-op/gocron v1.37.0
 	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/google/uuid v1.6.0
 	github.com/gorilla/websocket v1.5.3
@@ -100,6 +102,7 @@ require (
 	github.com/bytedance/sonic/loader v0.2.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.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/cloudflare/cloudflare-go v0.104.0 // 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-sql-driver/mysql v1.8.1 // 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/gofrs/flock v0.12.1 // 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/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 // 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/s2a-go v0.1.8 // 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/vmihailenco/msgpack/v5 v5.4.1 // 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/x448/float16 v0.8.4 // 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.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 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.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
 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.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
 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/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA=
 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.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.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.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.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/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
 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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 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/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 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-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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
 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.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
 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/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
 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/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/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/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 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.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 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/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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 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/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/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/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/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
 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/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
 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-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 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/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
 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/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/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/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/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
 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-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-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/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
 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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 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.1.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.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 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.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.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/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 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/go.mod h1:NsUw4l8QrLdIofRg1NYFBbW5ZERnmbZykVBszPZLORM=
 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-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/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/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
 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.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.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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 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/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/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/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
 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/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.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.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/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 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/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/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/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/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
 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.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
 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/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/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
 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/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
 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/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
 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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 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/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
 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/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/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/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+HeUMfHm2o=
 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/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/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/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/go.mod h1:9dt2V80cfJGRZA+5SKP3Ky+R/DxH02XfKObi2Uy2uPc=
 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.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 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/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/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/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/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
 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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 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/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 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/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 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-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-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/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
 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.8.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/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 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/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
 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.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 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/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 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-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.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/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 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.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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 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.3.0/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.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.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/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 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/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
 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.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 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/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
 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-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-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/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/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/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 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.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 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/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=
@@ -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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 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/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/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 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.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 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/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
 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/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/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
 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.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 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/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/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 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/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 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) {
 	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/cron"
 	"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/model"
 	"github.com/0xJacky/Nginx-UI/query"
@@ -50,6 +51,7 @@ func InitAfterDatabase() {
 		cron.InitCronJobs,
 		cluster.RegisterPredefinedNodes,
 		analytic.RetrieveNodesStatus,
+		passkey.Init,
 	}
 
 	for _, v := range syncs {

+ 2 - 2
internal/kernal/skip_install.go

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

+ 1 - 1
internal/middleware/secure_session.go

@@ -14,7 +14,7 @@ func RequireSecureSession() gin.HandlerFunc {
 			c.Next()
 			return
 		}
-		cUser := u.(*model.Auth)
+		cUser := u.(*model.User)
 		if !cUser.EnabledOTP() {
 			c.Next()
 			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")
 )
 
-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()
 	if err != nil {

+ 1 - 1
internal/user/otp.go

@@ -19,7 +19,7 @@ var (
 	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 != "" {
 		decrypted, err := crypto.AesDecrypt(user.OTPSecret)
 		if err != nil {

+ 7 - 7
internal/user/user.go

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

+ 35 - 6
model/auth.go

@@ -1,8 +1,12 @@
 package model
 
-import "gorm.io/gorm"
+import (
+	"github.com/go-webauthn/webauthn/webauthn"
+	"github.com/spf13/cast"
+	"gorm.io/gorm"
+)
 
-type Auth struct {
+type User struct {
 	Model
 
 	Name       string `json:"name"`
@@ -18,11 +22,36 @@ type AuthToken struct {
 	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) AfterFind(_ *gorm.DB) error {
+	u.Enabled2FA = u.EnabledOTP()
+	return nil
+}
+
+func (u *User) EnabledOTP() bool {
 	return len(u.OTPSecret) != 0
 }
+
+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 {
 	return []any{
 		ConfigBackup{},
-		Auth{},
+		User{},
 		AuthToken{},
 		Cert{},
 		ChatGPTLog{},
@@ -37,6 +37,7 @@ func GenerateAllModel() []any {
 		AcmeUser{},
 		BanIP{},
 		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"
 )
 
-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
 	ID        field.Int
@@ -58,34 +58,34 @@ type auth struct {
 	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 {
 		return nil, false
 	}
@@ -93,32 +93,32 @@ func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 	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")
-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 generateSQL strings.Builder
@@ -126,14 +126,14 @@ func (a authDo) FirstByID(id int) (result *model.Auth, err error) {
 	generateSQL.WriteString("id=? ")
 
 	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
 
 	return
 }
 
 // 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 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=? ")
 
 	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
 
 	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 {
 		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
 // 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 {
 		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
 	} 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
 	} 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
 	} 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...) }()
 		return fc(tx, batch)
 	})
 	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 {
-		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 {
-		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
 	} 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
 	} 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 {
 		return
 	}
@@ -350,29 +350,29 @@ func (a authDo) FindByPage(offset int, limit int) (result []*model.Auth, count i
 		return
 	}
 
-	count, err = a.Offset(-1).Limit(-1).Count()
+	count, err = u.Offset(-1).Limit(-1).Count()
 	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 {
 		return
 	}
 
-	err = a.Offset(offset).Limit(limit).Scan(result)
+	err = u.Offset(offset).Limit(limit).Scan(result)
 	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 (
 	Q             = new(Query)
 	AcmeUser      *acmeUser
-	Auth          *auth
 	AuthToken     *authToken
 	BanIP         *banIP
 	Cert          *cert
@@ -28,14 +27,15 @@ var (
 	DnsCredential *dnsCredential
 	Environment   *environment
 	Notification  *notification
+	Passkey       *passkey
 	Site          *site
 	Stream        *stream
+	User          *user
 )
 
 func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	*Q = *Use(db, opts...)
 	AcmeUser = &Q.AcmeUser
-	Auth = &Q.Auth
 	AuthToken = &Q.AuthToken
 	BanIP = &Q.BanIP
 	Cert = &Q.Cert
@@ -45,15 +45,16 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	DnsCredential = &Q.DnsCredential
 	Environment = &Q.Environment
 	Notification = &Q.Notification
+	Passkey = &Q.Passkey
 	Site = &Q.Site
 	Stream = &Q.Stream
+	User = &Q.User
 }
 
 func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 	return &Query{
 		db:            db,
 		AcmeUser:      newAcmeUser(db, opts...),
-		Auth:          newAuth(db, opts...),
 		AuthToken:     newAuthToken(db, opts...),
 		BanIP:         newBanIP(db, opts...),
 		Cert:          newCert(db, opts...),
@@ -63,8 +64,10 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		DnsCredential: newDnsCredential(db, opts...),
 		Environment:   newEnvironment(db, opts...),
 		Notification:  newNotification(db, opts...),
+		Passkey:       newPasskey(db, opts...),
 		Site:          newSite(db, opts...),
 		Stream:        newStream(db, opts...),
+		User:          newUser(db, opts...),
 	}
 }
 
@@ -72,7 +75,6 @@ type Query struct {
 	db *gorm.DB
 
 	AcmeUser      acmeUser
-	Auth          auth
 	AuthToken     authToken
 	BanIP         banIP
 	Cert          cert
@@ -82,8 +84,10 @@ type Query struct {
 	DnsCredential dnsCredential
 	Environment   environment
 	Notification  notification
+	Passkey       passkey
 	Site          site
 	Stream        stream
+	User          user
 }
 
 func (q *Query) Available() bool { return q.db != nil }
@@ -92,7 +96,6 @@ func (q *Query) clone(db *gorm.DB) *Query {
 	return &Query{
 		db:            db,
 		AcmeUser:      q.AcmeUser.clone(db),
-		Auth:          q.Auth.clone(db),
 		AuthToken:     q.AuthToken.clone(db),
 		BanIP:         q.BanIP.clone(db),
 		Cert:          q.Cert.clone(db),
@@ -102,8 +105,10 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		DnsCredential: q.DnsCredential.clone(db),
 		Environment:   q.Environment.clone(db),
 		Notification:  q.Notification.clone(db),
+		Passkey:       q.Passkey.clone(db),
 		Site:          q.Site.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{
 		db:            db,
 		AcmeUser:      q.AcmeUser.replaceDB(db),
-		Auth:          q.Auth.replaceDB(db),
 		AuthToken:     q.AuthToken.replaceDB(db),
 		BanIP:         q.BanIP.replaceDB(db),
 		Cert:          q.Cert.replaceDB(db),
@@ -129,14 +133,15 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		DnsCredential: q.DnsCredential.replaceDB(db),
 		Environment:   q.Environment.replaceDB(db),
 		Notification:  q.Notification.replaceDB(db),
+		Passkey:       q.Passkey.replaceDB(db),
 		Site:          q.Site.replaceDB(db),
 		Stream:        q.Stream.replaceDB(db),
+		User:          q.User.replaceDB(db),
 	}
 }
 
 type queryCtx struct {
 	AcmeUser      *acmeUserDo
-	Auth          *authDo
 	AuthToken     *authTokenDo
 	BanIP         *banIPDo
 	Cert          *certDo
@@ -146,14 +151,15 @@ type queryCtx struct {
 	DnsCredential *dnsCredentialDo
 	Environment   *environmentDo
 	Notification  *notificationDo
+	Passkey       *passkeyDo
 	Site          *siteDo
 	Stream        *streamDo
+	User          *userDo
 }
 
 func (q *Query) WithContext(ctx context.Context) *queryCtx {
 	return &queryCtx{
 		AcmeUser:      q.AcmeUser.WithContext(ctx),
-		Auth:          q.Auth.WithContext(ctx),
 		AuthToken:     q.AuthToken.WithContext(ctx),
 		BanIP:         q.BanIP.WithContext(ctx),
 		Cert:          q.Cert.WithContext(ctx),
@@ -163,8 +169,10 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		DnsCredential: q.DnsCredential.WithContext(ctx),
 		Environment:   q.Environment.WithContext(ctx),
 		Notification:  q.Notification.WithContext(ctx),
+		Passkey:       q.Passkey.WithContext(ctx),
 		Site:          q.Site.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
+}

+ 2 - 0
settings/settings.go

@@ -29,6 +29,7 @@ var sections = map[string]interface{}{
 	"cluster":   &ClusterSettings,
 	"auth":      &AuthSettings,
 	"crypto":    &CryptoSettings,
+	"webauthn":  &WebAuthnSettings,
 }
 
 func init() {
@@ -66,6 +67,7 @@ func Setup() {
 	parseEnv(&LogrotateSettings, "LOGROTATE_")
 	parseEnv(&AuthSettings, "AUTH_")
 	parseEnv(&CryptoSettings, "CRYPTO_")
+	parseEnv(&WebAuthnSettings, "WEBAUTHN_")
 
 	// 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.

+ 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_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"
 	Setup()
 
@@ -98,6 +102,10 @@ func TestSetup(t *testing.T) {
 	assert.Equal(t, "logrotate /custom/logrotate.conf", LogrotateSettings.CMD)
 	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.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{}